1、旅行员售货问题
问题描述
某售货员要到若干城市去推销商品,已知各城市之间的路程(旅费),他要选定一条从驻地出发,经过每个城市一遍,最后回到驻地的路线,使总的路程(总旅费)最小。
问题分析
旅行售货员问题的解空间是一棵排列树。对于排列树的回溯法与生成1,2,……n的所有排列的递归算法Perm类似。开始时x=[1,2,……n],则相应的排列树有x[1:n]的所有排列构成。
在递归算法Backtrack中,当i=n时,当前扩展节点是排列树的叶节点的父节点。此时算法检测图G是否存在一条从顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边。如果这两条边都存在,则找到一条旅行员售货回路。此时,算法还需要判断这条回路的费用是否优于已找到的当前最优回流的费用bestc。如果是,则必须更新当前最优值bestc和当前最优解bestx。
当i<n时,当前扩展节点位于排列树的第i-1层。图G中存在从顶点x[i-1]到顶点x[i]的边时,x[1:i]构成图G的一条路径,且当x[1:i]的费用小于当前最优值时算法进入树的第i层,否则将剪去相应的子树。
算法具体代码如下:
//5d9 旅行员售货问题 回溯法求解 #include "stdafx.h" #include <iostream> #include <fstream> using namespace std; ifstream fin("5d9.txt"); const int N = 4;//图的顶点数 template<class Type> class Traveling { template<class Type> friend Type TSP(Type **a, int n); private: void Backtrack(int i); int n, // 图G的顶点数 *x, // 当前解 *bestx; // 当前最优解 Type **a, // 图G的领接矩阵 cc, // 当前费用 bestc; // 当前最优值 int NoEdge; // 无边标记 }; template <class Type> inline void Swap(Type &a, Type &b); template<class Type> Type TSP(Type **a, int n); int main() { cout<<"图的顶点个数 n="<<N<<endl; int **a=new int*[N+1]; for(int i=0;i<=N;i++) { a[i]=new int[N+1]; } cout<<"图的邻接矩阵为:"<<endl; for(int i=1;i<=N;i++) { for(int j=1;j<=N;j++) { fin>>a[i][j]; cout<<a[i][j]<<" "; } cout<<endl; } cout<<"最短回路的长为:"<<TSP(a,N)<<endl; for(int i=0;i<=N;i++) { delete []a[i]; } delete []a; a=0; return 0; } template<class Type> void Traveling<Type>::Backtrack(int i) { if (i == n) { if (a[x[n-1]][x[n]] != 0 && a[x[n]][1] != 0 && (cc + a[x[n-1]][x[n]] + a[x[n]][1] < bestc || bestc == 0)) { for (int j = 1; j <= n; j++) bestx[j] = x[j]; bestc = cc + a[x[n-1]][x[n]] + a[x[n]][1]; } } else { for (int j = i; j <= n; j++) { // 是否可进入x[j]子树? if (a[x[i-1]][x[j]] != 0 && (cc + a[x[i-1]][x[i]] < bestc || bestc == 0)) { // 搜索子树 Swap(x[i], x[j]); cc += a[x[i-1]][x[i]]; //当前费用累加 Backtrack(i+1); //排列向右扩展,排列树向下一层扩展 cc -= a[x[i-1]][x[i]]; Swap(x[i], x[j]); } } } } template<class Type> Type TSP(Type **a, int n) { Traveling<Type> Y; Y.n=n; Y.x=new int[n+1]; Y.bestx=new int[n+1]; for(int i=1;i<=n;i++) { Y.x[i]=i; } Y.a=a; Y.cc=0; Y.bestc=0; Y.NoEdge=0; Y.Backtrack(2); cout<<"最短回路为:"<<endl; for(int i=1;i<=n;i++) { cout<<Y.bestx[i]<<" --> "; } cout<<Y.bestx[1]<<endl; delete [] Y.x; Y.x=0; delete [] Y.bestx; Y.bestx=0; return Y.bestc; } template <class Type> inline void Swap(Type &a, Type &b) { Type temp=a; a=b; b=temp; }
算法backtrack在最坏情况下可能需要更新当前最优解O((n-1)!)次,每次更新bestx需计算时间O(n),从而整个算法的计算时间复杂性为O(n!)。
程序运行结果如图:
2、圆排列问题
问题描述
给定n个大小不等的圆c1,c2,…,cn,现要将这n个圆排进一个矩形框中,且要求各圆与矩形框的底边相切。圆排列问题要求从n个圆的所有排列中找出有最小长度的圆排列。例如,当n=3,且所给的3个圆的半径分别为1,1,2时,这3个圆的最小长度的圆排列如图所示。其最小长度为。
问题分析
圆排列问题的解空间是一棵排列树。按照回溯法搜索排列树的算法框架,设开始时a=[r1,r2,……rn]是所给的n个元的半径,则相应的排列树由a[1:n]的所有排列构成。
解圆排列问题的回溯算法中,CirclePerm(n,a)返回找到的最小的圆排列长度。初始时,数组a是输入的n个圆的半径,计算结束后返回相应于最优解的圆排列。center计算圆在当前圆排列中的横坐标,由x^2 = sqrt((r1+r2)^2-(r1-r2)^2)推导出x = 2*sqrt(r1*r2)。Compoute计算当前圆排列的长度。变量min记录当前最小圆排列长度。数组r表示当前圆排列。数组x则记录当前圆排列中各圆的圆心横坐标。
在递归算法Backtrack中,当i>n时,算法搜索至叶节点,得到新的圆排列方案。此时算法调用Compute计算当前圆排列的长度,适时更新当前最优值。
当i<n时,当前扩展节点位于排列树的i-1层。此时算法选择下一个要排列的圆,并计算相应的下界函数。
算法具体代码如下:
//圆排列问题 回溯法求解 #include "stdafx.h" #include <iostream> #include <cmath> using namespace std; float CirclePerm(int n,float *a); template <class Type> inline void Swap(Type &a, Type &b); int main() { float *a = new float[4]; a[1] = 1,a[2] = 1,a[3] = 2; cout<<"圆排列中各圆的半径分别为:"<<endl; for(int i=1; i<4; i++) { cout<<a[i]<<" "; } cout<<endl; cout<<"最小圆排列长度为:"; cout<<CirclePerm(3,a)<<endl; return 0; } class Circle { friend float CirclePerm(int,float *); private: float Center(int t);//计算当前所选择的圆在当前圆排列中圆心的横坐标 void Compute();//计算当前圆排列的长度 void Backtrack(int t); float min, //当前最优值 *x, //当前圆排列圆心横坐标 *r; //当前圆排列 int n; //圆排列中圆的个数 }; // 计算当前所选择圆的圆心横坐标 float Circle::Center(int t) { float temp=0; for (int j=1;j<t;j++) { //由x^2 = sqrt((r1+r2)^2-(r1-r2)^2)推导而来 float valuex=x[j]+2.0*sqrt(r[t]*r[j]); if (valuex>temp) { temp=valuex; } } return temp; } // 计算当前圆排列的长度 void Circle::Compute(void) { float low=0,high=0; for (int i=1;i<=n;i++) { if (x[i]-r[i]<low) { low=x[i]-r[i]; } if (x[i]+r[i]>high) { high=x[i]+r[i]; } } if (high-low<min) { min=high-low; } } void Circle::Backtrack(int t) { if (t>n) { Compute(); } else { for (int j = t; j <= n; j++) { Swap(r[t], r[j]); float centerx=Center(t); if (centerx+r[t]+r[1]<min)//下界约束 { x[t]=centerx; Backtrack(t+1); } Swap(r[t], r[j]); } } } float CirclePerm(int n,float *a) { Circle X; X.n = n; X.r = a; X.min = 100000; float *x = new float[n+1]; X.x = x; X.Backtrack(1); delete []x; return X.min; } template <class Type> inline void Swap(Type &a, Type &b) { Type temp=a; a=b; b=temp; }如果不考虑计算当前圆排列中各圆的圆心横坐标和计算当前圆排列长度所需的计算时间按,则 Backtrack需要O(n!)计算时间。由于算法Backtrack在最坏情况下需要计算O(n!)次圆排列长度,每次计算需要O(n)计算时间,从而整个算法的计算时间复杂性为O((n+1)!)
上述算法尚有许多改进的余地。例如,像1,2,…,n-1,n和n,n-1, …,2,1这种互为镜像的排列具有相同的圆排列长度,只计算一个就够了,可减少约一半的计算量。另一方面,如果所给的n个圆中有k个圆有相同的半径,则这k个圆产生的k!个完全相同的圆排列,只计算一个就够了。
程序运行结果为: