1. 程式人生 > >遺傳演算法(GA)解決旅行家問題(TSP)

遺傳演算法(GA)解決旅行家問題(TSP)

問題引入

旅行家問題(Travelling salesman problem,TSP)是這樣一個問題:給定一系列城市以及每兩個城市之間的距離,要求從某一點出發經過其餘每一個點之後任回到該點,在保證每個點只能經過一次的情況下,求產生最短路徑的城市序列。從圖論的角度來看,該問題實質是在一個帶權完全無向圖中,找一個權值最小的哈密爾頓(Hamilton)迴路。該問題屬於NP完全問題,隨著城市數量的增加,任何TSP演算法最壞情況下的執行時間都有可能展示出超高(指數)級別的增長。早期的研究者使用精確演算法求解該問題,常用的方法包括:分枝定界法、線性規劃法、動態規劃法等。但是,隨著問題規模的增大,精確演算法將變得無能為力,因此,在後來的研究中,國內外學者重點使用近似演算法或啟發式演算法,主要有遺傳演算法、模擬退火法、蟻群演算法、禁忌搜尋演算法、貪婪演算法和神經網路等。

本文概述

本文采用遺傳演算法(Genetic Algorithm)解決旅行家問題。本文首先通過一個例子講解了什麼是遺傳演算法,然後依次給出了相關的資料結構和關鍵功能的程式碼實現。文章末給出了全部程式碼和資料集。
本文並沒有做資料視覺化,有興趣的小夥伴可以動手試一試。

不足之處敬請指正

遺傳演算法

1. 什麼是遺傳演算法?

遺傳演算法模擬物競天擇的生物進化過程,通過維護一個潛在解的群體執行了多方向的搜尋,並支援這些方向上的資訊構成和交換。是以面為單位的搜尋,比以點為單位的搜尋,更能發現全域性最優解。
在遺傳演算法中,有很多袋鼠,它們降落到喜瑪拉雅山脈的任意地方。這些袋鼠並不知道它們的任務是尋找珠穆朗瑪峰。但每過幾年,就在一些海拔高度較低的地方射殺一些袋鼠,並希望存活下來的袋鼠是多產的,在它們所處的地方生兒育女。或者換個說法。從前,有一大群袋鼠,它們被莫名其妙的零散地遺棄於喜馬拉雅山脈。於是只好在那裡艱苦的生活。海拔低的地方瀰漫著一種無色無味的毒氣,海拔越高毒氣越稀薄。可是可憐的袋鼠們對此全然不覺,還是習慣於活蹦亂跳。於是,不斷有袋鼠死於海拔較低的地方,而越是在海拔高的袋鼠越是能活得更久,也越有機會生兒育女。就這樣經過許多年,這些袋鼠們竟然都不自覺地聚攏到了一個個的山峰上,可是在所有的袋鼠中,只有聚攏到珠穆朗瑪峰的袋鼠被帶回了美麗的澳洲。

說到底,遺傳演算法就是不斷通過操作父代得到子代,再通過父代子代更新父代的一個迴圈往復從而得到更優解的一個過程。

2. 遺傳演算法的大體流程

下文會用到的相關“術語”(個人喜好):

  • 種群:相同型別的個體的集合叫做種群
  • 群體:相同代個體的集合叫做群體,例如父代群體和子代群體
  • 個體: 參與遺傳的最小單位;表現性的承載者
  • 表現型:個體所表示在外的特徵或是狀態
  • 基因型:個體基因的具體序列;基因型決定表現性,也就是多個基因型可能會導致同一種表現型
  • 繁殖: 一次繁殖可包括交叉和變異
  • 選擇: 對父代群體進行選擇個體操作
  • 交叉: 選取父代群體中的兩個個體(父親和母親)進行交叉遺傳交換基因操作
  • 變異: 對子代進行基因突變的操作
  • 擇優:選取優秀個體,使其進入下一次繁殖
  • 適應度:個體適應當前環境的程度

步驟如下:

  1. 初始化種群,生成父代群體
  2. 對父代進行選擇操作(輪盤賭策略)
  3. 用父代個體進行繁殖操作(交叉和變異)
  4. 進行擇優操作:綜合父代群體和子代群體選取優秀個體
  5. 若最優個體不滿足要求(或未達到最大迭代次數),返回至第2步;否則停止迴圈,輸出。

