双调欧几里得旅行商问题是一个经典动态规划问题。《算法导论(第二版)》思考题15-1
旅行商问题描述:平面上n个点,确定一条连接各点的最短闭合旅程。这个解的一般形式为NP的(在多项式时间内可以求出)
J.L. Bentley 建议通过只考虑双调旅程(bitonictour)来简化问题,这种旅程即为从最左点开始,严格地从左到右直至最右点,然后严格地从右到左直至出发点。下图(b)显示了同样的7个点的最短双调路线。在这种情况下,多项式的算法是可能的。事实上,存在确定的最优双调路线的O(n*n)时间的算法。
上图中,a是最短闭合路线,这个路线不是双调的。b是最短双调闭合路线
以上截取来自:http://blog.csdn.net/xiajun07061225/article/details/8092247
/** * 欧几里得旅行商(travelling salesman problem)问题 * 分解模型: * 一个人从Pi严格递减的走到P1,然后再严格递增的回到Pj,求总路径的最小值,其中P1,P2,...,Pj的 * 所有节点都要走一次,求最短的路径; * 对于1 <= i <= j <= n, 我们定义P(i, j)是一条包含了P1, P2, P3 ...Pj的途径 * 这条路径可以分成2部分:递减序列与递增序列:起点是Pi(1 <= i <= j),拐点是P1, * 终点是Pj, P[i, j]为其最小值;路径为(Pi-_>Pm-->P1-->Pn-->Pj,其中1<m<i<n<j) * 动态规划方程: * p[i,j]为最小值,dist(m,n)为m与n的直接距离 * p[1,2]=dist(1,2) * i<j-1时,从P(i+1)到Pj之间的点必然出现在递增序列中 * p[i,j]=p[i,j-1]+dist(j-1,j) * i=j-1时,假设P1到Pi之间的一个中间节点Pk,使得p[k,i]+dist(k,j)最小,也就是说,找到了 * 将P1到Pj的所有节点走一遍的最短路径 * p[i,j]=min{p[k,i]+dist(k,j)}(其中1<=k<=i) * p[k,i]又可以按照以上方式继续找出最小路径 * * 对于欧几里得旅行商问题,要求的最短路径是p[n,n] * p[n,n]=p[n-1,n]+dist(n-1,n); * * */
</pre><pre name="code" class="html">
以下是C++实现,来自:http://blog.csdn.net/xiajun07061225/article/details/8092247
#include <iostream> #include <cmath> #include <iomanip> using namespace std; const int n = 7;//点的数目 const int MaxVal = 999999; const int MaxLen = 201; struct tagPoint{ double x,y; }; //计算点i和点j之间的直线距离 double distance(tagPoint *points,int i,int j) { return sqrt((points[i].x - points[j].x) * (points[i].x - points[j].x) + (points[i].y - points[j].y) * (points[i].y - points[j].y)); } double DP(tagPoint *points,int n) { double b[MaxLen][MaxLen];//记录最短路径的长度 //计算所有情况下的b[i][j],1 <= i <= j //初始化 b[1][2] = distance(points,1,2); for (int j = 3;j <= n;++j) { //i < j-1 for (int i = 1;i <= j - 2;++i) { b[i][j] = b[i][j - 1] + distance(points,j - 1,j); } //i = j - 1,b[i][j] = min(b[k][j - 1] + distance(k,j)); b[j - 1][j] = MaxVal; for (int k = 1;k <= j - 2;++k) { double temp = b[k][j - 1] + distance(points,k,j); if (temp < b[j - 1][j]) { b[j - 1][j] = temp; } } } b[n][n] = b[n - 1][n] + distance(points,n - 1,n); return b[n][n]; } int main() { int NUM; while(cin >> NUM) { tagPoint *points = new tagPoint[NUM + 1]; for (int i = 1;i <= NUM;++i) { cin >> points[i].x; cin >> points[i].y; } double minDis = DP(points,NUM); //设置输出格式:精确到小数点后2位 cout.setf(ios::fixed); cout << setprecision(2) << minDis << endl; } }
以下是java实现,基本思想是一样的。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.Scanner; import org.omg.CORBA.PRIVATE_MEMBER; /** * @date 2014-09-20 * @author acer * 欧几里得旅行商(travelling salesman problem)问题 * 分解模型: * 一个人从Pi严格递减的走到P1,然后再严格递增的回到Pj,求总路径的最小值,其中P1,P2,...,Pj的 * 所有节点都要走一次,求最短的路径; * 对于1 <= i <= j <= n, 我们定义P(i, j)是一条包含了P1, P2, P3 ...Pj的途径 * 这条路径可以分成2部分:递减序列与递增序列:起点是Pi(1 <= i <= j),拐点是P1, * 终点是Pj, P[i, j]为其最小值;路径为(Pi-_>Pm-->P1-->Pn-->Pj,其中1<m<i<n<j) * 动态规划方程: * p[i,j]为最小值,dist(m,n)为m与n的直接距离 * p[1,2]=dist(1,2) * i<j-1时,从P(i+1)到Pj之间的点必然出现在递增序列中 * p[i,j]=p[i,j-1]+dist(j-1,j) * i=j-1时,假设P1到Pi之间的一个中间节点Pk,使得p[k,i]+dist(k,j)最小,也就是说,找到了 * 将P1到Pj的所有节点走一遍的最短路径 * p[i,j]=min{p[k,i]+dist(k,j)}(其中1<=k<=i) * p[k,i]又可以按照以上方式继续找出最小路径 * * 对于欧几里得旅行商问题,要求的最短路径是p[n,n] * p[n,n]=p[n-1,n]+dist(n-1,n); * 解释:Pn-->Pn,如果第一个点是Pn-1,那么上面的式子显然成立; * 如果第一个点是Pk,(1<=k<=n-2),那么Pn-1一定在Pk-->Pn的递增序列中,所以 * p[n,n]=p[k,n]+dist(k,n)=p[k,n-1]+dist(n-1,n)+dist(k,n); * 其中p[n-1,n]=p[k,n-1]+dist(k,n) * 所以 p[n,n]=p[n-1,n]+dist(n-1,n)必然成立; * * */ public class TSProblem { public static void main(String[] args) { // TODO Auto-generated method stub String line=null;//读取每一行的输入数据 int[][] pointXY;//记录横纵坐标 System.out.print("请输入点的个数N:"); InputStreamReader isr=new InputStreamReader(System.in); BufferedReader bReader=new BufferedReader(isr); try { line=bReader.readLine().toString(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } int n=Integer.parseInt(line);//记录点的个数 pointXY=new int[n][2]; System.out.println("请输入每个点的坐标,坐标格式为:x,y (以EOF结束输入)"); Scanner scn=new Scanner(System.in); int i=0; while(!"EOF".equals(line=scn.nextLine())){ String[] p=line.split(","); int j=0; for(String str:p){ pointXY[i][j++]=Integer.parseInt(str); } i++; if(i>=n){ System.out.println("输入的点数已经足够,自动结束输入"); break;//退出输入 } } //System.out.println("ok"); int length=pointXY.length; TSProblem tsp=new TSProblem(pointXY,length); } public TSProblem(int[][] points,int length){ //points中存储所有的节点,乱序排列 //1、首先将所有的节点按照x坐标顺序排列,可选用多种排序方式,这里采用快排,时间复杂度为o(nlogn) points=this.QuickSort(points, 0, length-1); // //打印排序后的点 // for(int i=0;i<length;i++){ // System.out.println(points[i][0]+","+points[i][1]); // } //2、找到最短路径 double leastDis=DP(points, length); System.out.println("最短路径为:"+leastDis); } //计算最短旅行路径 double DP(int[][] points,int length){ //p用于记录最短路径的长度 double[][] p=new double[length][length]; p[0][1]=dist(points, 0, 1); for(int j=2;j<length;j++){ //case1:i<j-1 // p[i,j]=p[i,j-1]+dist(j-1,j) for(int i=0;i<j-1;i++){ p[i][j]=p[i][j-1]+dist(points, i, j); } //case2:i=j-1 // p[i,j]=min{p[k,i]+dist(k,j)}(其中1<=k<=i) p[j-1][j]=Float.MAX_VALUE; double temp=0; for(int k=0;k<j-1;k++){ temp=p[k][j-1]+dist(points, k, j); if(temp<p[j-1][j]){ p[j-1][j]=temp; } } } p[length-1][length-1]=p[length-2][length-1]+dist(points, length-2, length-1); //返回Pn-->Pn,即最短路径 return p[length-1][length-1]; } //计算两点之间的直接距离dist(i,j) double dist(int[][] point ,int i,int j){ double distance=0; distance=Math.sqrt(Math.pow((point[j][0] -point[i][0]),2) +Math.pow((point[j][1]-point[i][1]),2)); return distance; } //快排 int[][] QuickSort(int[][] num,int left,int right){ /** * 1、先从数列中取出一个数作为基数 * 2、大数置于该数的右边,小数置于该数的左边 * 3、重复步骤1,2 */ if(left<right){ int tempX=num[left][0];//设置基数,不唯一 int tempY=num[left][1]; int i=left,j=right; while(i<j){ while(i<j&&num[j][0]>=tempX) j--; //右边的数小于基数时退出 if(i<j){ //如果在右边找到一个数小于基数,num[i]此时为空,将该数填到num[i] num[i][0]=num[j][0]; num[i][1]=num[j][1]; i++; } while(i<j&&num[i][0]<tempX) i++; //左边的数大于基数时退出 if(i<j){ //如果在左边找到一个数大于基数,num[j]此时为空,将该数填到num[j] num[j][0]=num[i][0]; num[j][1]=num[j][1]; j--; } }//当i=j时退出 //num[i]为空值,将基数填入 num[i][0]=tempX; num[i][1]=tempY; QuickSort(num, left, i-1);//左边快排 QuickSort(num, i+1, right);//右边快排 } //返回已经排好序的数组 return num; } }