旅行商问题(travelling salesman problem, TSP) 解题报告

来源:互联网 发布:淘宝开店后如何推广 编辑:程序博客网 时间:2024/05/22 06:25

旅行商问题是个熟知的问题。这次是因为coursera上面选的算法课而写代码实现。这里做个简单总结。

测试程序:

25
20833.3333 17100.0000
20900.0000 17066.6667
21300.0000 13016.6667
21600.0000 14150.0000
21600.0000 14966.6667
21600.0000 16500.0000
22183.3333 13133.3333
22583.3333 14300.0000
22683.3333 12716.6667
23616.6667 15866.6667
23700.0000 15933.3333
23883.3333 14533.3333
24166.6667 13250.0000
25149.1667 12365.8333
26133.3333 14500.0000
26150.0000 10550.0000
26283.3333 12766.6667
26433.3333 13433.3333
26550.0000 13850.0000
26733.3333 11683.3333
27026.1111 13051.9444
27096.1111 13415.8333
27153.6111 13203.3333
27166.6667 9833.3333
27233.3333 10450.0000

说明:这是25个城市的二维坐标,城市之间的距离为欧几里得距离。求从一个城市出发,每个城市只走一遍,再回到原城市的最短路程。

背景:http://en.wikipedia.org/wiki/Travelling_salesman_problem。 wiki百科对TSP的历史,精确解法,近似解法等都做了归纳。

根据题目要求,我要求的是一个精确解。维基百科说得不是很具体。可能大家都知道的一个精确解法是用动态规划,时间复杂度为O(n^22^n)。对于这样一个时间和空间都是指数复杂度的解法,遇到20以上节点就非常吃力了。我对其具体实现想得也不是很清楚,所以没有采用。后来使用的方法是branch and bound。其实就是搜索加剪枝。如果一个部分解的下界已经超过了当前的最好解,则该部分解的分支就不必进行了。可以看出,对一个具有n!解空间的问题,剪枝是否有效(当然,前提是正确)对算法的时间和空间消耗有很大的影响。

我个人觉得很有用的两个资料是:

1.http://www.academic.marist.edu/~jzbv/algorithms/Branch%20and%20Bound.htm

2.http://lcm.csa.iisc.ernet.in/dsa/node187.html

简单说明,部分解指的是只确定了部分城市的路径,还有一部分城市没有探索。部分解的下界估计值得的已确定的路径长度加上没确定的城市的路径最小的可能值。合起来就作为这个部分解的下界。即这个部分解所有的可能的完整解的最小值的下界。

两种链接1中的方法很简单,对于一个部分解,估计它的下界使用的方法是,当前已确定的路径长度加上没有确定路径的城市的最短邻接路径长度之和。这种方法实现起来很简单,能给人信心,但是好处也就到这里。对这个25个城市的TSP问题,我跑了24个小时都没跑出结果。。。链接中提供了Java代码实现,我的C++实现见后面。因为没有跑出结果,所以不能保证我的程序的正确性。