詳解:

  1. 在TSP問題中,每個個體的染色體其實就是一個特定的城市序列。
  2. 為了保證表現型越好(路徑越短)的個體適應度越高,我們用路徑長度的倒數作為個體的適應度。
  3. 染色體中的每個基因就是一個城市,每個城市攜帶著其編號與座標資訊。
  4. 輪盤賭策略是一種累加選擇的方式,依次累加個體的概率直到概率和達到期望值,則選取最後一個累加的個體(下文講解)。
  5. 在TSP問題中,交叉操作交換基因時會引起衝突(一個基因在染色體中出現多次),這時要解決衝突(下文講解)。

各引數設定推薦(個人經驗):

變異概率 交叉概率 種群大小 繁殖次數
0.01 - 0.1 0.8 - 1.0 200 - 1000 2000 - 10000

3.大體流程程式碼實現(程式框架)

首先,我們需要從檔案中讀取資料,進行初始化操作,這裡可能會需要以下函式:

private void init();
    |
    | --- private List<City> getData(); //從檔案中讀取資訊
    |
    | --- private void initEntities(); //初始化群體
    ·       |
    ·       | --- private boolean isExist(); //判斷個體是否已經出現過
    ·       | 
    ·       | --- private void calLength(); //計算個體的路徑長度
    ·       |
    ·       | --- private void setReproducePercent(); //計算個體輪盤賭策略中的概率

做好初始化工作之後,就可以開始進行繁殖操作了,可能會用到以下函式:

private void genetic(); //一次繁殖操作
    |
    | --- private void hybridization(); //一次雜交操作
    ·       |
    ·       | --- private Entity getEntity(); //從父代中選取一個個體(輪盤賭策略 - 選擇操作)
    ·       |
    ·       | --- private void swapAndHandleConflict(); //交換基因並解決衝突(交叉操作)
    ·
    |
    | --- private void variable(); //變異子代(變異操作)
    |
    | --- private void updateEntity(); //精英保留,更新父代資訊
    ·       | 
    ·       | --- this.setReproducePercent(); //對新一代父代重新計算其個體參與輪盤賭策略的概率
    |
    | --- private void choose(); //選取記錄最優個體

這就是整個流程的大體框架,程式設計風格因人而異,可能有些人不喜歡這樣的設計方式,不過還是推薦大家先構思好程式的框架再編寫邏輯程式碼。

接下來就解決兩個較難理解的點:

  1. 輪盤賭策略
  2. 衝突解決

輪盤賭策略

輪盤賭策略圖解

輪盤賭策略是一個累加選取的過程:

首先,我們維護一個累加概率p,將其初始化為0.0:p = 0.0。依次遍歷每個節點 A - E,A加上累加概率p的值為12.5% + 0 = 12.5%:12.5% < 37.5%;目前未達到目標值 37.5%,所以我們接著向下遍歷,B加上累加概率p的值為 12.5% + 12.5% = 25%:25% < 37.5%;continue;C加上累加概率p的值為 25% + 37.5% = 62.5%:62.5% > 37.5%。到此為止,我們的累加概率為 62.5%,已經滿足了選擇概率(即大於選擇概率),所以我們就選擇節點C。這就是一次完整的輪盤賭策略的選擇。

在TSP問題中,我們需要得到每個個體的選擇概率(也就是當前個體在總個體中被選擇的概率)。如前文所說,為了保證滿足總路徑越短的個體適應度越高,我們使用路徑的倒數作為適應度。而現在,為了保證滿足總路徑越短的個體被挑選則概率越大,我們也可以使用路徑的倒數,在對其進行歸一化處理之後,作為其選擇概率。

歸一化處理,指的就是將所有個體的適應度求和作為“單位1”,然後就可以計算出每個個體的選擇概率,也就是個體適應度在“單位一”中所佔的比重。例如現在有三個個體 A、B、C,其適應度分別為a、b、c,“單位一”就是 a+b+c,而個體 A 的選擇概率就是 a/(a+b+c),個體 B、C 的選擇概率依次類推。

衝突解決

現在,我們假設有兩個染色體:

染色體R1 : 1 2 3 | 4 5 6 | 7 8

染色體R2 : 5 1 8 | 3 4 7 | 6 2

現在選擇第四個到第六個間的基因片段(‘ | ’之內的片段)進行交叉互換,互換完後染色體分別為:

染色體R1 : 1 2 3 | 3 4 7 | 7 8

染色體R2 : 5 1 8 | 4 5 6 | 6 2

對於R1來說,‘3’、‘7’出現了不止一次,對於R2來說,‘5’、‘6’出現了不止一次。這就出現了衝突。也就是說衝突就是交換基因片段外(下文簡稱為“片段外”)中存在著交換基因片段內(下文簡稱為“片段內”)的基因,也就是說衝突存在的地方是片段外。例如:對於R1,片段外(1 2 3 7 8 )中存在著片段內(3 4 7)中的基因‘3’和基因‘7’。
所以欲解決衝突,我們需要將片段外的重複基因替換為沒有出現過的基因,以下提供兩種解決衝突的方法:

方法一:

對於R1中的重複基因‘3’而言:重複基因‘3’在片段內對應的R2的基因為‘4’,所以我們將R1片段外的重複基因‘3’換成基因‘4’,而我們發現R1片段記憶體在基因‘4’,且基因‘4’對應著R2的基因‘5’,所以我們接著把剛剛換出的基因‘4’換成基因‘5’,因為基因‘5’並未在R1片段內出現,所以本次交換完畢,向後遍歷找到下一個重複基因並完成類似操作。

圖解解決衝突操作
交叉互換後:
R11 2 3 | 3 4 7 | 7 8  // 片段內'3'對應'4'
              |       
R25 1 8 | 4 5 6 | 6 2
第一次對 ‘3’ 進行操作後:
R11 2 4 | 3 4 7 | 7 8  // '3' 換成了 '4' - 該基因仍存在衝突, 片段內 '4' 對應 '5'
                |       
R25 1 8 | 4 5 6 | 6 2
第二次對 ‘3’(也就是 ‘4’ ) 進行操作後:
R11 2 5 | 3 4 7 | 7 8  // '4' 換成了 '5' - 該基因衝突解決完畢

R25 1 8 | 4 5 6 | 6 2

找到下一個衝突基因並以相同的方法解決衝突…

方法二:

對於R1, 我們可以發現其缺少了基因‘5’‘6’,遍歷R1,我們可以發現基因‘3’和基因‘7’是重複基因,所以我們把‘3’和‘7’換成‘5’和‘6’就行。

圖解解決衝突操作
交叉互換後:
          ^           ^
R1 : 1 2 3 | 3 4 7 | 7 8  // '1 2 3 4 5 6 7 8' - '1 2 3 4 7 8' = '5 6'

R2 : 5 1 8 | 4 5 6 | 6 2
修改重複基因 ‘3’ ‘7’ 後:
R11 2 5 | 3 4 7 | 6 8  // '3' -> '5'   '7' -> '6' 該染色體衝突解決  

R25 1 8 | 4 5 6 | 6 2

看到這兒,有人不禁會問了,有沒有一種可以實現0衝突的交叉方法?方法當然是有的,如下:

0衝突方法:

對於交換完的兩個染色體:

R1 : _ _ _ | 3 4 7 | _ _  // 橫線上依次應是 '1 2 3 7 8' 

R2 : _ _ _ | 4 5 6 | _ _  // 橫線上依次應是 '5 1 8 6 2'

現在我們把R1橫線上應存在的數倒序放到R2的橫線上(R2同理)。因此,R1、R2就成了這個樣子:

R1 : 2 6 8 | 3 4 7 | 1 5

R2 : 8 7 3 | 4 5 6 | 2 1

是不是可以驚奇的發現。衝突並不存在,因為R1、R2實際上就是保持片段內基因順序不變,把剩餘基因倒序重新排放了,所以當然不會產生衝突!至於該方法是否符合你所認為的“交叉互換”就仁者見仁智者見智了。不過在我看來,子代保留了父代的相關基因(片段內基因)並有屬於自己的基因(倒序的基因)就已經算得上是一次真正的繁殖了。

下面我們就開始填充框架,完成核心程式碼的實現。

4.相關資料結構和關鍵功能具體實現

為了方便對於個體染色體的基因進行一系列的操作,我們維護了一個List<基因>用於存放基因的相關資訊,而在其餘任何地方對基因物件class的操作都轉化為對基因編號int的操作以避免操作物件時可能出現的一系列問題(引用賦值的弊端)。

(1) 資料結構

