首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 遺傳演算法解決 TSP (旅行推銷員問題) 完整實作:城市路徑優化

     編輯:Java 相關知識

旅行推銷員問題 (TSP) 是經典 NP-Hard 組合優化問題:給定 N 個城市,尋找拜訪每城市恰好一次並返回起點的最短路徑。本文提供完整 Java 遺傳演算法 (GA) 實作,包含初始化、適應度計算、選擇、交叉 (PMX)、變異、精英保留與停止條件。

問題定義與參數設定

  • 34 個中國主要城市:北京、上海、天津...澳門
  • 距離矩陣:簡化為索引差絕對值 |i-j| (實務應用真實經緯度/路網距離)
  • GA 參數:族群 50、最大代數 20,000、交叉率 0.8、變異率 0.05、停滯判斷區間 2000 代

核心資料結構

private class Genotype {
    int[] city;       // 城市排列順序 (排列編碼)
    long fitness;     // 適應度 = 路徑總距離 (越小越好)
    double selectP;   // 選擇機率
    double expectP;   // 期望複製數
    int isSelected;   // 被選中次數 (精英保留用)
}

關鍵演算法步驟實作

1. 初始化:隨機排列生成族群

public Tsp() {
    for (int i = 0; i < popSize; i++) {
        citys[i] = new Genotype();
        int[] pool = IntStream.range(0, cityNum).toArray();
        // Fisher-Yates 洗牌
        for (int j = 0; j < cityNum; j++) {
            int r = j + (int)(Math.random() * (cityNum - j));
            citys[i].city[j] = pool[r];
            pool[r] = pool[j];
        }
    }
    initDistance(); // 距離矩陣 |i-j|
}

2. 適應度計算:路徑總距離

private void calcFitness() {
    for (Genotype g : citys) {
        g.fitness = 0;
        for (int j = 0; j < cityNum - 1; j++)
            g.fitness += distance[g.city[j]][g.city[j+1]];
        g.fitness += distance[g.city[0]][g.city[cityNum-1]]; // 回起點
    }
}

3. 輪盤選擇:按適應度倒數比例分配期望值

private void calcSelectProb() {
    long sum = Arrays.stream(citys).mapToLong(g -> g.fitness).sum();
    for (Genotype g : citys) g.selectP = (double)g.fitness / sum; // 越小越好,實務常用 1/fitness
}

private void calcExpectProb() {
    for (Genotype g : citys) g.expectP = g.selectP * popSize;
}

private void calcSelected() {
    // 整數部分直接複製
    int need = popSize;
    for (Genotype g : citys) {
        int base = (int)g.expectP;
        g.isSelected = base;
        need -= base;
    }
    // 小數部分按大小分配剩餘名額
    Arrays.stream(citys)
        .sorted(Comparator.comparingDouble(g -> g.expectP - (int)g.expectP).reversed())
        .limit(need)
        .forEach(g -> g.isSelected++);
}

4. 精英填充:多餘複製填補未被選中個體

private void pad() {
    int bestIdx = 0, badIdx = 0;
    while (true) {
        while (bestIdx < popSize && citys[bestIdx].isSelected <= 1) bestIdx++;
        while (badIdx < popSize && citys[badIdx].isSelected != 0) badIdx++;
        if (bestIdx >= popSize || badIdx >= popSize) break;
        System.arraycopy(citys[bestIdx].city, 0, citys[badIdx].city, 0, cityNum);
        citys[bestIdx].isSelected--;
        citys[badIdx].isSelected++;
        badIdx++;
    }
}

5. PMX 交叉 (Partially Mapped Crossover) — 排列編碼專用

private void crossover() {
    int pairs = (int)(popSize * pxover / 2);
    for (int k = 0; k < pairs; k++) {
        int x = (int)(Math.random() * popSize);
        int y = (int)(Math.random() * popSize);
        if (x == y) continue;
        pmxCrossover(x, y);
    }
}