#include <iostream>#include <cassert>#include <climits>#include <vector>#include <queue>#include <ctime>using namespace std;const int N = 25;int n;double cities[N][2];double dis[N][N];double minedge[N];struct TSPNode{vector<int> path;double plen;double bnd;TSPNode(){plen = 0;bnd = -1;}double pathlen(){return plen;}int size(){return path.size();}int lastcity(){if(size() < 1){fprintf(stdout, "error: path is empty.\n");return -1;}return path[path.size() - 1];}void add(int city){path.push_back(city);}void copypath(const vector<int> &p){path = p;}bool contains(int city){for(int i = 0; i < path.size(); ++i){if(path[i] == city){return true;}}return false;}double bound(){if(bnd >= 0){return bnd;}bnd = plen;bool marked[N];memset(marked, false, sizeof(marked));for(int i = 0; i < path.size() - 1; ++i){marked[path[i]] = true;}for(int i = 0; i < n; ++i){if(!marked[i]){bnd += minedge[i];}}return bnd;}bool operator<(TSPNode& other){return bound() < other.bound();}};class CompareTSPNode {public:bool operator()(TSPNode& t1, TSPNode& t2){if(t1.bound() - t1.size()> t2.bound() - t2.size()) {return true;}//if(abs(t1.bound() - t2.bound()) < 1 && t1.size() < t2.size())//{//return true;//}return false;}};int main(){FILE *fin = fopen("tsp.txt", "r");freopen("log.txt", "w", stdout);fscanf(fin, "%d", &n);assert(n == N);for(int i = 0; i < n; ++i){fscanf(fin, "%lf%lf", &cities[i][0], &cities[i][1]);}//for(int i = 0; i < n; ++i)//{//fprintf(stdout, "%d: (%.4lf, %.4lf)\n", i, cities[i][0], cities[i][1]);//}for(int i = 0; i < n; ++i){//dis[i][i] = 0;for(int j = i + 1; j < n; ++j){dis[i][j] = sqrt((cities[i][0] - cities[j][0]) * (cities[i][0] - cities[j][0])+ (cities[i][1] - cities[j][1]) * (cities[i][1] - cities[j][1]));dis[j][i] = dis[i][j];}}int initialpath[] = {0, 1, 5, 10, 9, 11, 14, 18, 17, 21, 22, 20, 19, 24, 23, 15, 16, 13, 12, 8, 6, 2, 7, 3, 4, 0};double infinity = 0;for(int i = 0; i < n; ++i){infinity += dis[initialpath[i]][initialpath[i + 1]];}fprintf(stdout, "%lf\n", infinity);for(int i = 0; i < n; ++i){dis[i][i] = infinity;minedge[i] = infinity;for(int j = 0; j < n; ++j){if(dis[i][j] < minedge[i]){minedge[i] = dis[i][j];}}}//priority_queue<TSPNode, vector<TSPNode>, CmpTSPNode> pq;priority_queue<TSPNode, vector<TSPNode>, CompareTSPNode> pq;//priority_queue<TSPNode> pq; TSPNode node;node.add(0);//node.bnd = node.calbound();pq.push(node);time_t start;time(&start);//cout<<"start: "<<start<<endl;time_t before = start, now;double mincost = infinity;while(!pq.empty()){TSPNode node = pq.top();pq.pop();if(node.bound() >= mincost){continue;}fprintf(stdout, "size: %d, bound: %.4lf\n", node.size(), node.bound());fprintf(stdout, "path: ");for(int i = 0; i < node.size(); ++i){fprintf(stdout, "%d,", node.path[i]);}fprintf(stdout, "\n");if(node.size() == n - 1){int remaining;for(int i = 0; i < n; ++i){if(!node.contains(i)){remaining = i;break;}}double cost = node.plen + dis[node.lastcity()][remaining] + dis[remaining][0];if(cost < mincost){mincost = cost;fprintf(stdout, "mincost: %.4lf\n", mincost);time(&now);fprintf(stdout, "[%d secs].\n", now - start);}continue;}for(int i = 0; i < n; ++i){if(!node.contains(i)){TSPNode newn;newn.path = node.path;newn.add(i);newn.plen = node.plen + dis[node.lastcity()][i];//newn.bnd = newn.calbound();if(newn.bound() < mincost){pq.push(newn);}}}}return 0;}

方法2对下界的估计更准确,当然也更复杂。已出现在路径中的边不会再作为未出现的点的边进行估计。而且对每个点,取的是左右两条最小边的一半,这比只取一条边要准确。具体分析见链接(或的Java实现),还包括对不正确状态的判断(如产生环等)。我的最后能求出正确解的方法就是这个。附Java代码实现见后面。