個體:
個體由染色體唯一辨別。在TSP問題中,染色體就是走完所有城市所產生的一個城市序列。每個個體的染色體(城市序列)確定了其路徑長度(適應度)。

/**
 * 個體類
 *  包含 :
 *      1. 個體的基因序列(城市序列,為避免物件操作,只儲存城市序號)
 *      2. 路徑長度
 *      3. 適應度
 */
class Entity {

    public List<Integer> cities;  //個體的城市有序序列-染色體
    public double totalLength; //個體的路徑總長度-倒數為適應度
    public double reproducePercent; //個體參與繁殖的概率-參與輪盤賭策略的值

}

基因:
個體的染色體由基因組成。在TSP問題中,每個基因就是一個城市節點,每個節點至少儲存了該城市的座標資訊。

/**
 * 城市類
 *  包含 :
 *      1. 城市編號(從1開始)
 *      2. 城市橫縱座標
 */
class City {

    public int no; //序號
    public int x;  //橫座標
    public int y;  //縱座標

}
(2) 關鍵功能
相關常數設定
private final double variable_percent = 0.1; //變異概率
private final double hybridization_percent = 1.0; //雜交概率
private final int inheritance_number = 10000; //遺傳次數

private final int city_number = 48; //城市數量
private final int entity_number = 1000; //個體數量
private final String data_path = "[FILE_PATH]"; //資料集檔案 att48 - 10628
private final String data_separator = " "; //資料分隔符
初始化相關

初始化函式:

/**
 * 初始化函式
 * 根據城市的數量初始化城市節點 - 從data中讀取每個城市的座標,封裝到一個城市實體中
 * 根據個體數量初始化個體 - 建立一個城市list, 每次shuffle, 並保證沒有相同的個體存在
 * @throws IOException 檔案讀取異常
 */
private void init() throws IOException {
    List<City> cityList = this.readData(); //讀取所有城市資訊
    this.cityInfo = new ArrayList<>();
    this.cityInfo.addAll(cityList); //儲存所有城市資訊
    List<Integer> cityNo = new ArrayList<>();
    for(int i = 0; i < cityList.size(); ++i) {
        cityNo.add(cityList.get(i).no); //對城市操作時,只操作城市的序號 (int方便操作-比較/替換)
    }
    this.initEntities(cityNo); //初始化個體
}

讀取檔案函式:

/**
 * 從檔案中讀取城市資料
 */
private List<City> readData() throws IOException {
    List<City> cityList = new ArrayList<>();
    BufferedReader br = new BufferedReader(new FileReader(new File(this.data_path)));
    String s;
    while((s = br.readLine()) != null) { //資料為 '序號 橫座標 縱座標' - 上文已給出 資料分隔符為' '
        int no = Integer.valueOf(s.split(this.data_separator)[0]);
        int x = Integer.valueOf(s.split(this.data_separator)[1]);
        int y = Integer.valueOf(s.split(this.data_separator)[2]);
        cityList.add(new City(no, x, y));
    }
    return cityList;
}

初始化個體函式:
為了避免物件操作,我在組建個體染色體存放城市節點時,只存放城市的編號,並且維護了一個存放了城市所有資訊的List<City>,所以通過城市的編號i就可以通過list.get(i - 1)來得到城市的所有資訊。

/**
 * 初始化個體, 初始化每個個體的城市列表, 設定路徑長度等
 * @param cityNo 所有城市列表(僅有城市編號)
 */
private void initEntities(List<Integer> cityNo) {
    this.entities = new Entity[this.entity_number];
    for(int i = 0; i < this.entities.length; ++i) {
        Collections.shuffle(cityNo); //隨機打亂序列
        while(isExist(cityNo, entities)) { //保證個體不重複
            Collections.shuffle(cityNo);
        }
        this.entities[i] = new Entity(cityNo); //new個體
        this.entities[i].totalLength = this.calLength(cityNo); //計算路徑長度
    }
    this.setReproducePercent(); //計算參與輪盤賭選擇的概率
}

/**
 * 判斷城市序列是否存在於一個個體陣列中(個體由染色體也就是城市序列唯一辨別)
 * @param randomCity 隨機的城市列表
 * @param entities 個體列表(城市列表為個體的屬性)
 * @return true 存在/ false 不存在
 */
