第5章 回溯法

5.2 应用范例

1.0-1背包问题

有n件物品和一个容量为c的背包。第i件物品的重量是w[i],价值是p[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

·算法设计:
(1)解空间:子集树

(2)算法调用递归函数Backtrack
(3)可行性约束函数:
(4)上界函数:当前包内物品重量(cw)+物品的选中情况(x[i])*物品的重量(w[i])<=背包的容量©
(5)最后构造最优解

·代码实现:

#include<stdio.h>
int n,c,bestp;//物品的个数,背包的容量,最大价值
int p[10000],w[10000],x[10000],bestx[10000];//物品的价值,物品的重量,x[i]暂存物品的选中情况,物品的选中情况void Backtrack(int i,int cp,int cw)//cw当前包内物品重量,cp当前包内物品价值
{if(i>n)                      //回溯结束{if(cp>bestp)             //当前重量优于最优解{bestp=cp;            //更新最优解与最优值for(i=0; i<=n; i++){bestx[i]=x[i];}}}else                          //访问左、右子树{for(int j=0; j<=1; j++){x[i]=j;               //j=1访问左子树,j=0访问右子树if(cw+x[i]*w[i]<=c)   //限界函数{cw+=w[i]*x[i];cp+=p[i]*x[i];Backtrack(i+1,cp,cw);cw-=w[i]*x[i];cp-=p[i]*x[i];}}}
}int main()
{int i;bestp=0;printf("请输入背包最大容量:\n");scanf("%d",&c);printf("请输入物品个数:\n");scanf("%d",&n);printf("请依次输入物品的重量:\n");for(i=1; i<=n; i++)scanf("%d",&w[i]);printf("请依次输入物品的价值:\n");for(i=1; i<=n; i++)scanf("%d",&p[i]);Backtrack(1,0,0);printf("最大价值为:\n");printf("%d\n",bestp);printf("被选中的物品依次是(0表示未选中,1表示选中):\n");for(i=1; i<=n; i++)printf("%d ",bestx[i]);printf("\n");return 0;
}
/*
样例:
input请输入背包最大容量:
70
请输入物品个数:
4
请依次输入物品的重量:
20 15 25 30
请依次输入物品的价值:
60 25 55 60output最大价值为:
145
被选中的物品依次是(0表示未选中,1表示选中):
1 1 0 1
*/

2.装载问题

有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且
装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。如果有,找出一种装载方案。

·问题分析:
容易证明,如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近。由此可知,装载问题等价于以下特殊的0-1背包问题。

·复杂度:
用回溯法设计解装载问题的O(2n)计算时间算法。在某些情况下该算法优于动态规划算法。

·算法设计:
解空间:子集树最合适。

(4)算法MaxLoading调用递归函数Backtrack
(5)可行性约束函数(选择当前元素):
(3)上界函数,减去不含最优解的子树:当前载重量(cw)+剩余集装箱的重量®<=当前最优载重量(bestw)

(4)最后构造最优解

·代码实现:

#include <iostream>
using namespace std;typedef int* pINT;
template<class Type>
class Loading
{public:friend Type MaxLoading(Type* w,int num ,Type C1,int* bestx);friend void SolveLoading(int C2,bool* x,int* w,int num);void Backtrack(int i);int num;        //集装箱数目int * x;        //当前解int * bestx;    //当前最优解Type* w;        //集装箱重量数组Type C1;        //第一艘船的容量Type cw;        //当前载重量Type bestw;     //当前最优载重量Type r;         //剩余集装箱重量
};//搜索第i层结点
template<class Type>
void Loading<Type>::Backtrack(int i)
{//[1]到达叶结点if(i > num){if (cw > bestw)                 //当前重量优于最优解{for (int i = 1; i <= num ; i++){bestx[i] = x[i];        //更新最优解与最优值bestw = cw;}}return ;}//[2]搜索子树r -= w[i];if (cw+w[i] <= C1)          //搜索左子树{x[i] = 1;cw += w[i];Backtrack(i+1);cw -= w[i];             //恢复状态}if (cw+r > bestw)           //可能存在最优解,搜索右子树(剪枝条件:cw+r<=bestw){x[i] = 0;Backtrack(i+1);}r += w[i];                  //恢复状态
}//求解最优装载
template<class Type>
Type MaxLoading(Type* w,int num ,Type C1,int* bestx)
{Loading<Type> X;//初始化XX.x = new int[num+1];X.w = w;X.C1= C1;X.num = num;X.bestx = bestx;X.bestw = 0;X.cw = 0;X.r = 0;for (int i = 1; i <= num ; i++){X.r += w[i];}X.Backtrack(1);delete[] X.x;return X.bestw;
}
//输出最优装载
template<class Type>
void SolveLoading(int C2,int* x,Type* w,int num)
{int totalW = 0;int c1W = 0;          //第一艘船总载重for (int i = 1; i <= num ; i++){if (x[i] == 1){c1W += w[i];}totalW += w[i];}if (totalW-c1W > C2){cout<<"没有合理的装载方案! :( ";return;}cout<<"装载方案如下:\n";cout<<"第一艘船装: ";for (int i = 1; i <= num ; i++){if ( x[i] == 1 ){cout<<i<<" ";}}cout<<"\n总载重:"<<c1W<<"\n";cout<<"第二艘船装: ";for (int i = 1; i <= num ; i++){if ( ! x[i] ){cout<<i<<" ";}}cout<<"\n总载重:"<<totalW-c1W<<"\n";
}int main(int argc,char* argv[])
{int C1 = 0;int C2 = 0;int num = 0;int* x = NULL;int** m = NULL;int* w = NULL;cout<<"输入第一艘船最大载重量:";cin>>C1;cout<<"输入第二艘船最大载重量:";cin>>C2;cout<<"输入货物个数:";cin>>num;x = new int[num+1];w = new int[num+1];m = new pINT[num+1];for (int i = 0; i < num+1 ; i++){m[i] = new int[num+1];}cout<<"分别输入货物重量(回车结束):\n";for (int i = 1; i <= num ; i++){cin>>w[i];}MaxLoading(w, num, C1, x);SolveLoading(C2, x, w, num);delete[] x;delete[] w;delete[] m;return 0;
}
/*
测试样例:
input输入第一艘船最大载重量:150
输入第二艘船最大载重量:100
输入货物个数:7
分别输入货物重量(回车结束):
10 35 25 30 40 55 15output
装载方案如下:
第一艘船装: 1 4 5 6 7
总载重:150
第二艘船装: 2 3
总载重:60
*/

3.符号三角形问题

下图是由14个“+”和14个“-”组成的符号三角形。2个同号下面都是“+”,2个异号下面都是“-”。

在一般情况下,符号三角形的第一行有n个符号。符号三角形问题要求对于给定的n,计算有多少个不同的符号三角形,使其所含的“+”和“-”的个数相同。

·算法设计:
(1)解向量:用n元组x[1:n]表示符号三角形的第一行
(2)算法调用递归函数Backtrack
(3)可行性约束函数:当前符号三角形所包含的“+”个数与“-”个数均不超过n*(n+1)/4
(4)无解的判断:n*(n+1)/2为奇数
(5)构造最优解

·代码实现:

#include <iostream>
#include <string.h>
using namespace std;char cc[2]= {'+','-'};  //便于输出
int n;                  //第一行符号总数
int half;               //全部符号总数一半
int counter;            //1计数,即“-”号计数
int **p;                //符号存储空间
long sum;               //符合条件的三角形计数void Backtrace(int t)   //第一行第t个符号
{int i,j;if(t > n)           //符号填充完毕,打印符号{sum++;          //三角形计数加1cout<<"\n第"<<sum<<"个三角形:"<<endl;for(i=1; i<=n; i++){for(j=1; j<i; j++){cout << " ";}for(j=1; j<=n-i+1; j++){cout << cc[ p[i][j] ] << " ";}cout << endl;}}else{for(i=0; i<2; i++){p[1][t] = i;                 //第一行第t个符号counter += i;                //“-”号统计,因为"+"的值是0for(j=2; j<=t; j++)          //当第一行符号>=2时,可以运算出下面行的某些符号,j可代表行号{p[j][t-j+1] = p[j-1][t-j+1]^p[j-1][t-j+2];  //通过异或运算下行符号,t-j+1确定的很巧counter += p[j][t-j+1];}//剪枝[2](限界函数):若符号统计未超过半数,并且另一种符号也未超过半数,同时隐含两者必须相等才能结束if((counter <= half) && (t*(t+1)/2 - counter <= half)){Backtrace(t+1);          //在第一行增加下一个符号}//回溯,判断另一种符号情况,像是出栈一样,恢复所有对counter的操作for(j=2; j<=t; j++){counter -= p[j][t-j+1];}counter -= i;}}
}int main()
{cout << "请输入第一行符号个数n:";cin >> n;counter = 0;sum = 0;half = n*(n+1)/2;int i;//剪枝[1](约束函数):总数须为偶数,若为奇数则无解if( half%2 == 0 ){half /= 2;p = new int *[n+1];for(i=0; i<=n; i++){p[i] = new int[n+1];memset(p[i], 0, sizeof(int)*(n+1));}Backtrace(1);for(i=0; i<=n; i++)   //删除二维动态数组的方法{delete[] p[i];}delete[] p;}cout<<"-----------------------------"<<endl;cout<<"共有"<<sum<<"个符号三角形"<<endl;return 0;
}

4.旅行售货员问题

某售货员要到若干个城市去推销商品,已知各个城市之间的路程(旅费)。他要选择一条从驻地出发,必须经过各个城市且仅一遍,最后返回到驻地的总成本(路程或费用等)最小路线。
即在带权图G=(V,E)中寻找一条成本最小的周游路线。

·算法设计:
(1)解空间:一棵排列树
(2)算法调用递归函数Backtrack
(3)剪枝函数:x[1:i]的费用小于当前最优值时算法进入树的第i层,否则将剪去相应子树。
(4)构造最优解

·算法分析:
对于排列树的回溯法与生成1,2,……n的所有排列的递归算法类似。开始时x=[1,2,……n],则相应的排列树有x[1:n]的所有排列构成。
在递归算法Backtrack中,
(1)当i=n时,当前扩展节点是排列树的叶节点的父节点。此时算法检测图G是否存在一条从顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边。如果这两条边都存在,则找到一条旅行员售货回路。此时,算法还需要判断这条回路的费用是否优于已找到的当前最优回流的费用bestc。如果是,则必须更新当前最优值bestc和当前最优解bestx。
(2)当i<n时,当前扩展节点位于排列树的第i-1层。图G中存在从顶点x[i-1]到顶点x[i]的边时,x[1:i]构成图G的一条路径,且当x[1:i]的费用小于当前最优值时算法进入树的第i层,否则将剪去相应的子树。

·复杂度分析:
算法backtrack在最坏情况下可能需要更新当前最优解O((n-1)!)次,每次更新bestx需计算时间O(n),从而整个算法的计算时间复杂性为O(n!)。

·代码实现:

#include <iostream>
using namespace std;
#define N 4
#define NoEdge -1        //无边标记-1
class Traveling
{friend double TSP(double (*a)[N+1], int n);
private:void Backtrack(int i);void Swap(int &x, int &y);int n;               //图G的顶点数int *x;              //当前解int *bestx;          //最优解,保存全排列中最优的解double (*a)[N+1];    //图G的邻接矩阵double cc;           //当前费用double bestc;        //当前最优值bool iscycle;        //判断是否有回路
};
void Traveling::Backtrack(int i) //对数组x中第i起到结尾进行全排列的试探,数组x下标为0的元素保留不用
{if(i == n) //找到符合条件的全排列{if (a[x[i-1]][x[i]] != NoEdge && a[x[i]][x[1]] != NoEdge && (bestc > cc + a[x[i-1]][x[i]] +a[x[i]][x[1]] || bestc == NoEdge)) //判断是否有回路、发现最优值{iscycle = true;bestc = cc + a[x[i-1]][x[i]] +a[x[i]][x[1]]; //保存最优值for (int i = 1; i <= n; i++){bestx[i] = x[i]; //保存最优解}}}else{for (int j =i; j <= n; j++){if(a[x[i-1]][x[j]] != NoEdge && (cc + a[x[i-1]][x[j]] < bestc || bestc == NoEdge))// 是否可进入x[j]子树{// 搜索子树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]);}}}
}
void Traveling::Swap(int &x, int &y)
{int temp;temp = x;x = y;y= temp;
}
double TSP(double (*a)[N+1], int n)
{Traveling T;//初始化TT.bestc = NoEdge;T.cc = 0;T.n = n;T.x = new int[n+1];T.bestx = new int[n+1];T.a = a;T.iscycle = false;//置x为单位排列for (int i = 1; i <= n; i++){T.x[i] = i;}T.Backtrack(2); //以T.x数组中下标为1的顶点作为旅行售货员的出发点。if (T.iscycle){cout<<"\n旅行售货员的最优回路代价:"<<T.bestc<<endl<<"旅行售货员的最优回路路径:";for (int i = 1; i <= n; i++){cout<<T.bestx[i]<<" ";}cout<<1<<endl;}elsecout<<"图中无回路!"<<endl;delete [] T.x;delete [] T.bestx;return T.bestc;
}
int main(int argc, char* argv[])
{//对图a初始化double a[N+1][N+1];a[1][2] = 30;a[1][3] = 6;a[1][4] = 4;a[2][3] = 5;a[2][4] = 10;a[3][4] = 20;for (int i = 1; i <= N; i++){for (int j = i + 1; j<= N; j++){a[j][i] = a[i][j];}}cout<<"图的顶点个数 n="<<N<<endl;TSP(a, N);return 0;
}

5.圆排列问题

给定n个大小不等的圆c1,c2,…,cn,现要将这n个圆排进一个矩形框中,且要求各圆与矩形框的底边相切。圆排列问题要求从n个圆的所有排列中找出有最小长度的圆排列。例如,当n=3,且所给的3个圆的半径分别为1,1,2时,这3个圆的最小长度的圆排列如图所示。其最小长度为

·算法设计:
(5)解空间:一棵排列树
(6)算法调用递归函数Backtrack
(7)剪枝函数:x[1:i]的费用小于当前最优值时算法进入树的第i层,否则将剪去相应子树。
(8)构造最优解

·算法分析:
按照回溯法搜索排列树的算法框架,设开始时a=[r1,r2,……rn]是所给的n个元的半径,则相应的排列树由a[1:n]的所有排列构成。

解圆排列问题的回溯算法中,
(1)初始时,数组a是输入的n个圆的半径,计算结束后返回相应于最优解的圆排列。
(2)center计算圆在当前圆排列中的横坐标,由x2 = sqrt((r1+r2)2-(r1-r2)2)推导出x = 2*sqrt(r1*r2)。
(3)Compoute计算当前圆排列的长度。变量min记录当前最小圆排列长度。数组r表示当前圆排列。
(4)数组x则记录当前圆排列中各圆的圆心横坐标。

在递归算法Backtrack中,
(1)当i>n时,算法搜索至叶节点,得到新的圆排列方案。此时算法调用Compute计算当前圆排列的长度,适时更新当前最优值。
(2)当i<n时,当前扩展节点位于排列树的i-1层。此时算法选择下一个要排列的圆,并计算相应的下界函数。

·复杂度分析:
由于算法backtrack在最坏情况下可能需要计算O(n!)次当前圆排列长度,每次计算需O(n)计算时间,从而整个算法的计算时间复杂性为O((n+1)!)

·代码实现:

//圆排列问题 回溯法求解
#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;    //样例结果为2+4*sqrt(2)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;
}

