一、TSP問題
TSP問題(Travelling Salesman Problem)即旅行商問題,又譯爲旅行推銷員問題、貨郎擔問題,是數學領域中著名問題之一。假設有一個旅行商人要拜訪n個城市,他必須選擇所要走的路徑,路徑的限制是每個城市只能拜訪一次,而且最後要回到原來出發的城市。路徑的選擇目標是要求得的路徑路程爲所有路徑之中的最小值。
TSP問題是一個組合優化問題。該問題可以被證明具有NPC計算複雜性。TSP問題可以分爲兩類,一類是對稱TSP問題(Symmetric TSP),另一類是非對稱問題(Asymmetric TSP)。所有的TSP問題都可以用一個圖(Graph)來描述:
V={c1, c2, …, ci, …, cn},i = 1,2, …, n,是所有城市的集合.ci表示第i個城市,n爲城市的數目;
E={(r, s): r,s∈ V}是所有城市之間連接的集合;
C = {crs: r,s∈ V}是所有城市之間連接的成本度量(一般爲城市之間的距離);
如果crs = csr, 那麼該TSP問題爲對稱的,否則爲非對稱的。
一個TSP問題可以表達爲:
求解遍歷圖G = (V, E, C),所有的節點一次並且回到起始節點,使得連接這些節點的路徑成本最低。
二、遺傳算法
遺傳算法(Genetic Algorithms )是基於生物進化理論的原理發展起來的一種廣爲應用的、高效的隨機搜索與優化的方法。其主要特點是羣體搜索策略和羣體中個體之間的信息交換,搜索不依賴於梯度信息。它是在70年代初期由美國密西根( Michigan )大學的霍蘭( Holland )教授發展起來的。1975年霍蘭教授發表了第一本比較系統論述遺傳算法的專著《自然系統與人工系統中的適應性》(《 Adaptationin Natural and Artificial Systems 》)。遺傳算法最初被研究的出發點不是爲專門解決最優化問題而設計的,它與進化策略、進化規劃共同構成了進化算法的主要框架,都是爲當時人工智能的發展服務的。迄今爲止,遺傳算法是進化算法中最廣爲人知的算法。
遺傳火算法的實施步驟如下(以目標函數求最小爲例)。
第一步:初始化 t←0進化代數計數器;T是最大進化代數;隨機生成M個個體作爲初始羣體P(t);
第二步:個體評價 計算P(t)中各個個體的適應度;
第三步:選擇運算 將選擇算子作用於羣體;
第四步:交叉運算 將交叉算子作用於羣體;
第五步:變異運算 將變異算子作用於羣體,並通過以上運算得到下一代羣體P(t + 1);
第六步:終止條件判斷 t≦T:t← t+1 轉到步驟2;t>T:終止 輸出解。
遺傳算法應用步驟:
1)確定決策變量及各種約束條件,即個體的表現型X和問題的解空間;
2)建立優化模型 (目標函數最大OR 最小) 數學描述形式 量化方法;
3)染色體編碼方法;
4)解碼方法;
5)個體適應度的量化評價方法 F(x)
6)設計遺傳算子;
7)確定有關運行參數。
三、遺傳算法求解TSP問題
其實之前不大想寫這個遺傳算法求TSP,因爲我之前已經有遺傳算法求01揹包了,但有些讀者建議,加上確實tsp與01揹包差別還是很大的,就再寫一個。對於TSP之類的問題NP問題,使用啓發式搜索算法是一個明智的選擇,因爲精確算髮已經力不從心了。
使用遺傳算法第一件事情就是確定染色編碼方式,它根據不同的問題模型使用不同編碼方式,有二進制編碼也有整數編碼和浮點數編碼,面對TSP問題,我肯定選用整數編碼,因爲很簡單,對於每個城市用一個整數來編號,例如有48個城市,就用0到47來標識每一個城市,然後一個路徑就是一條染色體編碼,染色體長度爲48,如:0,1,2,3,4...47就是一個染色體,它表達的意思就是旅行者從0號城市出發,依次訪問1,2,...47號城市再回到0號城市;遺傳算法的第二個要點就是評價函數,TSP的評價函數很簡單,就是染色體編碼表達的路徑總長度;最後很簡單,其實在這個模型中就是將0到47這48個數進行全排列,從中找出最短的一條路徑(想想48個數全排列,然後比較。。。)。清楚瞭解了這些,咱們就可以按照上面的遺傳算法框架來進行編程了。
我們使用TSP問題依然來自於tsplib上的數據att48,這是一個對稱TSP問題,城市規模爲48,其最優值爲10628.其距離計算方法下圖所示:
具體代碼如下:
package noah;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Random;
public class GA {
private int scale;// 種羣規模
private int cityNum; // 城市數量,染色體長度
private int MAX_GEN; // 運行代數
private int[][] distance; // 距離矩陣
private int bestT;// 最佳出現代數
private int bestLength; // 最佳長度
private int[] bestTour; // 最佳路徑
// 初始種羣,父代種羣,行數表示種羣規模,一行代表一個個體,即染色體,列表示染色體基因片段
private int[][] oldPopulation;
private int[][] newPopulation;// 新的種羣,子代種羣
private int[] fitness;// 種羣適應度,表示種羣中各個個體的適應度
private float[] Pi;// 種羣中各個個體的累計概率
private float Pc;// 交叉概率
private float Pm;// 變異概率
private int t;// 當前代數
private Random random;
public GA() {
}
/**
* constructor of GA
*
* @param s
* 種羣規模
* @param n
* 城市數量
* @param g
* 運行代數
* @param c
* 交叉率
* @param m
* 變異率
*
**/
public GA(int s, int n, int g, float c, float m) {
scale = s;
cityNum = n;
MAX_GEN = g;
Pc = c;
Pm = m;
}
// 給編譯器一條指令,告訴它對被批註的代碼元素內部的某些警告保持靜默
@SuppressWarnings("resource")
/**
* 初始化GA算法類
* @param filename 數據文件名,該文件存儲所有城市節點座標數據
* @throws IOException
*/
private void init(String filename) throws IOException {
// 讀取數據
int[] x;
int[] y;
String strbuff;
BufferedReader data = new BufferedReader(new InputStreamReader(
new FileInputStream(filename)));
distance = new int[cityNum][cityNum];
x = new int[cityNum];
y = new int[cityNum];
for (int i = 0; i < cityNum; i++) {
// 讀取一行數據,數據格式1 6734 1453
strbuff = data.readLine();
// 字符分割
String[] strcol = strbuff.split(" ");
x[i] = Integer.valueOf(strcol[1]);// x座標
y[i] = Integer.valueOf(strcol[2]);// y座標
}
// 計算距離矩陣
// ,針對具體問題,距離計算方法也不一樣,此處用的是att48作爲案例,它有48個城市,距離計算方法爲僞歐氏距離,最優值爲10628
for (int i = 0; i < cityNum - 1; i++) {
distance[i][i] = 0; // 對角線爲0
for (int j = i + 1; j < cityNum; j++) {
double rij = Math
.sqrt(((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j])
* (y[i] - y[j])) / 10.0);
// 四捨五入,取整
int tij = (int) Math.round(rij);
if (tij < rij) {
distance[i][j] = tij + 1;
distance[j][i] = distance[i][j];
} else {
distance[i][j] = tij;
distance[j][i] = distance[i][j];
}
}
}
distance[cityNum - 1][cityNum - 1] = 0;
bestLength = Integer.MAX_VALUE;
bestTour = new int[cityNum + 1];
bestT = 0;
t = 0;
newPopulation = new int[scale][cityNum];
oldPopulation = new int[scale][cityNum];
fitness = new int[scale];
Pi = new float[scale];
random = new Random(System.currentTimeMillis());
/*
* for(int i=0;i<cityNum;i++) { for(int j=0;j<cityNum;j++) {
* System.out.print(distance[i][j]+","); } System.out.println(); }
*/
// 初始化種羣
}
// 初始化種羣
void initGroup() {
int i, j, k;
// Random random = new Random(System.currentTimeMillis());
for (k = 0; k < scale; k++)// 種羣數
{
oldPopulation[k][0] = random.nextInt(65535) % cityNum;
for (i = 1; i < cityNum;)// 染色體長度
{
oldPopulation[k][i] = random.nextInt(65535) % cityNum;
for (j = 0; j < i; j++) {
if (oldPopulation[k][i] == oldPopulation[k][j]) {
break;
}
}
if (j == i) {
i++;
}
}
}
/*
* for(i=0;i<scale;i++) { for(j=0;j<cityNum;j++) {
* System.out.print(oldPopulation[i][j]+","); } System.out.println(); }
*/
}
public int evaluate(int[] chromosome) {
// 0123
int len = 0;
// 染色體,起始城市,城市1,城市2...城市n
for (int i = 1; i < cityNum; i++) {
len += distance[chromosome[i - 1]][chromosome[i]];
}
// 城市n,起始城市
len += distance[chromosome[cityNum - 1]][chromosome[0]];
return len;
}
// 計算種羣中各個個體的累積概率,前提是已經計算出各個個體的適應度fitness[max],作爲賭輪選擇策略一部分,Pi[max]
void countRate() {
int k;
double sumFitness = 0;// 適應度總和
double[] tempf = new double[scale];
for (k = 0; k < scale; k++) {
tempf[k] = 10.0 / fitness[k];
sumFitness += tempf[k];
}
Pi[0] = (float) (tempf[0] / sumFitness);
for (k = 1; k < scale; k++) {
Pi[k] = (float) (tempf[k] / sumFitness + Pi[k - 1]);
}
/*
* for(k=0;k<scale;k++) { System.out.println(fitness[k]+" "+Pi[k]); }
*/
}
// 挑選某代種羣中適應度最高的個體,直接複製到子代中
// 前提是已經計算出各個個體的適應度Fitness[max]
public void selectBestGh() {
int k, i, maxid;
int maxevaluation;
maxid = 0;
maxevaluation = fitness[0];
for (k = 1; k < scale; k++) {
if (maxevaluation > fitness[k]) {
maxevaluation = fitness[k];
maxid = k;
}
}
if (bestLength > maxevaluation) {
bestLength = maxevaluation;
bestT = t;// 最好的染色體出現的代數;
for (i = 0; i < cityNum; i++) {
bestTour[i] = oldPopulation[maxid][i];
}
}
// System.out.println("代數 " + t + " " + maxevaluation);
// 複製染色體,k表示新染色體在種羣中的位置,kk表示舊的染色體在種羣中的位置
copyGh(0, maxid);// 將當代種羣中適應度最高的染色體k複製到新種羣中,排在第一位0
}
// 複製染色體,k表示新染色體在種羣中的位置,kk表示舊的染色體在種羣中的位置
public void copyGh(int k, int kk) {
int i;
for (i = 0; i < cityNum; i++) {
newPopulation[k][i] = oldPopulation[kk][i];
}
}
// 賭輪選擇策略挑選
public void select() {
int k, i, selectId;
float ran1;
// Random random = new Random(System.currentTimeMillis());
for (k = 1; k < scale; k++) {
ran1 = (float) (random.nextInt(65535) % 1000 / 1000.0);
// System.out.println("概率"+ran1);
// 產生方式
for (i = 0; i < scale; i++) {
if (ran1 <= Pi[i]) {
break;
}
}
selectId = i;
// System.out.println("選中" + selectId);
copyGh(k, selectId);
}
}
//進化函數,正常交叉變異
public void evolution() {
int k;
// 挑選某代種羣中適應度最高的個體
selectBestGh();
// 賭輪選擇策略挑選scale-1個下一代個體
select();
// Random random = new Random(System.currentTimeMillis());
float r;
// 交叉方法
for (k = 0; k < scale; k = k + 2) {
r = random.nextFloat();// /產生概率
// System.out.println("交叉率..." + r);
if (r < Pc) {
// System.out.println(k + "與" + k + 1 + "進行交叉...");
//OXCross(k, k + 1);// 進行交叉
OXCross1(k, k + 1);
} else {
r = random.nextFloat();// /產生概率
// System.out.println("變異率1..." + r);
// 變異
if (r < Pm) {
// System.out.println(k + "變異...");
OnCVariation(k);
}
r = random.nextFloat();// /產生概率
// System.out.println("變異率2..." + r);
// 變異
if (r < Pm) {
// System.out.println(k + 1 + "變異...");
OnCVariation(k + 1);
}
}
}
}
//進化函數,保留最好染色體不進行交叉變異
public void evolution1() {
int k;
// 挑選某代種羣中適應度最高的個體
selectBestGh();
// 賭輪選擇策略挑選scale-1個下一代個體
select();
// Random random = new Random(System.currentTimeMillis());
float r;
for (k = 1; k + 1 < scale / 2; k = k + 2) {
r = random.nextFloat();// /產生概率
if (r < Pc) {
OXCross1(k, k + 1);// 進行交叉
//OXCross(k,k+1);//進行交叉
} else {
r = random.nextFloat();// /產生概率
// 變異
if (r < Pm) {
OnCVariation(k);
}
r = random.nextFloat();// /產生概率
// 變異
if (r < Pm) {
OnCVariation(k + 1);
}
}
}
if (k == scale / 2 - 1)// 剩最後一個染色體沒有交叉L-1
{
r = random.nextFloat();// /產生概率
if (r < Pm) {
OnCVariation(k);
}
}
}
// 類OX交叉算子
void OXCross(int k1, int k2) {
int i, j, k, flag;
int ran1, ran2, temp;
int[] Gh1 = new int[cityNum];
int[] Gh2 = new int[cityNum];
// Random random = new Random(System.currentTimeMillis());
ran1 = random.nextInt(65535) % cityNum;
ran2 = random.nextInt(65535) % cityNum;
// System.out.println();
// System.out.println("-----------------------");
// System.out.println("----"+ran1+"----"+ran2);
while (ran1 == ran2) {
ran2 = random.nextInt(65535) % cityNum;
}
if (ran1 > ran2)// 確保ran1<ran2
{
temp = ran1;
ran1 = ran2;
ran2 = temp;
}
// System.out.println();
// System.out.println("-----------------------");
// System.out.println("----"+ran1+"----"+ran2);
// System.out.println("-----------------------");
// System.out.println();
flag = ran2 - ran1 + 1;// 刪除重複基因前染色體長度
for (i = 0, j = ran1; i < flag; i++, j++) {
Gh1[i] = newPopulation[k2][j];
Gh2[i] = newPopulation[k1][j];
}
// 已近賦值i=ran2-ran1個基因
for (k = 0, j = flag; j < cityNum;)// 染色體長度
{
Gh1[j] = newPopulation[k1][k++];
for (i = 0; i < flag; i++) {
if (Gh1[i] == Gh1[j]) {
break;
}
}
if (i == flag) {
j++;
}
}
for (k = 0, j = flag; j < cityNum;)// 染色體長度
{
Gh2[j] = newPopulation[k2][k++];
for (i = 0; i < flag; i++) {
if (Gh2[i] == Gh2[j]) {
break;
}
}
if (i == flag) {
j++;
}
}
for (i = 0; i < cityNum; i++) {
newPopulation[k1][i] = Gh1[i];// 交叉完畢放回種羣
newPopulation[k2][i] = Gh2[i];// 交叉完畢放回種羣
}
// System.out.println("進行交叉--------------------------");
// System.out.println(k1+"交叉後...");
// for (i = 0; i < cityNum; i++) {
// System.out.print(newPopulation[k1][i] + "-");
// }
// System.out.println();
// System.out.println(k2+"交叉後...");
// for (i = 0; i < cityNum; i++) {
// System.out.print(newPopulation[k2][i] + "-");
// }
// System.out.println();
// System.out.println("交叉完畢--------------------------");
}
// 交叉算子,相同染色體交叉產生不同子代染色體
public void OXCross1(int k1, int k2) {
int i, j, k, flag;
int ran1, ran2, temp;
int[] Gh1 = new int[cityNum];
int[] Gh2 = new int[cityNum];
// Random random = new Random(System.currentTimeMillis());
ran1 = random.nextInt(65535) % cityNum;
ran2 = random.nextInt(65535) % cityNum;
while (ran1 == ran2) {
ran2 = random.nextInt(65535) % cityNum;
}
if (ran1 > ran2)// 確保ran1<ran2
{
temp = ran1;
ran1 = ran2;
ran2 = temp;
}
// 將染色體1中的第三部分移到染色體2的首部
for (i = 0, j = ran2; j < cityNum; i++, j++) {
Gh2[i] = newPopulation[k1][j];
}
flag = i;// 染色體2原基因開始位置
for (k = 0, j = flag; j < cityNum;)// 染色體長度
{
Gh2[j] = newPopulation[k2][k++];
for (i = 0; i < flag; i++) {
if (Gh2[i] == Gh2[j]) {
break;
}
}
if (i == flag) {
j++;
}
}
flag = ran1;
for (k = 0, j = 0; k < cityNum;)// 染色體長度
{
Gh1[j] = newPopulation[k1][k++];
for (i = 0; i < flag; i++) {
if (newPopulation[k2][i] == Gh1[j]) {
break;
}
}
if (i == flag) {
j++;
}
}
flag = cityNum - ran1;
for (i = 0, j = flag; j < cityNum; j++, i++) {
Gh1[j] = newPopulation[k2][i];
}
for (i = 0; i < cityNum; i++) {
newPopulation[k1][i] = Gh1[i];// 交叉完畢放回種羣
newPopulation[k2][i] = Gh2[i];// 交叉完畢放回種羣
}
}
// 多次對換變異算子
public void OnCVariation(int k) {
int ran1, ran2, temp;
int count;// 對換次數
// Random random = new Random(System.currentTimeMillis());
count = random.nextInt(65535) % cityNum;
for (int i = 0; i < count; i++) {
ran1 = random.nextInt(65535) % cityNum;
ran2 = random.nextInt(65535) % cityNum;
while (ran1 == ran2) {
ran2 = random.nextInt(65535) % cityNum;
}
temp = newPopulation[k][ran1];
newPopulation[k][ran1] = newPopulation[k][ran2];
newPopulation[k][ran2] = temp;
}
/*
* for(i=0;i<L;i++) { printf("%d ",newGroup[k][i]); } printf("\n");
*/
}
public void solve() {
int i;
int k;
// 初始化種羣
initGroup();
// 計算初始化種羣適應度,Fitness[max]
for (k = 0; k < scale; k++) {
fitness[k] = evaluate(oldPopulation[k]);
// System.out.println(fitness[k]);
}
// 計算初始化種羣中各個個體的累積概率,Pi[max]
countRate();
System.out.println("初始種羣...");
for (k = 0; k < scale; k++) {
for (i = 0; i < cityNum; i++) {
System.out.print(oldPopulation[k][i] + ",");
}
System.out.println();
System.out.println("----" + fitness[k] + " " + Pi[k]);
}
for (t = 0; t < MAX_GEN; t++) {
//evolution1();
evolution();
// 將新種羣newGroup複製到舊種羣oldGroup中,準備下一代進化
for (k = 0; k < scale; k++) {
for (i = 0; i < cityNum; i++) {
oldPopulation[k][i] = newPopulation[k][i];
}
}
// 計算種羣適應度
for (k = 0; k < scale; k++) {
fitness[k] = evaluate(oldPopulation[k]);
}
// 計算種羣中各個個體的累積概率
countRate();
}
System.out.println("最後種羣...");
for (k = 0; k < scale; k++) {
for (i = 0; i < cityNum; i++) {
System.out.print(oldPopulation[k][i] + ",");
}
System.out.println();
System.out.println("---" + fitness[k] + " " + Pi[k]);
}
System.out.println("最佳長度出現代數:");
System.out.println(bestT);
System.out.println("最佳長度");
System.out.println(bestLength);
System.out.println("最佳路徑:");
for (i = 0; i < cityNum; i++) {
System.out.print(bestTour[i] + ",");
}
}
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
System.out.println("Start....");
GA ga = new GA(30, 48, 1000, 0.8f, 0.9f);
ga.init("c://data.txt");
ga.solve();
}
}
運行結果截圖:
四、總結
看到上面兩個結果差別還是相當大的,其實我代碼裏提供了兩個進化函數,兩種交叉算子,需要提醒的是上面比較好的結果其實是使用了evolution1()這個進化函數,較差的是使用了evolution()這個函數,他們的最大區別就是前者保留最好染色體不進行交叉變異,後者則是按照基本遺傳算法框架的完全交叉變異操作的,其區別之大各位可以深入琢磨,兩種交叉算子的區別是一個就是簡單的類OX交叉算子,另外一個交叉算子改進了一下,相同染色體交叉產生不同子代染色體,實際上它兩對結果影響並不是很大,以上是我的一點點簡單的總結,還是那句話,或許你覺得遺傳算法效率其實也不怎樣,實際上基本的遺傳算法是有很多不足的,如容易選入局部收斂,全局搜索能力不夠強,但是基本的遺傳算法是有很多可以改進的地方,如交叉算子的設計、變異算子的設計、選擇策略等等,有關遺傳算法個人覺得作爲一種智能啓發式搜索算法它甚至比別的普通算法(如動態規劃)理解起來還容易,而且它特別容易與別的算法相結合,設計新的混合算法,另外在基本遺傳算法的基礎上還衍生出了很多別的算法,如免疫遺傳算法,這些都是一些比較高級的算法。