private boolean isExist(List<Integer> randomCity, Entity[] entities) {
    for(int i = 0; i < entities.length; ++i) {
        if(entities[i] == null) {
            return false;
        } else {
            List<Integer> target = entities[i].cities;
            for(int j = 0; j < target.size(); ++j) {
                if(randomCity.get(j) != target.get(j)) { //有一位不同即不同
                    return false;
                }
            }
            return true; //遍歷完 全都相同即相同
        }
    }
    return false;
}

/**
 * 計算個體的路徑距離
 * @param cityNo 個體的城市列表
 * @return double-距離
 */
private double calLength(List<Integer> cityNo) {
    double totalLength = 0.0;
    for(int i = 0; i < cityNo.size(); ++i) {
        if(i == 0) {
            totalLength += this.getDistance(cityNo.get(0), cityNo.get(cityNo.size() - 1)); //第一個到最後一個城市兩點間的距離
        } else {
            totalLength += this.getDistance(cityNo.get(i), cityNo.get(i - 1)); //當前一個到前面一個城市兩點間的距離
        }
    }
    return totalLength;
}

/**
 * 設定每個個體被選擇繁殖的概率(根據路徑長短歸一化)
 * 例如有三個適應度 a b c 
 * 則 a 被選擇的概率為 a / (a + b + c), 以此類推...
 */
private void setReproducePercent() {
    double sumLengthToOne = 0.0;
    for(Entity entity : this.entities) {
        sumLengthToOne += 1 / entity.totalLength;
    }
    for(Entity entity : this.entities) {
        entity.reproducePercent = (1 / entity.totalLength) / sumLengthToOne;
    }
}
交叉相關函式

交叉之前要進行選擇操作,也就是通過輪盤賭策略選擇將參加交叉的父母雙親。我的選擇操作體現在getEntity(),每次進行了選擇操作之後再進行交叉操作。當然也可以進行完所有的選擇操作之後再進行交叉操作。並且,交叉的時候要保證父母雙親不是同一個體,不發生自交。

/**
 * 一次雜交
 *      1. 選取父母
 *      2. 選取交換片段
 *      3. 解決衝突
 */
private void oneHybridization() {
    Entity father = this.getEntity(); //父親 選擇操作
    Entity mother = this.getEntity(); //母親 選擇操作
    Entity best = this.getBestParent(); //最優個體不參與交叉互換 - 個人偏好(可有可無)
    while(father == null || father.equals(best)) {
        father = this.getEntity();
    }
    int cnt = 0;
    while(mother == null || father.equals(mother) || mother.equals(best)) {
        if(cnt > this.entity_number / 2) {
            break;
        }
        cnt++;
        mother = this.getEntity(); //直到父母不等,保證不發生自交
    }
    Random random = new Random(); //隨機長度基因交換
    int crossStartIndex = random.nextInt(this.city_number - 1) + 1; //基因交換起始點
    int crossEndIndex = random.nextInt(this.city_number - 1) + 1; //基因交換截止點
    while(crossStartIndex == crossEndIndex) {
        crossEndIndex = random.nextInt(this.city_number - 1) + 1; //起止不相等
    }
    if(crossStartIndex > crossEndIndex) { //交換開始結束, 使結束>開始
        int temp = crossStartIndex;
        crossStartIndex = crossEndIndex;
        crossEndIndex = temp;
    }
    Entity[] newEntity = this.swapAndNoConflict(father, mother, crossStartIndex, crossEndIndex); //兒子個體 - 採取0衝突方法
    for(Entity entity : newEntity) {
        if(entity != null && !isExist(entity.cities, this.entityListToEntityArray(this.sonEntities)) && !isExist(entity.cities, this.entities)) { //去除重複兒子個體
            this.sonEntities.add(entity); //新增到兒子列表
        }
    }
}

/**
 * 輪轉賭法獲取一個個體並返回(歸一體現在reproducePercent)
 * @return 個體
 */
private Entity getEntity() {
    Random random = new Random();
    double selectPercent = random.nextDouble();
    double distributionPercent = 0.0;
    for(int i = 0; i < this.entities.length; ++i) {
        distributionPercent += this.entities[i].reproducePercent;  // 累加選擇
        if(distributionPercent > selectPercent) {
            return this.entities[i];
        }
    }
    return null;
}

/**
 * 獲取當前entities最優個體
 * 本方法為了上文所述的保留最優個體不參與交叉操作直接複製遺傳到下一代
 * @return 最優個體
 */
