一.問題描述
某售貨員要到若干城市去推銷商品, 已知各城市之間的路程(旅費), 他要選定一條從駐地出發, 經過每個城市一遍, 最后回到駐地的路線, 使總的路程(總旅費)最小。
二.解題思路
旅行售貨員問題的解空間是一棵排列樹。對於排列樹的回溯法與生成1, 2, ……n的所有排列的遞歸算法Perm類似。開始時x=[1, 2, ……n], 則相應的排列樹有x[1:n]的所有排列構成。
在遞歸算法Backtrack中, 當i=n時, 當前擴展節點是排列樹的葉節點的父節點。此時算法檢測圖G是否存在一條從頂點x[n-1]到頂點x[n]的邊和一條從頂點x[n]到頂點1的邊。如果這兩條邊都存在, 則找到一條旅行員售貨回路。此時, 算法還需要判斷這條回路的費用是否優於已找到的當前最優回流的費用bestc。如果是, 則必須更新當前最優值bestc和當前最優解bestx。
當i<n時, 當前擴展節點位於排列樹的第i-1層。圖G中存在從頂點x[i-1]到頂點x[i]的邊時, x[1:i]構成圖G的一條路徑, 且當x[1:i]的費用小於當前最優值時算法進入樹的第i層, 否則將剪去相應的子樹。
代碼如下:
// 旅行售貨員問題
#include<bits/stdc++.h>
using namespace std;
class Traveling
{
friend int TSP(int **, int *, int , int);
private:
void Backtrack(int i);
int n, //圖G的頂點個數
*x, //當前解
*bestx; //當前最優解
int **a, //圖G的鄰接矩陣
cc, //當前費用
bestc, //當前最優值
NoEdge; //無邊標記
};
void Traveling::Backtrack(int i)
{
if(i == n)
{
cout<<"當前第"<<i<<"層為最后一層"<<",選擇"<<x[i]<<endl;
if(a[x[n-1]][x[n]] != NoEdge && a[x[n]][1] != NoEdge && (cc+a[x[n-1]][x[n]]+a[x[n]][1] < bestc || bestc==NoEdge))
{
cout<<"得到一個更優解:";
for(int j=1; j<=n; j++)
{
bestx[j] = x[j];
cout<<x[j]<<" ";
}
bestc = cc+a[x[n-1]][x[n]]+a[x[n]][1];
cout<<"最優解更新為:"<<bestc<<endl;
}
else
{
for(int j=1; j<=n; j++)
cout<<x[j]<<" ";
cout<<"此路徑得不到更優解,回溯到第"<<i-1<<"層"<<endl;
}
}
else
{
for(int j=i; j<=n; j++)
{
//是否可以進入x[j]子樹
if(a[x[i-1]][x[j]] != NoEdge && (cc+a[x[i-1]][x[j]] < bestc || bestc==NoEdge)) //剪枝函數
{
//搜索子樹
swap(x[i], x[j]); //必須交換,這樣才可以做到使得x[2:i]的表示已經選過的,x[i+1:n]表示還未選過!!!
cc += a[x[i-1]][x[i]];
cout<<"當前第"<<i<<"層"<<",選擇"<<x[i]<<",遞歸深入一層,將到達第"<<i+1<<"層"<<endl;
Backtrack(i+1);
cout<<"當前第"<<i+1<<"層,遞歸回退一層,將到達第"<<i<<"層"<<endl;
cc -= a[x[i-1]][x[i]];
swap(x[i], x[j]);
}
else cout<<"不滿足剪枝函數,對應子樹被剪枝"<<endl;
}
}
}
int TSP(int **a, int *v, int n, int NoEdge)
{
Traveling Y;
// 初始化Y
Y.x = new int[n+1];
for(int i=1; i<=n; i++) Y.x[i] = i;
Y.a = a;
Y.n = n;
Y.bestc = INT_MAX;
Y.bestx = v;
Y.cc = 0;
Y.NoEdge = NoEdge;
Y.Backtrack(2); //搜索x[2:n]的全排列
delete[] Y.x;
return Y.bestc;
}
int main()
{
cout<<"請輸入旅行地點個數:";
int n;
while(cin>>n && n)
{
cout<<"請輸入鄰接矩陣"<<endl;
int **a = new int*[n+1];
for(int i=0; i<=n; i++) a[i] = new int[n+1];
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
cin>>a[i][j];
int NoEdge = 0;
int *v = new int[n+1];
for(int i=0; i<=n; i++) v[i] = 0;
int ans = TSP(a, v, n, NoEdge);
cout<<"旅行路費最少為:"<<ans<<endl;
for(int i=0; i<=n; i++) delete[] a[i];
delete[] a;
delete[] v;
cout<<"請輸入旅行地點個數";
}
system("pause");
return 0;
}
運行結果:
結合排列樹,大家自己動手畫一畫,應該會比較清晰.
這里有一個問題就是為啥需要 swap(x[i], x[j])呢?
在前面的N皇后問題以及圖的m着色問題都不需要交換,為什么在這里我們需要交換呢?
我個人的看法是:因為在這里只有交換了x[i]和x[j],這樣才能使得 x[2 : i]表示都是已經選擇過的, x[i+1 : n]表示我們還沒有選擇的,這樣遞歸深入后,就可以從j == i(相對於上一層而言是i+1)開始往后遍歷.
這就和經典的全排列問題是一個框架,通過交換來保證之前的都是已經選擇的,選擇的情況由x數組給記錄下來,之后的x[i+1:n]都是我們還未選擇的,所以利用for循環(j==i開始)一個一個去嘗試看看能不能找到最優解.如果大家還是不太清楚,建議再回頭看看全排列的解題方法,代碼我給大家貼在下面.
但是如果說實在不想要交換可不可以呢?
答案是可以的,但是我們必須要有一個數組isSelect記錄了元素是否被選擇過了,然后我們每遞歸深入一層也可以從j==1開始遍歷,只要isSelect[j]為True的話,那么就直接continue;記得回溯的時候撤銷操作.但是這樣做損失了順序信息,對於一些題目來說不可行,當然如果你比較執着的話,當然還可以再用一個數組來記錄順序信息,不過顯然這並不值得做.
不用交換方法的就比如之前我們說過的0-1背包問題的回溯解法:用一個select數組記錄物品是否被選擇.最后可以通過select數組得到整體的選擇情況.但是不具有先后順序,我們僅僅是知道了它被選擇了沒有,並不知道它是第幾個選擇的,當然了,對於這一題來說這就已經夠了.
而如果對於需要考慮先后順序的題目,我們就需要使用的是交換的做法,到達葉節點后我們通過x數組得到的答案就已經包含了順序的信息,這很重要!
template <typename T>
void Perm(T list[], int low, int high)
{
if(low == high)
{
static int count = 1;
cout<<"第"<<count++<<"個排列為:";
for(int i=0; i<high; ++i)
{
cout<<list[i];
cout.width(4);
}
cout<<'\n';
}
for(int i=low; i<high; ++i)
{
swap(list[i], list[low]); //這里的swap保證了list[1:low]是已經選擇的,list[low+1:high]是未選擇的
Perm(list, low+1, high);
swap(list[i], list[low]);
}
參考畢方明老師《算法設計與分析》課件.
歡迎大家訪問個人博客網站---喬治的編程小屋, 和我一起加油吧!