旅行商问题是个熟知的问题。这次是因为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 pylab
import 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秒。

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

旅行商问题(travelling salesman problem, TSP) 解题报告相关推荐

  1. 旅行商问题(Travelling salesman problem, TSP)

    旅行商问题建模与证明 – 个人学习记录

  2. PAT甲级1150 Travelling Salesman Problem:[C++题解]旅行商问题、图论

    文章目录 题目分析 题目链接 题目分析 来源:acwing 分析: 旅行商问题:访问每个城市并回到原城市的最短路. 思路: 1)判断相邻两点有无距离(NA):2)每个点是否都能到:3)是否是回路:4) ...

  3. 单目标应用:求解单仓库多旅行商问题(Single-Depot Multiple Travelling Salesman Problem, SD-MTSP)的人工兔优化算法ARO

    一.算法简介 人工兔优化算法(Artificial Rabbits Optimization ,ARO)由Liying Wang等人于2022年提出,该算法模拟了兔子的生存策略,包括绕道觅食和随机躲藏 ...

  4. cf1504. Travelling Salesman Problem

    cf1504. Travelling Salesman Problem 题意: n个城市,编号1~n,每个城市有美丽值a[i],现在要从城市1出发,其他所有城市走一遍,最后回到城市1,城市i到j的花费 ...

  5. PAT 1150 Travelling Salesman Problem(25 分)- 甲级

    The "travelling salesman problem" asks the following question: "Given a list of citie ...

  6. 旅行商问题(Traveling Salesman Problem,TSP)的+Leapms线性规划模型及c++调用

    知识点 旅行商问题的线性规划模型 旅行商问题的+Leapms模型及CPLEX求解 C++调用+Leapms 旅行商问题 旅行商问题是一个重要的NP-难问题.一个旅行商人目前在城市1,他必须对其余n-1 ...

  7. HDOJ 1002 A + B Problem II 解题报告

    今天研究 1002,,,搞得半死,虽说是一道简单的题目,真正做起来绝不是很容易的... A + B Problem II Time Limit: 2000/1000 MS (Java/Others)  ...

  8. 【HDU 5402】Travelling Salesman Problem(构造)

    被某题卡SB了,结果这题也没读好...以为每一个格子能够有负数就当搜索做了.怎么想也搜只是去,后来发现每一个格子是非负数,那么肯定就是构造题. 题解例如以下: 首先假设nn为奇数或者mm为奇数,那么显 ...

  9. 1150 Travelling Salesman Problem (25 分)【难度: 难 / 知识点: 图 模拟 未完成】

    https://pintia.cn/problem-sets/994805342720868352/problems/1038430013544464384

最新文章

  1. LeetCode 91. Decode Ways--动态规划DP的Python和Java解法
  2. java 打印 排序 数字塔_求助,用循环方法输出以下数字塔
  3. python 各种推导式玩法
  4. python秒表小项目_适合新手练习的几个python小项目
  5. 黑马C++设计模式2
  6. Visual Studio 2013 error MS8020 Build tools v140 cannot be found 解决系统 不同部分软件 所依赖编译器版本不同的矛盾
  7. Spring全家桶面试真题
  8. 邻接矩阵有向图的介绍
  9. 《天天数学》连载08:一月八日
  10. python3 + selenium 之窗口切换
  11. 巡风代码架构简介以及Flask的项目文件结构简介
  12. SQL SERVER 2008数据库管理与维护总结
  13. bug10-graphviz.backend.ExecutableNotFound: failed to execute [‘dot‘, ‘-Tpdf‘, ‘-O‘, ‘Source.gv‘]
  14. 60.Linux/Unix 系统编程手册(下) -- SOCKET: 服务器设计
  15. 网络爬虫研发重点介绍
  16. 搭建自己的OwnCloud私有云
  17. tftp服务器配置及说明
  18. deepfacelab安卓版_Deepfacelab下载
  19. 遥感影像的特征空间图
  20. Python并发编程之进程间通信

热门文章

  1. html实现滑块拖动验证
  2. 工具 | Windows 功能猎手 (WFH)
  3. 第七十四课 图的遍历(BFS)
  4. 中科创达C++ 一面(技术面、24min、offer)
  5. 2017.12.2 用java做一个日历
  6. 几何画板 linux,用几何画板怎么作函数图像
  7. Android SD卡及U盘插拔状态监听和内容读取
  8. vue3获取当前页面组件的路由参数信息
  9. Linux上运行扫雷,基于linux环境下扫雷应用程序.docx
  10. 目录打开显示提示文件或目录损坏且无法读取、文件或目录损坏且无法读取的破解之道