private Entity getBestParent() {
    if(this.bestEntity == null) {
        Entity bestParent = this.entities[0].clone();
        for(int i = 1; i < this.entities.length; ++i) {
            if(this.entities[i].totalLength < bestParent.totalLength) {
                bestParent = this.entities[i].clone();
            }
        }
        return bestParent;
    } else {
        return this.bestEntity.clone();
    }
}

交叉操作 - 產生衝突的交換方式:

/**
 * 交換基因段並且解決衝突生成新的兩個子個體
 * @param father 父親個體
 * @param mother 母親個體
 * @param startIndex 交換起始位置
 * @param endIndex 交換截止位置
 * @return 兩個個體
 */
private Entity[] swapAndHandleConflict(Entity father, Entity mother, int startIndex, int endIndex) {
    Entity[] newEntities = new Entity[2]; //最多兩個兒子
    Entity fatherClone = father.clone(); //父母交換基因段得到兒子, 所以用克隆體
    Entity motherClone = mother.clone();
    Map<Integer, Integer> fatherCityRelation = new HashMap<>();
    Map<Integer, Integer> motherCityRelation = new HashMap<>();
    for(int i = 0; i < this.city_number; ++i) {
        if(i >= startIndex && i <= endIndex) {
            int temp = fatherClone.cities.get(i);
            fatherClone.cities.set(i, motherClone.cities.get(i));
            motherClone.cities.set(i, temp); //交換
            fatherCityRelation.put(fatherClone.cities.get(i), motherClone.cities.get(i)); //獲取對應關係, 用來解決衝突
            motherCityRelation.put(motherClone.cities.get(i), fatherClone.cities.get(i)); //父母衝突對應關係恰好相反(偷懶-增加了空間消耗)
        }
    }
    this.handleConflict(fatherClone, fatherCityRelation, startIndex, endIndex);
    this.handleConflict(motherClone, motherCityRelation, startIndex, endIndex);
    newEntities[0] = fatherClone;
    newEntities[1] = motherClone;
    return newEntities;
}

/**
 * 解決衝突, 把entity.cities中不在start-end區間內的city換成cityRelation中相同key對應的value
 * @param entity 可能存在衝突的個體
 * @param cityRelation 衝突對應關係
 * @param startIndex 起始位置
 * @param endIndex 截止位置
 */
private void handleConflict(Entity entity, Map<Integer, Integer> cityRelation, int startIndex, int endIndex) {
    while(conflictExist(entity, cityRelation, startIndex, endIndex)) {
        for(int i = 0; i < entity.cities.size(); ++i) {
            if(i < startIndex || i > endIndex) {
                int temp = entity.cities.get(i);
                if(cityRelation.containsKey(temp)) {
                    entity.cities.set(i, cityRelation.get(temp));
                }
            }
        }
    }
}

/**
 * 是否存在衝突, 即entity.cities中是否出現過cityRelation中keySet中的任一元素
 * @param entity 可能存在衝突的實體
 * @param cityRelation 衝突對應關係
 * @param startIndex 起始位置
 * @param endIndex 截止位置
 * @return true-存在 / false-不存在
 */
private boolean conflictExist(Entity entity, Map<Integer,Integer> cityRelation, int startIndex, int endIndex) {
    for(int i = 0; i < entity.cities.size(); ++i) {
        if(i < startIndex || i > endIndex) {
            if(cityRelation.containsKey(entity.cities.get(i))) {
                return true;
            }
        }
    }
    return false;
}

交叉操作 - 0衝突交換方式:

/**
 * 另一種交叉方法, 避免了衝突
 * 從過程來看 -> 更優的方法
 * 1 4 6 | 2 3 5 | 8 7  ->  3 6 8 | 1 5 4 | 7 2 實際上就是保持 _ _ _ | 1 5 4 | _ _ 順序不便變, 將 2 7 8 6 3 倒序放入橫線上, 即 3 6 8 1 5 4 7 2
 * 2 7 8 | 1 5 4 | 6 3  ->  7 8 6 | 2 3 5 | 4 1
 * @param father 父親
 * @param mother 母親
 * @param startIndex 開始位置
 * @param endIndex  截止位置
 * @return 子代陣列
 */
