1234443332244223图示给出的是结点数为 4 时,固定结点 1 对剩下的结点进行全排列的结果。可见,排列树的叶子结点个数会随着规模的增加而成指数上升。
对于本问题来说,利用深度优先搜索是一个较直观的搜索策略。为了进行剪枝从而排除一些不必要的计算以加快计算效率,我们可以在 DFS
的基础上利用 分支定界(Branch & Bound)的思想,设计 代价函数 剪枝。
当搜索完成了前j 步时,排列树此时已导出解为Kj=<k1,k2,...,kj>,则当前的路径长为:L′=i=1∑j−1d(ki,ki+1). 此时还剩下K−Kj=<kj+1..kn> 这系列结点没有经过。
那么最终路径长度存在一个很明显的下界,该下界由L′ 与剩余所有结点的最小边权之和共同组成。
这是因为,与任意结点km∈K−Kj 相连的边权大小一定不超过与该点相连的最小边。所以每一个剩余结点的最小边之和自然组成了剩余n−j 步搜索结果的下界。
即:
L−=L′+i=j∑nkm∈K−{ki}mind(ki,km)
以下图为例,假设当前这个搜索分支导出了一个子解<1,3,2> ,与端点 (2)
相连的最短边长度是 2
,与端点 (4)
相连的最短边长度是 2
。所以最终得到下界路径长为 26
.
我们就以该下界作为代价函数,进行剪枝:
- 完全搜索其中一个分支(解),得到该分支下的路径长,记为Lmin;
- 回溯搜索下一个分支,在结点处计算此时的代价函数L;
- 如果L>Lmin 说明在该分支下最理想的情况得到的解也不可能低于Lmin,则放弃该分支,转到 (2) ;
- 如果L<Lmin 则继续遍历该分支的下一个结点,然后计算此时的代价函数L,继续判断是放弃分支还是继续遍历。如果该分支搜索结束后仍有L<Lmin 则更新Lmin←L,转到 (2);
- 直到所以分支均讨论完毕则算法结束。
用vis[1..n] 对结点的访问进行标记记录,以实现回溯。
用step 表示当前分支的第step 个结点标号。
用d[1..n,1..n] 存储两两结点间的距离,其中规定d[i,i]=∞。
用path[1..n] 存储当前分支的解。
用Lmin 存储 TSP 问题的最短路径值,初始化Lmin←+∞。
用L 存储当前子解中已走过的路径长,即为上面我们推导时出现的L′。
算法伪码:
1.2.3.4.5.6.7.8.9.10.11.12.Algorithm: TSP-DFS(L,step)ifstep=nthenLmin←min(Lmin,L+d[step,1])elsesum←cost(step)//计算代价ifL+sum>Lminthenreturn//剪枝fori=2tondovis[i]←1path[step]←iTSP-DFS(L+d(path[step−1],path[step]),step+1)//访问下一个结点vis[i]←0//回溯return
注:上述伪码中,第 4
行的 cost()
函数是根据 step
计算剩余结点的最短边之和,可用下面的数学式表达:
cost(step)=⎩⎨⎧vis[i]=0∑1≤j≤n,j=imind[i,j]⎭⎬⎫+1≤j≤n,j=stepmind[step,j]
不难得出,在固定起点后,排列树的叶子结点树在O((n−1)!),每一个分支的每一个结点都需要进行O(1) 的代价函数计算,一共n 个结点,因此该算法在最坏情况下的时间复杂度为O(n!).
由于需要对解空间进行保存,对结点的访问情况进行标记,所以空间复杂度为O(n).
算法的具体 c
系列语言实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| #include <iostream> #include <string.h> #include <cstdlib> using namespace std; int n = 4; int d[4][4] = {{INT32_MAX, 5, 9, 4}, {5, INT32_MAX, 13, 2}, {9, 13, INT32_MAX, 7}, {4, 2, 7, INT32_MAX}}; int visited[4] = {0}; int path[4] = {0}; int Lmin = INT32_MAX;
int mincost(int step){ int sum = 0, tmp; for(int i = 1; i < n; i++){ tmp = INT32_MAX; if(visited[i] == 0){ for(int j = 0; j < n; j++){ tmp = min(tmp,d[i][j]); } sum += tmp; } } tmp = INT32_MAX; for(int j = 0; j < n; j++){ tmp = min(tmp,d[path[step-1]][j]); } sum += tmp; return sum; }
void dfs(int L, int step){ if(step == n){ cout << "paths: " << endl; for(int i = 0; i < n; i++){ cout << path[i]+1 << " "; } L += d[path[step-1]][0]; if(L < Lmin) Lmin = L; cout << "Length: " << L << endl << endl; return; } int sum = mincost(step); if(L+sum > Lmin){ cout << "CUT OVER" << endl << endl; return; } for(int i = 1; i < n; i++){ if(visited[i] == 0){ visited[i] = 1; path[step] = i; dfs(L+d[path[step-1]][path[step]], step+1); visited[i] = 0; } } }
int main(void){ visited[0] = 1; path[0] = 0; dfs(0, 1); cout << endl << "min Length = " << Lmin; return 1; }
|
运行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| paths: 1 2 3 4 Length: 29
paths: 1 2 4 3 Length: 23
CUT OVER
paths: 1 3 4 2 Length: 23
paths: 1 4 2 3 Length: 28
paths: 1 4 3 2 Length: 29
min Length = 23
|
这表明,当选择排列<1,2,4,3> 或<1,3,4,2> 时,所构成的哈密顿回路路径最短,为23 个单位。
MST+分支定界
动态规划+状压
LKH算法
智能算法
可重复TSP问题
利用最短路径算法(如Floyd算法)将不能连通的节点进行“连通”,此过程记录上路径。
之后再利用求解TSP的算法(如动态规划)进行处理,并反推出路径。
最终将两个路径整理,得到总路径。
MTSP问题
参考资料