排列算法0031算法笔记——旅行员售货问题和圆排列问题回溯法求解
新手发帖,很多方面都是刚入门,有错误的地方请大家见谅,欢迎批评指正
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!个完全雷同的圆排列,只算计一个就够了。
程序运行结果为:
文章结束给大家分享下程序员的一些笑话语录:
小沈阳版程序员~~~ \n程序员其实可痛苦的了......需求一做一改,一个月就过去了;嚎~ \n需求再一改一调,一季度就过去了;嚎~ \n程序员最痛苦的事儿是啥,知道不?就是,程序没做完,需求又改了; \n程序员最最痛苦的事儿是啥,知道不? 就是,系统好不容易做完了,方案全改了; \n程序员最最最痛苦的事儿是啥,知道不? 就是,系统做完了,狗日的客户跑了; \n程序员最最最最最痛苦的事儿是啥,知道不? 就是,狗日的客户又回来了,程序给删没了!