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

旅行商问题是个熟知的问题。这次是因为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
			// vertex
			int[] 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
			// vertex
			int[] 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 include
			DirectedCycle 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 sys

dash_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', dl
		t = 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) 解题报告)