6.连续邮资问题

假设国家发行了n种不同面值的邮票,并且规定每张信封上最多只允许贴m张邮票。连续邮资问题要求对于给定的n和m的值,给出邮票面值的最佳设计,在1张信封上可贴出从邮资1开始,增量为1的最大连续邮资区间。
例如,当n=5和m=4时,面值为(1,3,11,15,32)的5种邮票可以贴出邮资的最大连续邮资区间是1到70。

·算法设计:
(1)解向量:用n元组x[1:n]表示n种不同的邮票面值,并约定它们从小到大排列。x[1]=1是唯一的选择。
(2)可行性约束函数:已选定x[1:i-1],最大连续邮资区间是[1:r],接下来x[i]的可取值范围是[x[i-1]+1:r+1]。

·算法分析:
如何确定r的值?
计算X[1:i]的最大连续邮资区间在本算法中被频繁使用到,因此势必要找到一个高效的方法。考虑到直接递归的求解复杂度太高,我们不妨尝试计算用不超过m张面值为x[1:i]的邮票贴出邮资k所需的最少邮票数y[k]。通过y[k]可以很快推出r的值。事实上,y[k]可以通过递推在O(n)时间内解决:

for (int j=0; j<= x[i-2]*(m-1);j++)if (y[j]<m)for (int k=1;k<=m-y[j];k++)if (y[j]+k<y[j+x[i-1]*k]) y[j+x[i-1]*k]=y[j]+k;while (y[r]<maxint) r++;