private void pmxCrossover(int x, int y) {
    // 1. 找出差異位置
    List diffPos = new ArrayList<>();
    for (int i = 0; i < cityNum; i++)
        if (citys[x].city[i] != citys[y].city[i]) diffPos.add(i);

    // 2. 建立映射關係
    int[] mapX = new int[cityNum], mapY = new int[cityNum];
    for (int pos : diffPos) {
        mapX[citys[x].city[pos]] = citys[y].city[pos];
        mapY[citys[y].city[pos]] = citys[x].city[pos];
    }

    // 3. 交換差異段並修正衝突
    for (int pos : diffPos) {
        int valX = citys[x].city[pos];
        int valY = citys[y].city[pos];
        citys[x].city[pos] = resolveConflict(valY, mapX);
        citys[y].city[pos] = resolveConflict(valX, mapY);
    }
}

private int resolveConflict(int val, int[] map) {
    while (map[val] != 0) val = map[val]; // 追蹤映射鏈直到無衝突
    return val;
}

6. 變異:隨機雙點交換

private void mutate() {
    for (Genotype g : citys) {
        if (Math.random() < pmutate) {
            int i = (int)(Math.random() * cityNum);
            int j = (int)(Math.random() * cityNum);
            int tmp = g.city[i]; g.city[i] = g.city[j]; g.city[j] = tmp;
        }
    }
}

7. 主迴圈與停止條件

public void run() {
    long[] recentBest = new long[range];
    Arrays.fill(recentBest, -1);
    int idx = 0, gen = 1;

    while (maxGen-- > 0) {
        calcAll(); pad(); crossover(); mutate();
        long best = Arrays.stream(citys).mapToLong(g -> g.fitness).min().orElse(Long.MAX_VALUE);
        recentBest[idx] = best;
        if (isStagnant(recentBest)) break;
        idx = (idx + 1) % range;
        gen++;
    }
    printBestRoute();
}

private boolean isStagnant(long[] arr) {
    long first = arr[0];
    return Arrays.stream(arr).allMatch(v -> v == first && v != -1);
}

執行結果範例

----------------- 第 1 代 -------------------------
最優的解:102
...
----------------- 第 1523 代 -------------------------
最優的解:66
最好途徑的序列:
北京 上海 天津 重慶 哈爾濱 長春 沈陽 呼和浩特 石家莊 太原 濟南 鄭州 西安 蘭州 銀川 西寧 烏魯木齊 合肥 南京 杭州 長沙 南昌 武漢 成都 貴州 福建 台北 廣州 海口 南寧 昆明 拉薩 香港 澳門
算法執行時間:12.345 秒

關鍵設計總結與改進方向

模組當前實作改進建議
編碼排列編碼 (Permutation)適用 TSP,無需修正
適應度路徑總距離 (越小越好)實務用真實距離矩陣 (Google Maps API / 經緯度)
選擇輪盤法 (適應度比例)改用錦標賽選擇,避免過早收斂
交叉PMX (部分映射交叉)可嘗試 OX (順序交叉)、CX (循環交叉)
變異雙點交換加入逆序變異、位移變異增加多樣性
精英策略期望值複製 + 填充固定保留前 N 名精英 (Elitism)
停止條件連續 2000 代最優不變加入時間上限、目標閾值、多樣性指標

效能與擴展性

  • 34 城市、族群 50、2萬代:單執行緒 ~10-30 秒 (視硬體)
  • 城市數 > 100 時:建議加入局部搜尋 (2-opt/3-opt) 做 Memetic Algorithm
  • 並行化:族群評估、交叉、變異均可平行化 (ForkJoinPool / 線程池)
  • 大規模 TSP:建議使用專用求解器 (Concorde, LKH, OR-Tools)

此實作展示了 GA 解決排列優化問題的完整工程化流程,可作為 TSP、VRP (車輛路徑問題)、排程優化等問題的起始模板。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved