第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 60
output
最大价值为:
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;
//初始化X
X.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 15
output
装载方案如下:
第一艘船装: 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++; //三角形计数加1
cout<<"\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; //“-”号统计,因为"+"的值是0
for(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;
//初始化T
T.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;
}
else
cout<<"图中无回路!"<<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]<maxint
while(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)不同列:xixj
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;
}