一、題目
- 一個售貨員必須訪問n個城市,恰好訪問每個城市一次,並最終回到出發城市。
售貨員從城市i到城市j的旅行費用是一個整數,旅行所需的全部費用是他旅行經過的的各邊費用之和,而售貨員希望使整個旅行費用最低。 - (等價於求圖的最短哈密爾頓迴路問題)令G=(V, E)是一個帶權重的有向圖,頂點集V=(v0, v1, ..., vn-1)。從圖中任一頂點vi出發,經圖中所有其他頂點一次且只有一次,最後回到同一頂點vi的最短路徑。
二、測試用例
其中1,2,3,4,5代表五個城市。此模型可抽象爲圖,可用鄰接矩陣c表示,如下圖所示:
三、動態規劃方程
假設從頂點s出發,令d(i, V)表示從頂點i出發經過V(是一個點的集合)中各個頂點一次且僅一次,最後回到出發點s的最短路徑長度。
推導:(分情況來討論)
①當V爲空集,那麼,表示直接從i回到s了,此時
且
②如果V不爲空,那麼就是對子問題的最優求解。你必須在V這個城市集合中,嘗試每一個,並求出最優解。
注:表示選擇的城市和城市i的距離,
是一個子問題。
綜上所述,TSP問題的動態規劃方程就出來了:
四、用例分析
現在對問題定義中的例子來說明TSP的求解過程。(假設出發城市是 0城市)
這裏只畫出了d(1,{2,3,4}),由於篇幅有限這裏就不畫了。
①我們要求的最終結果是d(0,{1,2,3,4}),它表示,從城市0開始,經過{1,2,3,4}之中的城市並且只有一次,求出最短路徑.。
②d(0,{1,2,3,4})是不能一下子求出來的,那麼他的值是怎麼得出的呢?看上圖的第二層,第二層表明了d(0,{1,2,3,4})所需依賴的值。那麼得出:
③d(1,{2,3,4}),d(2,{1,3,4}),d(3,{1,2,4}),d(4,{1,2,3})同樣也不是一步就能求出來的,它們的解一樣需要有依賴,就比如說d(1,{2,3,4})
d(2,{1,3,4}),d(3,{1,2,4}),d(4,{1,2,3})同樣需要這麼求。
④按照上面的思路,只有最後一層的,當V爲空集時,就可以滿足 且
該條件,直接求出dp數組部分的值。
五、數據結構
由上述動態規劃公式d(i,V)表示從頂點i出發經過V(是一個點的集合)中各個頂點一次且僅一次,最後回到出發點s的最短路徑長度。根據上述給的測試用例有5個城市編號0,1,2,3,4。那麼訪問n個城市,恰好訪問每個城市一次,並最終回到出發城市的嘴短距離可表示爲d(0,{1,2,3,4}),那麼問題來了我們用什麼數據結構表示d(i,V),這裏我們就可二維數據dp[N][M]來表示,N表示城市的個數,M表示集合的數量,即,之所以這麼表示因爲集合V有
個子集。根據測試用例可得出如下dp數組表格:
那麼你們可能就有疑問了,爲什麼這麼表示?這裏說明一下比如集合{1,2,3,4}爲什麼用15表示,我們可以把 集合中元素看成二進制1的位置(二進制從右開始看),1表示從右開始第一位爲1,2表示從又開始第二位爲1,所以集合{1,2,3,4}可表示二進制(1111)轉化爲十進制爲15。再舉個例子比如集合{1,3}表示爲二進制爲0101,十進制爲5。所以我們求出dp[0][15](通用表示dp[0][])就是本題的最終解。
注意:
- 對於第y個城市,他的二進制表達爲,1<<(y-1)。
- 對於數字x,要看它的第i位是不是1,那麼可以通過判斷布爾表達式 (((x >> (i - 1) ) & 1) == 1或者(x & (1<<(i-1)))!= 0的真值來實現。
- 由動態規劃公式可知,需要從集合中剔除元素。假如集合用索引x表示,要剔除元素標號爲i,我們異或運算實現減法,其運算表示爲: x = x ^ (1<<(i - 1))。
六、最短路徑頂點的計算
我們先計算dp[N][M]數組之後,我可以用dp數組來反向推出其路徑。其算法思想如下:
比如在第一步時,我們就知道那個值最小,如下圖所示:
因爲dp[][]數組我們已經計算出來了,由計算可知C01+d(1,{2,3,4})最小,所以一開始從起始點0出發,經過1。接下來同樣計算d(1,{2,3,4})
由計算可知C14+d(4,{2,3})所以0--->1---->4,接下來同理求d(4,{2,3}),這裏就省略,讀者可以自行計算。最終計算出來的路徑爲:0--->1--->4--->2--->3--->0
七、代碼編寫
#include <iostream>
#include <cmath>
#include <cstring>
#include <vector>
using namespace std;
#define N 5
#define INF 10e7
#define min(a,b) ((a>b)?b:a)
static const int M = 1 << (N-1);
//存儲城市之間的距離
int g[N][N] = {{0,3,INF,8,9},
{3,0,3,10,5},
{INF,3,0,4,3},
{8,10,4,0,20},
{9,5,3,20,0}};
//保存頂點i到狀態s最後回到起始點的最小距離
int dp[N][M] ;
//保存路徑
vector<int> path;
//核心函數,求出動態規劃dp數組
void TSP(){
//初始化dp[i][0]
for(int i = 0 ; i < N ;i++){
dp[i][0] = g[i][0];
}
//求解dp[i][j],先跟新列在更新行
for(int j = 1 ; j < M ;j++){
for(int i = 0 ; i < N ;i++ ){
dp[i][j] = INF;
//如果集和j(或狀態j)中包含結點i,則不符合條件退出
if( ((j >> (i-1)) & 1) == 1){
continue;
}
for(int k = 1 ; k < N ; k++){
if( ((j >> (k-1)) & 1) == 0){
continue;
}
if( dp[i][j] > g[i][k] + dp[k][j^(1<<(k-1))]){
dp[i][j] = g[i][k] + dp[k][j^(1<<(k-1))];
}
}
}
}
}
//判斷結點是否都以訪問,不包括0號結點
bool isVisited(bool visited[]){
for(int i = 1 ; i<N ;i++){
if(visited[i] == false){
return false;
}
}
return true;
}
//獲取最優路徑,保存在path中,根據動態規劃公式反向找出最短路徑結點
void getPath(){
//標記訪問數組
bool visited[N] = {false};
//前驅節點編號
int pioneer = 0 ,min = INF, S = M - 1,temp ;
//把起點結點編號加入容器
path.push_back(0);
while(!isVisited(visited)){
for(int i=1; i<N;i++){
if(visited[i] == false && (S&(1<<(i-1))) != 0){
if(min > g[i][pioneer] + dp[i][(S^(1<<(i-1)))]){
min = g[i][pioneer] + dp[i][(S^(1<<(i-1)))] ;
temp = i;
}
}
}
pioneer = temp;
path.push_back(pioneer);
visited[pioneer] = true;
S = S ^ (1<<(pioneer - 1));
min = INF;
}
}
//輸出路徑
void printPath(){
cout<<"最小路徑爲:";
vector<int>::iterator it = path.begin();
for(it ; it != path.end();it++){
cout<<*it<<"--->";
}
//單獨輸出起點編號
cout<<0;
}
int main()
{
TSP();
cout<<"最小值爲:"<<dp[0][M-1]<<endl;
getPath();
printPath();
return 0;
}
八、測試結果及性能分析
時間複雜度:
空間複雜度: