遺傳演算法解決TSP問題實現以及與最小生成樹的對比
摘要:
本實驗採用遺傳演算法實現了旅行商問題的模擬求解,並在同等規模問題上用最小生成樹演算法做了一定的對比工作。遺傳演算法在計算時間和佔用記憶體上,都遠遠優於最小生成樹演算法。
程式採用Microsoft visual studio 2008 結合MFC基本對話方塊類庫開發。32位windows 7系統下除錯執行。
引言
遺傳演算法(Genetic Algorithm)是模擬達爾文生物進化論的自然選擇和遺傳學機理的生物進化過程的計算模型,是一種通過模擬自然進化過程搜尋最優解的方法,由密歇根大學的約翰•霍蘭德和他的同事於二十世紀六十年代在對細胞自動機(英文:cellular automata)進行研究時率先提出, 並於1975年出版了頗有影響的專著《Adaptation in Natural and Artificial Systems》,GA這個名稱才逐漸為人所知,約翰•霍蘭德教授所提出的GA通常為簡單遺傳演算法(SGA)。在二十世紀八十年代中期之前,對於遺傳演算法的研究還僅僅限於理論方面,直到在伊利諾伊大學召開了第一屆世界遺傳演算法大會。隨著計算機計算能力的發展和實際應用需求的增多,遺傳演算法逐漸進入實際應用階段。1989年,紐約時報作者約翰•馬科夫寫了一篇文章描述第一個商業用途的遺傳演算法--進化者(英文:Evolver)。之後,越來越多種類的遺傳演算法出現並被用於許多領域中,財富雜誌500強企業中大多數都用它進行時間表安排、資料分析、未來趨勢預測、預算、以及解決很多其他組合優化
遺傳演算法是從代表問題可能潛在的解集的一個種群(population)開始的,而一個種群則由經過基因(gene)編碼的一定數目的個體(individual)組成。每個個體實際上是染色體(chromosome)帶有特徵的實體。染色體作為遺傳物質的主要載體,即多個基因的集合,其內部表現(即基因型)是某種基因組合,它決定了個體的形狀的外部表現,如黑頭髮的特徵是由染色體中控制這一特徵的某種基因組合決定的。因此,在一開始需要實現從表現型到基因型的對映即編碼工作。由於仿照基因編碼的工作很複雜,我們往往進行簡化,如二進位制編碼,初代種群產生之後,按照適者生存和優勝劣汰的原理,逐代(generation)演化產生出越來越好的近似解,在每一代,根據問題域中個體的適應度(fitness)大小選擇(selection)個體,並藉助於自然遺傳學的遺傳運算元(genetic operators)進行組合交叉(crossover)和變異(mutation),產生出代表新的解集的種群。這個過程將導致種群像自然進化一樣的後生代種群比前代更加適應於環境,末代種群中的最優個體經過解碼(decoding),可以作為問題近似最優解[1]。
遺傳演算法是借鑑生物界的進化規律(適者生存,優勝劣汰遺傳機制)演化而來的。其主要特點是直接對結構物件進行操作,不存在求導和函式連續性的限定;具有內在的隱並行性和更好的全域性尋優能力;採用概率化的尋優方法,能自動獲取和指導優化的搜尋空間,自適應地調整搜尋方向,不需要確定的規則。遺傳演算法的這些性質,已被人們廣泛地應用於組合優化、機器學習、訊號處理、自適應控制和人工生命等領域。它是現代有關智慧計算中的關鍵技術。
綜述:
程式總體流程圖:
這個程式的思想是,隨機生成“地點數”編輯框輸入的數字的地點,儲存在一個vector裡。然後用一個“基因類”表示該基因代表第幾個點,接著一個“基因組類”有序包含了很多“基因類”,如果一個“基因組類”包含的基因類順序為:基因組.基因[0].data = 第二個點;基因組.基因[1].data = 第三個點;基因組.基因[3].data = 第一個點;就說明該基因組表示的連線順序是從第二點連到第三個點再連到第一個點。給每個城市一個固定的基因編號,例如10個城市為 0 1 2 3 4 5 6 7 8 9 ,隨機地組成一個染色體(以下所有情況都以10個城市為例說明)。約定這10個城市之間的行走路線為:
(其餘基因序列的路線同樣道理)
接著有一個“遺傳機器類”包含了很多基因組。基因組的數量由“基因組數”編輯框決定。初始化的時候,每個基因組的基因順序是隨機決定的。進行第一代進化的時候,遍歷vector<基因組>,計算每個基因組代表的連線方式的連線長度。連線長度越長,說明這個基因組越差勁,因為我們要計算以何種方式連線連線長度最短。
我們用不適應度來記錄連線長度。接著就是選擇哪個基因組可以生育,遺傳給下一代。我採用了一個輪盤賭的策略,儘可能選擇不適應度低的基因組進行生育。選擇出的基因組進行交換變異後,就把這個基因組複製給下一代。
最後,選擇兩個最好的基因組,不進行任何變異,直接複製到下一代。這樣迴圈反覆,迭代“代數”編輯框輸入的代數次數之後,就可以輸出結果了。
結果就是最後一代最優秀的那個基因組代表的連線方式。
主要程式碼:
void cGAMachine::SetupNextGeneration()//生成下一代基因,進化到下一代
{
vector<cGenome> offspring;//儲存下一代基因
m_maxNotFitness = m_genomes[m_population - 1].m_notfitness;
//所有基因組最大不適應度
while (offspring.size() < (unsigned int)m_population - 2)
//選擇(最大基因組數-2)數量的基因組進行變異和遺傳
{
cGenome parent = SelectRouletteWheel();
//進行輪盤賭隨機選擇一個基因組出來進行生育
cGenome offspring1;
//儲存變異後的基因組
MutateInsert(parent.m_genes, offspring1.m_genes);//進行變異
offspring.push_back(offspring1);
//將變異後的基因組壓入第二代vector<基因組>裡
}
sort(m_genomes.begin(), m_genomes.end());
//對vector<基因組>進行排序,以便下一行程式碼選出最優秀的個基因組
CopyEliteInto(offspring);
//直接將最優秀的個基因組複製到下一代
m_genomes = offspring;
m_curGener++;//代數計數器+1
}
cGenome& cGAMachine::SelectRouletteWheel()
{
int nRand = rand() % (int)(m_crossOverRate * m_maxNotFitness) + 0.5 * m_maxNotFitness;
for (std::vector<cGenome>::iterator iter = m_genomes.begin(); iter != m_genomes.end(); ++iter)
{
if (iter->m_notfitness <= nRand)
{
return *iter;
break;
}
}
return m_genomes[0];
}
void cGAMachine::MutateInsert(const vector<cGene> &parent, vector<cGene> &offspring)//插入變異
{
if ((rand() / (double)(RAND_MAX)) > m_mutationRate)
{
offspring = parent;
return;
}
int nRandscr = rand() % (parent.size() - 1);
int nRanddes = rand() % (parent.size() - 1);
if (nRanddes == nRandscr)
{
offspring = parent;
return;
}
cGene geneInsert = parent[nRandscr];
cGene geneDes = parent[nRanddes];
offspring = parent;
offspring.erase(offspring.begin() + nRandscr);
if (nRandscr < nRanddes)
{
offspring.erase(offspring.begin() + nRanddes - 1);
offspring.insert(offspring.begin() + nRanddes - 1, geneInsert);
offspring.insert(offspring.begin() + nRandscr, geneDes);
}
else
{
offspring.erase(offspring.begin() + nRanddes);
offspring.insert(offspring.begin() + nRanddes, geneInsert);
offspring.insert(offspring.begin() + nRandscr, geneDes);
}
}
void cGAMachine::CopyEliteInto(std::vector<cGenome> &offspring)
{
for (int i = 0; i < 2 && i < m_population; i++)
{
offspring.push_back(m_genomes[i]);
}
}
cGenome& cGAMachine::GetBestResult()
{
sort(m_genomes.begin(), m_genomes.end());
return m_genomes[0];
}
實驗結果:
使用上圖隨機生成的節點採用最小生成樹
採用50個基因組,100次迭代進化,0.5的基因變異率
依次生成50個點,100個點,150個點,200個點,250個點的規模問題執行時間的對比:release版本程式
隨著節點數的增加遺傳演算法的執行時間基本保持在100ms左右
佔用記憶體對比:
發現的問題:
1. 雖然遺傳演算法在效能上優勢很大,但是有時候基本是收斂在區域性最優解上了,找全域性最優解需要改進的遺傳演算法。
2. 每次發現的解有很大的不確定性,看人品的演算法。
未來的工作:
1. 參照《最小生成樹演算法在旅行商問題中的應用》實現最小生成樹的TSP解法法。
2. 改進遺傳演算法,引入災變的思想,得到全域性最優解。
3. 進一步瞭解其他智慧演算法的TSP問題解決方案
參考文獻:
1.
2.
工程程式碼下載地址:
其他演算法:
//=====================================================================
//基本蟻群演算法原始碼
//使用的城市資料是eil51.tsp
//=====================================================================
// AO.cpp : 定義控制檯應用程式的入口點。
#pragma once
#include <iostream>
#include <math.h>
#include <time.h>
//=====================================================================
//常量定義和引數定義
//=====================================================================
const double ALPHA=1.0; //啟發因子,資訊素的重要程度
const double BETA=2.0; //期望因子,城市間距離的重要程度
const double ROU=0.5; //資訊素殘留引數
const int N_ANT_COUNT=34; //螞蟻數量
const int N_IT_COUNT=1000; //迭代次數
const int N_CITY_COUNT=51; //城市數量
const double DBQ=100.0; //總的資訊素
const double DB_MAX=10e9; //一個標誌數,10的9次方
double g_Trial[N_CITY_COUNT][N_CITY_COUNT]; //兩兩城市間資訊素,就是環境資訊素
double g_Distance[N_CITY_COUNT][N_CITY_COUNT]; //兩兩城市間距離
//eil51.tsp城市座標資料
double x_Ary[N_CITY_COUNT]=
{
37,49,52,20,40,21,17,31,52,51,
42,31,5,12,36,52,27,17,13,57,
62,42,16,8,7,27,30,43,58,58,
37,38,46,61,62,63,32,45,59,5,
10,21,5,30,39,32,25,25,48,56,
30
};
double y_Ary[N_CITY_COUNT]=
{
52,49,64,26,30,47,63,62,33,21,
41,32,25,42,16,41,23,33,13,58,
42,57,57,52,38,68,48,67,48,27,
69,46,10,33,63,69,22,35,15,6,
17,10,64,15,10,39,32,55,28,37,
40
};
//返回指定範圍內的隨機整數
int rnd(int nLow,int nUpper)
{
return nLow+(nUpper-nLow)*rand()/(RAND_MAX+1);
}
//返回指定範圍內的隨機浮點數
double rnd(double dbLow,double dbUpper)
{
double dbTemp=rand()/((double)RAND_MAX+1.0);
return dbLow+dbTemp*(dbUpper-dbLow);
}
//返回浮點數四捨五入取整後的浮點數
double ROUND(double dbA)
{
return (double)((int)(dbA+0.5));
}
//=====================================================================
//螞蟻類的定義和實現
//=====================================================================
//定義螞蟻類
class CAnt
{
public:
CAnt(void);
~CAnt(void);
public:
int m_nPath[N_CITY_COUNT]; //螞蟻走的路徑
double m_dbPathLength; //螞蟻走過的路徑長度
int m_nAllowedCity[N_CITY_COUNT]; //沒去過的城市
int m_nCurCityNo; //當前所在城市編號
int m_nMovedCityCount; //已經去過的城市數量
public:
int ChooseNextCity(); //選擇下一個城市
void Init(); //初始化
void Move(); //螞蟻在城市間移動
void Search(); //搜尋路徑
void CalPathLength(); //計算螞蟻走過的路徑長度
};
//建構函式
CAnt::CAnt(void)
{
}
//解構函式
CAnt::~CAnt(void)
{
}
//初始化函式,螞蟻搜尋前呼叫
void CAnt::Init()
{
for (int i=0;i<N_CITY_COUNT;i++)
{
m_nAllowedCity=1; //設定全部城市為沒有去過
m_nPath=0; //螞蟻走的路徑全部設定為0
}
//螞蟻走過的路徑長度設定為0
m_dbPathLength=0.0;
//隨機選擇一個出發城市
m_nCurCityNo=rnd(0,N_CITY_COUNT);
//把出發城市儲存入路徑陣列中
m_nPath[0]=m_nCurCityNo;
//標識出發城市為已經去過了
m_nAllowedCity[m_nCurCityNo]=0;
//已經去過的城市數量設定為1
m_nMovedCityCount=1;
}
//選擇下一個城市
//返回值 為城市編號
int CAnt::ChooseNextCity()
{
int nSelectedCity=-1; //返回結果,先暫時把其設定為-1
//==============================================================================
//計算當前城市和沒去過的城市之間的資訊素總和
double dbTotal=0.0;
double prob[N_CITY_COUNT]; //儲存各個城市被選中的概率
for (int i=0;i<N_CITY_COUNT;i++)
{
if (m_nAllowedCity == 1) //城市沒去過
{
//該城市和當前城市間的資訊素
prob=pow(g_Trial[m_nCurCityNo],ALPHA)*pow(1.0/g_Distance[m_nCurCityNo],BETA);
dbTotal=dbTotal+prob; //累加資訊素,得到總和
}
else //如果城市去過了,則其被選中的概率值為0
{
prob=0.0;
}
}
//==============================================================================
//進行輪盤選擇
double dbTemp=0.0;
if (dbTotal > 0.0) //總的資訊素值大於0
{
dbTemp=rnd(0.0,dbTotal); //取一個隨機數
for (int i=0;i<N_CITY_COUNT;i++)
{
if (m_nAllowedCity == 1) //城市沒去過
{
dbTemp=dbTemp-prob; //這個操作相當於轉動輪盤,如果對輪盤選擇不熟悉,仔細考慮一下
if (dbTemp < 0.0) //輪盤停止轉動,記下城市編號,直接跳出迴圈
{
nSelectedCity=i;
break;
}
}
}
}
//==============================================================================
//如果城市間的資訊素非常小 ( 小到比double能夠表示的最小的數字還要小 )
//那麼由於浮點運算的誤差原因,上面計算的概率總和可能為0
//會出現經過上述操作,沒有城市被選擇出來
//出現這種情況,就把第一個沒去過的城市作為返回結果
if (nSelectedCity == -1)
{
for (int i=0;i<N_CITY_COUNT;i++)
{
if (m_nAllowedCity == 1) //城市沒去過
{
nSelectedCity=i;
break;
}
}
}
//==============================================================================
//返回結果,就是城市的編號
return nSelectedCity;
}
//螞蟻在城市間移動
void CAnt::Move()
{
int nCityNo=ChooseNextCity(); //選擇下一個城市
m_nPath[m_nMovedCityCount]=nCityNo; //儲存螞蟻走的路徑
m_nAllowedCity[nCityNo]=0;//把這個城市設定成已經去過了
m_nCurCityNo=nCityNo; //改變當前所在城市為選擇的城市
m_nMovedCityCount++; //已經去過的城市數量加1
}
//螞蟻進行搜尋一次
void CAnt::Search()
{
Init(); //螞蟻搜尋前,先初始化
//如果螞蟻去過的城市數量小於城市數量,就繼續移動
while (m_nMovedCityCount < N_CITY_COUNT)
{
Move();
}
//完成搜尋後計算走過的路徑長度
CalPathLength();
}
//計算螞蟻走過的路徑長度
void CAnt::CalPathLength()
{
m_dbPathLength=0.0; //先把路徑長度置0
int m=0;
int n=0;
for (int i=1;i<N_CITY_COUNT;i++)
{
m=m_nPath;
n=m_nPath[i-1];
m_dbPathLength=m_dbPathLength+g_Distance[m][n];
}
//加上從最後城市返回出發城市的距離
n=m_nPath[0];
m_dbPathLength=m_dbPathLength+g_Distance[m][n];
}
//=====================================================================
//TSP類的定義和實現
//=====================================================================
//tsp類
class CTsp
{
public:
CTsp(void);
~CTsp(void);
public:
CAnt m_cAntAry[N_ANT_COUNT]; //螞蟻陣列
CAnt m_cBestAnt; //定義一個螞蟻變數,用來儲存搜尋過程中的最優結果
//該螞蟻不參與搜尋,只是用來儲存最優結果
public:
//初始化資料
void InitData();
//開始搜尋
void Search();
//更新環境資訊素
void UpdateTrial();
};
//建構函式
CTsp::CTsp(void)
{
}
CTsp::~CTsp(void)
{
}
//初始化資料
void CTsp::InitData()
{
//先把最優螞蟻的路徑長度設定成一個很大的值
m_cBestAnt.m_dbPathLength=DB_MAX;
//計算兩兩城市間距離
double dbTemp=0.0;
for (int i=0;i<N_CITY_COUNT;i++)
{
for (int j=0;j<N_CITY_COUNT;j++)
{
dbTemp=(x_Ary-x_Ary[j])*(x_Ary-x_Ary[j])+(y_Ary-y_Ary[j])*(y_Ary-y_Ary[j]);
dbTemp=pow(dbTemp,0.5);
//城市間距離四捨五入取整,eil51.tsp的最短路徑426是距離按四捨五入取整後得到的。
g_Distance[j]=ROUND(dbTemp);
}
}
//初始化環境資訊素,先把城市間的資訊素設定成一樣
//這裡設定成1.0,設定成多少對結果影響不是太大,對演算法收斂速度有些影響
for (int i=0;i<N_CITY_COUNT;i++)
{
for (int j=0;j<N_CITY_COUNT;j++)
{
g_Trial[j]=1.0;
}
}
}
//更新環境資訊素
void CTsp::UpdateTrial()
{
//臨時陣列,儲存各只螞蟻在兩兩城市間新留下的資訊素
double dbTempAry[N_CITY_COUNT][N_CITY_COUNT];
memset(dbTempAry,0,sizeof(dbTempAry)); //先全部設定為0
//計算新增加的資訊素,儲存到臨時數組裡
int m=0;
int n=0;
for (int i=0;i<N_ANT_COUNT;i++) //計算每隻螞蟻留下的資訊素
{
for (int j=1;j<N_CITY_COUNT;j++)
{
m=m_cAntAry.m_nPath[j];
n=m_cAntAry.m_nPath[j-1];
dbTempAry[n][m]=dbTempAry[n][m]+DBQ/m_cAntAry.m_dbPathLength;
dbTempAry[m][n]=dbTempAry[n][m];
}
//最後城市和開始城市之間的資訊素
n=m_cAntAry.m_nPath[0];
dbTempAry[n][m]=dbTempAry[n][m]+DBQ/m_cAntAry.m_dbPathLength;
dbTempAry[m][n]=dbTempAry[n][m];
}
//==================================================================
//更新環境資訊素
for (int i=0;i<N_CITY_COUNT;i++)
{
for (int j=0;j<N_CITY_COUNT;j++)
{
g_Trial[j]=g_Trial[j]*ROU+dbTempAry[j]; //最新的環境資訊素 = 留存的資訊素 + 新留下的資訊素
}
}
}
void CTsp::Search()
{
char cBuf[256]; //列印資訊用
//在迭代次數內進行迴圈
for (int i=0;i<N_IT_COUNT;i++)
{
//每隻螞蟻搜尋一遍
for (int j=0;j<N_ANT_COUNT;j++)
{
m_cAntAry[j].Search();
}
//儲存最佳結果
for (int j=0;j<N_ANT_COUNT;j++)
{
if (m_cAntAry[j].m_dbPathLength < m_cBestAnt.m_dbPathLength)
{
m_cBestAnt=m_cAntAry[j];
}
}
//更新環境資訊素
UpdateTrial();
//輸出目前為止找到的最優路徑的長度
sprintf(cBuf,"\n[%d] %.0f",i+1,m_cBestAnt.m_dbPathLength);
printf(cBuf);
}
}
//=====================================================================
//主程式
//=====================================================================
int main()
{
//用當前時間點初始化隨機種子,防止每次執行的結果都相同
time_t tm;
time(&tm);
unsigned int nSeed=(unsigned int)tm;
srand(nSeed);
//開始搜尋
CTsp tsp;
tsp.InitData(); //初始化
tsp.Search(); //開始搜尋
//輸出結果
printf("\nThe best tour is :\n");
char cBuf[128];
for (int i=0;i<N_CITY_COUNT;i++)
{
sprintf(cBuf,"%02d ",tsp.m_cBestAnt.m_nPath+1);
if (i % 20 == 0)
{
printf("\n");
}
printf(cBuf);
}
printf("\n\nPress any key to exit!");
getchar();
return 0;
}