·算法实现:

//连续邮资问题 回溯法求解
#include <iostream>
using namespace std;class Stamp
{friend int MaxStamp(int  ,int  ,int []);private:int Bound(int i);void Backtrack(int i,int r);int n;           //邮票面值数int m;           //每张信封最多允许贴的邮票数int maxvalue;    //当前最优值int maxint;      //大整数int maxl;        //邮资上界int *x;          //当前解int *y;          //贴出各种邮资所需最少邮票数int *bestx;      //当前最优解
};int MaxStamp(int n,int m,int bestx[]);int main()
{int *bestx;int n = 5;int m = 4;cout<<"邮票面值数:"<<n<<endl;cout<<"每张信封最多允许贴的邮票数:"<<m<<endl;bestx=new int[n+1];for(int i=1;i<=n;i++){bestx[i]=0;}cout<<"最大邮资:"<<MaxStamp(n,m,bestx)<<endl;cout<<"当前最优解:";for(int i=1;i<=n;i++){cout<<bestx[i]<<"  ";}cout<<endl;return 0;
}void Stamp::Backtrack(int i,int r)
{/**动态规划方法计算数组y的值。状态转移过程:*考虑将x[i-1]加入等价类集S中,将会引起数组x*能贴出的邮资范围变大,对S的影响是如果S中的*邮票不满m张,那就一直贴x[i-1],直到S中有m张*邮票,这个过程会产生很多不同的邮资,取能产生*最多不同邮资的用邮票最少的那个元素*/for(int j=0;j<=x[i-2]*(m-1);j++){if(y[j]<m){for(int k=1;k<=m-y[j];k++)//k x[i-1]的重复次数{if(y[j]+k<y[j+x[i-1]*k]){y[j+x[i-1]*k]=y[j]+k;}}}}//如果y[r]的值在上述动态规划运算过程中已赋值,则y[r]<maxintwhile(y[r]<maxint){r++;}if(i>n){if(r-1>maxvalue){maxvalue=r-1;for(int j=1;j<=n;j++){bestx[j]=x[j];}}return;}int *z=new int[maxl+1];for(int k=1;k<=maxl;k++){z[k]=y[k];}for(int j=x[i-1]+1;j<=r;j++){x[i]=j;Backtrack(i+1,r);for(int k=1;k<=maxl;k++){y[k]=z[k];}}delete[] z;
}int MaxStamp(int n,int m,int bestx[])
{Stamp X;int maxint=32767;int maxl=1500;X.n=n;X.m=m;X.maxvalue=0;X.maxint=maxint;X.maxl=maxl;X.bestx=bestx;X.x=new int [n+1];X.y=new int [maxl+1];for(int i=0;i<=n;i++){X.x[i]=0;}for(int i=1;i<=maxl;i++){X.y[i]=maxint;}X.x[1]=1;X.y[0]=0;X.Backtrack(2,1);delete[] X.x;delete [] X.y;return X.maxvalue;
}
/*
样例:
邮票面值数:5
每张信封最多允许贴的邮票数:4
最大邮资:70
当前最优解:1  3  11  15  32
*/

7.电路板排列问题

将n块电路板以最佳排列方式插入带有n个插槽的机箱中,n块电路板的不同排列方式对应不同的电路板插入方案。
设B={1, 2, …, n}是n块电路板的集合,
L={N1, N2, …, Nm}是连接这n块电路板中若干电路板的m个连接块。
Ni是B的一个子集,且Ni中的电路板用同一条导线连接在一起。
x表示n块电路板的一个排列,即在机箱的第i个插槽中插入的电路板编号是x[i]。
x所确定的电路板排列Density (x)密度定义为跨越相邻电路板插槽的最大连线数。

例:
如图,设n=8, m=5,
给定n块电路板及其m个连接块:
B={1, 2, 3, 4, 5, 6, 7, 8},N1={4, 5, 6},N2={2, 3},
N3={1, 3},N4={3, 6},N5={7, 8};
其中两个可能的排列如图所示,则该电路板排列的密度分别是2,3。

下1图中,跨越插槽2和3,4和5,以及插槽5和6的连线数均为2。插槽6和7之间无跨越连线。其余插槽之间只有1条跨越连线。在设计机箱时,插槽一侧的布线间隙由电路板的排列的密度确定。因此,电路板排列问题要求对于给定的电路板连接条件(连接块),确定电路板的最佳排列,使其具有最小密度。
  
·算法设计:
(1)解空间:电路板排列问题是NP难问题,因此不大可能找到解此问题的多项式时间算法。考虑采用回溯法系统的搜索问题解空间的排列树,找出电路板的最佳排列。
(2)设用数组B表示输入,B[i][j]的值为1当且仅当电路板i在连接块Nj中,total[j]是连接块Nj中的电路板数,对于电路板的部分排列x[1:i],now[j]是x[1:i]中所包含的Nj中的电路板数,由此可知,连接块Nj的连线跨越插槽i和i+1当且仅当now[j]>0且now[j]!=total[j],用这个条件来计算插槽i和i+1间的连线密度。

·算法实现:

#include <iostream>
#include <fstream>
#include <queue>
#include <algorithm>
using namespace std;const int MAX = 50;
int p[MAX][MAX];
int bestx[MAX];
int n, m;      //电路板数,连接块数class Node
{public:int dep;   //当前深度int cd;    //当前排列长度int *x;    //存储当前排列x[1:dep]int *low;  //电路块中最左边电路板int *high; //电路块中最右边电路板Node(){cd = 0;dep = 0;high = new int[m+1];low = new int[m+1];x = new int[n+1];}int len()  //计算当前排列最小长度{int temp = 0;for(int j=1; j<=m; j++){if(low[j]<=n && high[j]>0 && temp<high[j]-low[j])temp = high[j] - low[j];}return temp;}
};int search()
{queue<Node> q;Node enode;int bestd = n + 1;int i, j;for(j=1; j<=m; j++){enode.high[j] = 0;enode.low[j] = n + 1;}for(i=1; i<=n; i++)enode.x[i] = i;while(true){if(enode.dep == n-1) //仅一个儿子结点,已经排完n-1个电路板,现在排最后一个{for(int j=1; j<=m; j++)if(p[ enode.x[n] ][j]>0 && n>enode.high[j])enode.high[j] = n;enode.cd = enode.len();if(enode.cd < bestd){bestd = enode.cd;copy(enode.x, enode.x+n+1, bestx);}}else{int cur = enode.dep + 1;for(i=enode.dep+1; i<=n; i++)  //产生当前扩展结点的所有儿子结点{Node now;for(int j=1; j<=m; j++){now.low[j] = enode.low[j];now.high[j] = enode.high[j];if(p[ enode.x[i] ][j] > 0){if(cur < now.low[j])now.low[j] = cur;if(cur > now.high[j])now.high[j] = cur;}}now.cd = now.len();if(now.cd < bestd){now.dep = enode.dep + 1;copy(enode.x, enode.x+n+1, now.x);now.x[now.dep] = enode.x[i];     //相当于回溯中的swap(x[dep], x[i])now.x[i] = enode.x[now.dep];q.push(now);}}}if(q.empty())break;else{enode = q.front();  //下一层扩展结点q.pop();}}return bestd;
}int main()
{ifstream fin("input.txt");cout << "输入电路板个数:";fin >> n;cout << n;cout << "\n输入连接块个数:";fin  >> m;cout << m;cout << "\n输入矩阵:\n";int i, j;for(i=1; i<=n; i++){for(j=1; j<=m; j++){fin >> p[i][j];cout << p[i][j] << " ";}cout << endl;}cout << "\n排列的最小长度为:" << search();cout << "\n最佳排列为:\n" ;for(i=1; i<=n; i++)cout << bestx[i] << " ";cout << endl;cout << endl;fin.close();return 0;
}
/*
样例:
10 4
0 0 1 0 0
0 1 0 0 0
0 1 1 1 0
1 0 0 0 0
1 0 0 0 0
1 0 0 1 0
0 0 0 0 1
0 0 0 0 1
*/

8.n后问题

在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。

·算法设计:
(1)解向量:(x1, x2, … , xn)
(2)显约束:xi=1,2, … ,n
(3)隐约束:
1)不同列:xixj
2)不处于同一正、反对角线:|i-j||xi-xj|

·算法分析:

*回溯法解N皇后问题
*使用一个一维数组表示皇后的位置
*其中数组的下标表示皇后所在的行
*数组元素的值表示皇后所在的列
*这样设计的棋盘,所有皇后必定不在同一行
*
*假设前n-1行的皇后已经按照规则排列好
*那么可以使用回溯法逐个试出第n行皇后的合法位置
*所有皇后的初始位置都是第0列
*那么逐个尝试就是从0试到N-1
*如果达到N,仍未找到合法位置
*那么就置当前行的皇后的位置为初始位置0
*然后回退一行,且该行的皇后的位置加1,继续尝试
*如果目前处于第0行,还要再回退,说明此问题已再无解
*
*如果当前行的皇后的位置还是在0到N-1的合法范围内
*那么首先要判断该行的皇后是否与前几行的皇后互相冲突
*如果冲突,该行的皇后的位置加1,继续尝试
*如果不冲突,判断下一行的皇后
*如果已经是最后一行,说明已经找到一个解,输出这个解
*然后最后一行的皇后的位置加1,继续尝试下一个解

·算法实现:

#include<stdio.h>
#define MAX_LENGTH 1024
/** 检查第n行的皇后与前n-1行的皇后是否有冲突* 发生冲突的充分必要条件是:* a) 两皇后处于同一列,即a[i] == a[n]* b) 两皇后处于同一斜线,即|a[i] - a[n]| == |i - n| == n - i*/
int k=1;
int is_conflict(int *a, int n)
{int flag = 0;int i;for ( i = 0; i < n; i++ ){if ( a[i] == a[n] || a[i] - a[n] == n - i || a[n] - a[i] == n - i ){flag = 1;break;}}return flag;
}
/** 输出皇后的排列*/
void print_board(int *a, int n)
{int i, j;printf("第%d个排列为:\n",k++);for ( i = 0; i < n; i++ ){for ( j = 0; j < a[i]; j++ ){printf("○");}printf("●");for ( j = a[i] + 1; j < n; j++ ){printf("○");}printf("\n");}printf("------------------\n");
}
/** 初始化棋盘,所有皇后都在第0列*/
void init_board(int *a, int n)
{int i;for ( i = 0; i < n; i++ ){a[i] = 0;}
}
/** 解决N皇后问题*/
int queen(int n)
{int count = 0;int a[MAX_LENGTH];init_board(a, n);int i = 0;while ( 1 ){if ( a[i] < n ){// 如果皇后的位置尚未超出棋盘范围// 需要检查第i行的皇后是否与前i-1行的皇后冲突if ( is_conflict(a, i) ){// 如果冲突,尝试下一个位置a[i]++;continue;}if ( i >= n - 1 ){// 如果已经到最后一行,也即找到一个解,首先输出它count++;print_board(a, n);// 然后尝试当前行的皇后的下一个位置a[n-1]++;continue;}// 没有冲突,尝试下一行i++;continue;}else{// 皇后的位置已经超出棋盘范围// 那么该行的皇后复位a[i] = 0;// 回退到上一行i--;if ( i < 0 ){// 已经不能再退了,函数结束return count;}// 尝试上一行的皇后的下个位置a[i]++;continue;}}
}
int main(void)
{int n = 8;int count = queen(n);printf("%d solutions in %d queens problem\n", count, n);return 0;
}

算法设计与分析第5章 回溯法(二)【回溯法应用】相关推荐

  1. 算法设计与分析第4章 动态规划(二)【DP序列问题】

    第3章 动态规划(二)[DP序列问题] 3.2 DP序列问题 (51nod的动态规划教程很不错,讲解很详细,以下分析来自51nod) 1.矩阵取数问题 给定一个m行n列的矩阵,矩阵每个元素是一个正整数 ...

  2. 算法设计与分析第七章分支限界算法(完结篇)

    算法设计与分析第七章分支限界算法 一.分支界限算法概述 1.分支限界法类似于回溯法,是一种在问题的解空间树上搜索问题解的算法. 分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解 ...

  3. 计算机算法设计与分析第五章思维导图知识点总结 ( 初稿 )

    复习链接 计算机算法设计与分析第一章思维导图 计算机算法设计与分析第二章思维导图&&知识点总结 计算机算法设计与分析第三章思维导图&&知识点总结 计算机算法设计与分析第 ...

  4. 算法设计与分析第5章 回溯法(一)【回溯法】

    第5章 回溯法 5.1 回溯法 1.回溯法的提出  有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法. 2. 问题的解空间 (1)问题的解向量:回溯法希望 ...

  5. [XJTUSE 算法设计与分析] 第五章 回溯法

    第五章 回溯法 填空题会有代码填空,大题会手动回溯 学习要点 理解回溯法的深度优先搜索策略. 掌握用回溯法解题的算法框架 (1)递归回溯 (2)迭代回溯 (3)子集树算法框架 (4)排列树算法框架 5 ...

  6. 算法设计与分析基础 第一章谜题

    习题1.1 10.b 欧几里得游戏 一开始,板上写有两个不相等的正整数,两个玩家交替写数字,每一次,当前玩家都必须在板上写出任意两个板上数字的差,而且这两个数字必须是新的,也就是说,不能与板上任何一个 ...

  7. 【算法设计zxd】第四章蛮力法 1.枚举法 02穷举查找

    目录 蛮力法(brute force): [例4-1]链环数字对  问题分析  计算模型 pair_digital(int n): 代码: [例4-2]解数字迷: 思考题:ACM预测:​ 问题分析 ...

  8. 算法设计与分析——第五章回溯法 批处理作业调度 + 最大团问题+图的m着色问题

    文章目录 6.批处理作业调度(排列树) 7.最大团问题 8.图的m着色问题 6.批处理作业调度(排列树) 每一个作业Ji都有两项任务分别在2台机器上完成.每个作业必须先有机器1处理,然后再由机器2处理 ...

  9. 算法设计与分析第1章 算法概述

    第1章 算法概述(穷举算法) 重要人物:Alan Turing(图灵机).Donald Knuth(TEX系统) 算法:解决问题的一种方法或一个过程 特性:有穷性(Finiteness).确定性(De ...

最新文章

  1. 计算机应用能力考试xp,全国专业技术人员计算机应用能力考试XP
  2. matplotlib pcolormech 用法
  3. Android2.2 r1 API 中文文档系列(10) —— CheckBox
  4. Qt 4.8.4 Qt Creator 2.6.1 安装和配置(Windows)
  5. ycsb 测试验证模式的mongodb
  6. JeecgBoot版本4月份新版即将发布,抢先体验。。
  7. 【JAVA 第三章 流程控制语句】课后习题 月历打印
  8. catia曲面设计从入门到精通_CATIA V5 曲面设计从入门到精通
  9. 为什么有这么多网站有操作系统下载呢?做系统的会不会放病毒呢?
  10. 学 Win32 汇编[27] - 乘除指令: MUL、IMUL、DIV、IDIV
  11. python 正则表达式简介
  12. matcaffe训练与测试
  13. 数模美赛准备——Numpy
  14. PB的特点及Powerscript的语言基础
  15. Flash动画短片制作流程注意点
  16. android里面的USB功能-----Accessory模式
  17. 使用Python进行数独求解(一)
  18. 岁月的感知,生命的守望
  19. layer的btn按钮
  20. 数据仓库-hive分区表

热门文章

  1. 2022-2028年中国盲盒行业市场研究及前瞻分析报告
  2. 2022-2028年中国煤制甲醇产业投资分析及前景预测报告
  3. Python 高级特性 (1)- 闭包
  4. 1 用python进行OpenCV实战之用OpenCV3实现图片载入、显示和储存
  5. pytorch方法,Tensor及其基本操作_重点
  6. LeetCode简单题之删列造序
  7. 使用Nucleus SE实时操作系统
  8. 电子设计搜索引擎引入分析和见解
  9. 拥挤场景中的目标检测
  10. CVPR2020行人重识别算法论文解读