private Entity[] swapAndNoConflict(Entity father, Entity mother, int startIndex, int endIndex) {
    Entity[] newEntities = new Entity[2];
    Entity fatherClone = father.clone();
    Entity motherClone = mother.clone();
    List<Integer> fatherCities = new ArrayList<>();
    List<Integer> motherCities = new ArrayList<>();
    fatherCities.addAll(fatherClone.cities);
    motherCities.addAll(motherClone.cities);
    int rightIndex = this.city_number - 1;
    for(int i = 0; i < father.cities.size(); ++i) {
        if(i < startIndex || i > endIndex) {
            while(rightIndex >= startIndex && rightIndex <= endIndex ) {
                rightIndex--;
            }
            fatherClone.cities.set(i, motherCities.get(rightIndex));
            motherClone.cities.set(i, fatherCities.get(rightIndex));
            rightIndex--;
        } else {
            int temp = fatherClone.cities.get(i);
            fatherClone.cities.set(i, motherClone.cities.get(i));
            motherClone.cities.set(i, temp);
        }
    }
    newEntities[0] = fatherClone;
    newEntities[1] = motherClone;
    return newEntities;
}
變異相關函式
/**
 * 變異個體
 *      1. 選擇變異節點
 *      2. 交換
 *      3. 檢查是否重複
 * @param entity 待變異個體
 */
private void oneVariable(Entity entity) {
    Random random = new Random();
    int index1 = random.nextInt(this.city_number - 1) + 1;
    int index2 = random.nextInt(this.city_number - 1) + 1;
    while(index1 == index2) {
        index2 = random.nextInt(this.city_number); //保證突變基因不是同一個
    }
    int temp = entity.cities.get(index1);
    entity.cities.set(index1, entity.cities.get(index2));
    entity.cities.set(index2, temp);
}
擇優相關函式

綜合父代子代選取最優個體更新父代後重新計算每個個體的適應度,以參與下一次繁殖操作。

/**
 * 選擇優質個體
 *      1. 對所有個體計算路徑長短
 *      2. 排序
 *      3. 選擇較優個體
 */
private void updateEntity() {
    for(Entity entity : this.sonEntities) { //計算所有子代路徑長短
        double length = 0.0;
        List<Integer> city = entity.cities;
        for(int j = 0; j < city.size(); ++j) {
            if (j == 0) {
                length += this.getDistance(city.get(city.size() - 1), city.get(0));
            } else {
                length += this.getDistance(city.get(j), city.get(j - 1));
            }
        }
        entity.totalLength = length;
    }
    List<Entity> allEntities = new ArrayList<>();
    allEntities.addAll(this.sonEntities);
    allEntities.addAll(this.entityArrayToEntityList(this.entities));
    Collections.sort(allEntities); //排序所有個體
    List<Entity> bestEntities = new ArrayList<>();
    for(int i = 0; i < this.entity_number; ++i) {
        bestEntities.add(allEntities.get(i).clone());
    }
    Collections.shuffle(bestEntities);
    for(int i = 0; i < this.entities.length; ++i) {
        this.entities[i] = bestEntities.get(i); //選擇
    }
    this.setReproducePercent(); //重新設定選擇概率(歸一)
}
選擇最優個體儲存-評估函式
/**
 * 選擇最優解, 評估函式
 */
private void choose(int geneticNumber) {
    for(int i = 0; i < this.entities.length; ++i) {
        if(this.entities[i].totalLength < this.minLength) {
            this.bestEntity = this.entities[i].clone();
            this.bestEntity.reproducePercent = this.entities[i].reproducePercent;
            this.minLength = this.entities[i].totalLength;
            this.bestInheritance = geneticNumber;
        }
    }
}

全部程式碼以及資料集

程式碼
import java.io.*;
import java.util.*;
import java.util.List;

/**
 * use GA to solve TSP.
 */
public class TSP {

    private final double variable_percent = 0.1; //變異概率
    private final double hybridization_percent = 1.0; //雜交概率
    private final int inheritance_number = 10000; //遺傳次數

    private final int city_number = 48; //城市數量
    private final int entity_number = 1000; //個體數量
    private final String data_path = "[FILE_PATH]"; //資料集檔案 att48 - 10628
    private final String data_separator = " "; //資料分隔符

    private Entity[] entities; //個體集合 個體包含城市列表(序號),路徑長度,適應度
    private List<Entity> sonEntities; //子代集合 內容同個體

    private List<City> cityInfo = null; //所有城市資訊,僅此處存城市資訊

    private double minLength = Double.MAX_VALUE; //最短路徑
    private Entity bestEntity = null; //最佳個體
    private int bestInheritance = 0; //最佳個體出現代數

    public static void main(String[] args) {
        try {
            TSP tsp = new TSP();
            tsp.init(); //初始化資料
            System.out.println("初始資料為 : ");
            for(int i = 0; i < tsp.entities.length; ++i) {
                System.out.println(tsp.entities[i].toString() + " totalLength -> " + tsp.entities[i].totalLength + " reproducePercent -> " + tsp.entities[i].reproducePercent); //資料展示
            }
            tsp.genetic(); //開始遺傳
            System.out.println("整體最優解為 : "
                    + "\n    出現代數 : " + tsp.bestInheritance
                    + "\n    路徑長度 : " + (tsp.minLength / Math.sqrt(10.0))
                    + "\n    適應度 : " + tsp.bestEntity.reproducePercent
                    + "\n    路徑 : " + tsp.bestEntity.toString()); //結果展示
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化函式
     * 根據城市的數量初始化城市節點 - 從data中讀取每個城市的座標,封裝到一個城市實體中
     * 根據個體數量初始化個體 - 建立一個城市list, 每次shuffle, 並保證沒有相同的個體存在
     * @throws IOException 檔案讀取異常
     */
    private void init() throws IOException {
        List<City> cityList = this.readData(); //讀取所有城市資訊
        this.cityInfo = new ArrayList<>();
        this.cityInfo.addAll(cityList); //儲存所有城市資訊
        List<Integer> cityNo = new ArrayList<>();
        for(int i = 0; i < cityList.size(); ++i) {
            cityNo.add(cityList.get(i).no); //對城市操作時,只操作城市的序號 (int方便操作-比較/替換)
        }
        this.initEntities(cityNo); //初始化個體
    }

    /**
     * 從檔案中讀取城市資料
     */
    private List<City> readData() throws IOException {
        List<City> cityList = new ArrayList<>();
        BufferedReader br = new BufferedReader(new FileReader(new File(this.data_path)));
        String s;
        while((s = br.readLine()) != null) {
            int no = Integer.valueOf(s.split(this.data_separator)[0]);
            int x = Integer.valueOf(s.split(this.data_separator)[1]);
            int y = Integer.valueOf(s.split(this.data_separator)[2]);
            cityList.add(new City(no, x, y));
        }
        return cityList;
    }

    /**
     * 初始化個體, 初始化每個個體的城市列表, 設定路徑長度, 設定適應度
     * @param cityNo 所有城市列表
     */
    private void initEntities(List<Integer> cityNo) {
        this.entities = new Entity[this.entity_number];
        for(int i = 0; i < this.entities.length; ++i) {
            Collections.shuffle(cityNo);
            while(isExist(cityNo, entities)) {
                Collections.shuffle(cityNo);
            }
            this.entities[i] = new Entity(cityNo);
            this.entities[i].totalLength = this.calLength(cityNo);
        }
        this.setReproducePercent();
    }

    /**
     * 判斷城市順序列表是否存在於一個實體陣列中(實體屬性)
     * @param randomCity 隨機的城市列表
     * @param entities 個體列表(城市列表為個體的屬性)
     * @return true 存在/ false 不存在
     */
    private boolean isExist(List<Integer> randomCity, Entity[] entities) {
        for(int i = 0; i < entities.length; ++i) {
            if(entities[i] == null) {
                return false;
            } else {
                List<Integer> target = entities[i].cities;
                for(int j = 0; j < target.size(); ++j) {
                    if(randomCity.get(j) != target.get(j)) { //有一位不同即不同
                        return false;
                    }
                }
                return true; //遍歷完 全都相同即相同
            }
        }
        return false;
    }

    /**
     * 計算個體的路徑距離
     * @param cityNo 個體的城市列表
     * @return double-距離
     */
    private double calLength(List<Integer> cityNo) {
        double totalLength = 0.0;
        for(int i = 0; i < cityNo.size(); ++i) {
            if(i == 0) {
                totalLength += this.getDistance(cityNo.get(0), cityNo.get(cityNo.size() - 1)); //第一個到最後一個
            } else {
                totalLength += this.getDistance(cityNo.get(i), cityNo.get(i - 1)); //當前一個到前面一個