import java.util.Iterator;import java.util.LinkedList;import java.util.List;public class TSPBranchAndBound {private int N;private double mincost;private List<Integer> tour;private double[][] dis;class TSPNode {Digraph include;Digraph exclude;double length;DirectedEdge next;public TSPNode(){include = new Digraph(N);exclude = new Digraph(N);length = 0;next = new DirectedEdge(0, 1, dis[0][1]);}public TSPNode(TSPNode node){include = new Digraph(node.include);exclude = new Digraph(node.exclude);length = node.length;next = new DirectedEdge(0, 1, dis[0][1]);}private DirectedEdge next(DirectedEdge edge){int v = edge.from();int w = edge.to() + 1;if (w == v){w += 1;}if (w >= N){w = 0;v += 1;}if (v >= N){next = null;} else{next = new DirectedEdge(v, w, dis[v][w]);}return next;}public void include(DirectedEdge edge){include.addEdge(edge.from(), edge.to());length += dis[edge.from()][edge.to()];next = next(edge);}public void exclude(DirectedEdge edge){exclude.addEdge(edge.from(), edge.to());next = next(edge);}public List<Integer> tour(){List<Integer> cities = new LinkedList<Integer>();cities.add(0);int v = 0, w;while (true){Iterator<Integer> iter = include.adj(v).iterator();if (iter.hasNext()){w = iter.next();if (iter.hasNext()){return null;}if (w == 0){if (cities.size() == N){return cities;}return null;}cities.add(w);v = w;} else{return null;}}}public int size(){return include.E();}public double bound(){int[] in = new int[N];int[] out = new int[N];for (int i = 0; i < N; ++i){in[i] = -1;out[i] = -1;}for (int i = 0; i < N; ++i){Iterator<Integer> iter = include.adj(i).iterator();if (iter.hasNext()){int w = iter.next();out[i] = w;in[w] = i;}}boolean[][] inexclude = new boolean[N][N];boolean[][] outexclude = new boolean[N][N];for (int i = 0; i < N; ++i){Iterator<Integer> iter = exclude.adj(i).iterator();while (iter.hasNext()){int w = iter.next();outexclude[i][w] = true;inexclude[w][i] = true;}}double bnd = 0;for (int i = 0; i < N; ++i){if (in[i] == -1){double minin = -1;for (int j = 0; j < N; ++j){if (j == i || inexclude[i][j]){continue;}if (minin < 0 || dis[j][i] < minin){minin = dis[j][i];}}if (minin < 0){throw new RuntimeException("has no possible inbound vertex.");}bnd += minin;}else {bnd += dis[in[i]][i];}}for (int i = 0; i < N; ++i){if (out[i] == -1){double minout = -1;for (int j = 0; j < N; ++j){if (j == i || outexclude[i][j]){continue;}if (minout < 0 || dis[i][j] < minout){minout = dis[i][j];}}if (minout < 0){throw new RuntimeException("has no possible outbound vertex.");}bnd += minout;}else {bnd += dis[i][out[i]];}}bnd /= 2;return bnd;}public boolean isValid(){// check less than two adjacent edges(in and out) include for every// vertexint[] out = new int[N];int[] in = new int[N];for (int i = 0; i < N; ++i){Iterator<Integer> iter = include.adj(i).iterator();if (iter.hasNext()){out[i]++;int w = iter.next();in[w]++;if (in[w] > 1){return false;}if (iter.hasNext()){return false;}}}// check include + not exclude edges equal or larger than two for// every// vertexint[] inexclude = new int[N];int[] outexclude = new int[N];for (int i = 0; i < N; ++i){Iterator<Integer> iter = exclude.adj(i).iterator();while (iter.hasNext()){outexclude[i]++;int w = iter.next();inexclude[w]++;}}for (int i = 0; i < N; ++i){if (inexclude[i] > N - 2){return false;}if (outexclude[i] > N - 2){return false;}}// check cycle not exist or length equals N for includeDirectedCycle finder = new DirectedCycle(include);if (finder.hasCycle()){int cnt = 0;for (int v : finder.cycle()){cnt++;}if (cnt < N){return false;}}return true;}}public TSPBranchAndBound(String file){In in = new In(file);N = in.readInt();dis = new double[N][N];double[][] cities = new double[N][2];for (int i = 0; i < N; ++i){cities[i][0] = in.readDouble();cities[i][1] = in.readDouble();}for (int i = 0; i < N; ++i){for (int j = i + 1; j < N; ++j){dis[i][j] = Math.sqrt(Math.pow(cities[i][0] - cities[j][0], 2)+ Math.pow(cities[i][1] - cities[j][1], 2));dis[j][i] = dis[i][j];}}/*for(int i = 0; i < N; ++i){StdOut.print(i + ": ");for(int j = 0; j < N; ++j){StdOut.print((int)dis[i][j] + "\t");}StdOut.println();}*//*int initialpath[] = {0, 1, 5, 9, 10, 11, 14, 18, 17, 21, 22, 20, 16, 19, 24, 23, 15, 13, 12, 8, 6, 2, 3, 7, 4, 0};//int initialpath[] = {0, 1, 5, 10, 9, 11, 14, 18, 17, 21, 22, 20, 19, 24, 23, 15, 16, 13, 12, 8, 6, 2, 7, 3, 4, 0};//int initialpath[] = {0, 1, 4, 3, 2, 0};double cost = 0;for (int i = 0; i < initialpath.length - 1; ++i){cost += dis[initialpath[i]][initialpath[i + 1]];}StdOut.println("length: " + initialpath.length + ", cost: " + cost);*//* * mincost = 0; for (int i = 0; i < N - 1; ++i) { mincost += dis[i][i + * 1]; } mincost += dis[N - 1][0]; StdOut.println("inital mincost: " + * mincost); */mincost = Double.POSITIVE_INFINITY;//mincost = 26443;tour = new LinkedList<Integer>();TSPNode node = new TSPNode();branch(node);}private void branch(TSPNode node){if (node.size() == N){//assert node.length == node.bound() : "length and bound not equal";StdOut.println("old: " + mincost + ", new: " + node.length);if (node.length < mincost){mincost = node.length;tour = node.tour();}return;}if (node.next == null){return;}DirectedEdge next = node.next;TSPNode with = new TSPNode(node);with.include(next);TSPNode without = new TSPNode(node);without.exclude(next);if (!with.isValid()){if (without.isValid()){double withoutbnd = without.bound();if (withoutbnd < mincost){branch(without);}}} else{if (!without.isValid()){double withbnd = with.bound();if (withbnd < mincost){branch(with);}} else{double withbnd = with.bound(), withoutbnd = without.bound();if (withbnd > withoutbnd){if (withoutbnd < mincost){branch(without);}if (withbnd < mincost){branch(with);}} else{if (withbnd < mincost){branch(with);}if (withoutbnd < mincost){branch(without);}}}}}public void report(){StdOut.println("mincost: " + mincost + "\n");for (int i = 0; i < tour.size(); ++i){StdOut.print(tour.get(i));if (i != tour.size() - 1){StdOut.print(", ");} else{StdOut.println();}}}public static void main(String[] args){long start = System.nanoTime();TSPBranchAndBound tsp = new TSPBranchAndBound("tsp.txt");tsp.report();StdOut.println("time elapsed: " + ((double)System.nanoTime() - start) / 1000000000);}}


这个问题我的实现也就到这里了。从分析中可以看出,这种方法的下界判断还是过于宽泛,并没有限制这些最小的边能连接起来,而且下界计算和状态正确性判断比较复杂,每个状态需要记录的信息也很多,至少需要包括包含和排除的边,所以空间复杂度也很高。我采用的方法是用python画图显示出来了这25个城市,然后手工指定了一条路径。作为初始的最优解,这大大减少了程序运行时间和空间。但还是用了一晚上的时间(第二天睡醒看到的正确结果)。

import pylabimport sysdash_style = (    (0, 20, -15, 30, 10),    (1, 30, 0, 15, 10),    (0, 40, 15, 15, 10),    (1, 20, 30, 60, 10),    )fig = pylab.figure()ax = fig.add_subplot(111)with open(sys.argv[1], "r") as f:n = int(f.readline())for i in range(n):x, y = [float(x) for x in f.readline().split()]ax.plot(x, y, marker='o')(dd, dl, r, dr, dp) = dash_style[1 if i == 22 else (i % 2)]#print 'dashlen call', dlt = ax.text(x, y, str(i), withdash=True,               dashdirection=dd,               dashlength=dl,               rotation=r,               dashrotation=dr,               dashpush=dp,               )#ax.set_xlim((20000, 5.0))#ax.set_ylim((0.0, 5.0))pylab.show()

在论坛的讨论中我看到的最好的解法是这样的。对于一个部分解{a, S, b},即从a出发,经过集合S中的点,到达b(集合S包括a和b,最初始状态为{a, {a}, a})。用V表示所有城市的集合。这个部分解的下界等于a到V-S的最小距离,加上V-S的最小生成树的长度,再加上V-S到b的最小距离之和。这个解法从分析就可以看出每个状态下界需要记录的信息很小(只包括当前的路径),时间复杂度很低(bfs + prim或kruskal + bfs)。我没有实现,但据讨论说只有2秒。

再一次感叹,算法的效率真是有天壤之别啊!


原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 华为畅享7plus主板坏了怎么办 华为手机手机主板坏了没备份怎么办 华为手机一年内主板坏了怎么办 华为手机保修期内主板坏了怎么办 华为手机外置sd卡满了怎么办 红米4充不进去电怎么办 苹果5s锁屏密码忘记了怎么办 华为手机桌面和锁屏自动一样怎么办 苹果手机没电了没带充电器怎么办 华为p8手机后摄像头模糊的怎么办 中兴手机充电的地方坏了怎么办? 小米手机与电脑蓝牙传输失败怎么办 捡个华为收机没有账号密码怎么办 华为手机p9激活码忘了怎么办 华为畅享7plus声音小怎么办 华为畅享7plus忘记密码怎么办 华为畅享8plus卡顿怎么办 华为畅享7plus卡机怎么办 华为畅享8plus图标字小怎么办 华为畅享6反应慢发热怎么办 华为畅享5S反应迟钝怎么办 华为畅玩5x玩王者荣耀卡怎么办 不小心把手机里的照片删了怎么办 u盘文件在手机上删了怎么办 荒野行动透视挂功能加载失败怎么办 白色t桖衫被奶茶弄脏了该怎么办 游戏文件不小心解压到c盘了怎么办 装系统时c盘0mb怎么办 电脑设置的开机密码忘了怎么办 电脑开机密码忘了怎么办xp系统 我的电脑在开机时忘了密码怎么办? xp桌面我的电脑图标不见了怎么办 游戏全屏时卡了无法退到界面怎么办 u盘插电脑上提示有病毒怎么办 三星手机文件怎么删除不掉怎么办 用夜神模拟器玩第五人格太卡怎么办 雷电模拟器玩刺激战场太卡了怎么办 绝地求生刺激战场模拟器太卡怎么办 ddj sb2打碟功能没了怎么办 驼背怎么办 要能快速矫正的方法 苹果7中间的home键坏了怎么办