1、电路板排列问题
问题描述
将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。
左上图中,跨越插槽2和3,4和5,以及插槽5和6的连线数均为2。插槽6和7之间无跨越连线。其余插槽之间只有1条跨越连线。在设计机箱时,插槽一侧的布线间隙由电路板的排列的密度确定。因此,电路板排列问题要求对于给定的电路板连接条件(连接块),确定电路板的最佳排列,使其具有最小密度。
问题分析
电路板排列问题是NP难问题,因此不大可能找到解此问题的多项式时间算法。考虑采用回溯法系统的搜索问题解空间的排列树,找出电路板的最佳排列。设用数组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 "stdafx.h" #include <iostream> #include <fstream> using namespace std; ifstream fin("5d11.txt"); class Board { friend int Arrangement(int **B, int n, int m, int bestx[]); private: void Backtrack(int i,int cd); int n, //电路板数 m, //连接板数 *x, //当前解 *bestx,//当前最优解 bestd, //当前最优密度 *total, //total[j]=连接块j的电路板数 *now, //now[j]=当前解中所含连接块j的电路板数 **B; //连接块数组 }; template <class Type> inline void Swap(Type &a, Type &b); int Arrangement(int **B, int n, int m, int bestx[]); int main() { int m = 5,n = 8; int bestx[9]; //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} cout<<"m="<<m<<",n="<<n<<endl; cout<<"N1={4,5,6},N2={2,3},N3={1,3},N4={3,6},N5={7,8}"<<endl; cout<<"二维数组B如下:"<<endl; //构造B int **B = new int*[n+1]; for(int i=1; i<=n; i++) { B[i] = new int[m+1]; } for(int i=1; i<=n; i++) { for(int j=1; j<=m ;j++) { fin>>B[i][j]; cout<<B[i][j]<<" "; } cout<<endl; } cout<<"当前最优密度为:"<<Arrangement(B,n,m,bestx)<<endl; cout<<"最优排列为:"<<endl; for(int i=1; i<=n; i++) { cout<<bestx[i]<<" "; } cout<<endl; for(int i=1; i<=n; i++) { delete[] B[i]; } delete[] B; return 0; } void Board::Backtrack(int i,int cd)//回溯法搜索排列树 { if(i == n) { for(int j=1; j<=n; j++) { bestx[j] = x[j]; } bestd = cd; } else { for(int j=i; j<=n; j++) { //选择x[j]为下一块电路板 int ld = 0; for(int k=1; k<=m; k++) { now[k] += B[x[j]][k]; if(now[k]>0 && total[k]!=now[k]) { ld ++; } } //更新ld if(cd>ld) { ld = cd; } if(ld<bestd)//搜索子树 { Swap(x[i],x[j]); Backtrack(i+1,ld); Swap(x[i],x[j]); //恢复状态 for(int k=1; k<=m; k++) { now[k] -= B[x[j]][k]; } } } } } int Arrangement(int **B, int n, int m, int bestx[]) { Board X; //初始化X X.x = new int[n+1]; X.total = new int[m+1]; X.now = new int[m+1]; X.B = B; X.n = n; X.m = m; X.bestx = bestx; X.bestd = m+1; //初始化total和now for(int i=1; i<=m; i++) { X.total[i] = 0; X.now[i] = 0; } //初始化x为单位排列并计算total for(int i=1; i<=n; i++) { X.x[i] = i; for(int j=1; j<=m; j++) { X.total[j] += B[i][j]; } } //回溯搜索 X.Backtrack(1,0); delete []X.x; delete []X.total; delete []X.now; return X.bestd; } template <class Type> inline void Swap(Type &a, Type &b) { Type temp=a; a=b; b=temp; }算法效率
在解空间排列树的每个节点处,算法Backtrack花费O(m)计算时间为每个儿子节点计算密度。因此计算密度所消耗的总计算时间为O(mn!)。另外,生成排列树需要O(n!)时间。每次更新当前最优解至少使bestd减少1,而算法运行结束时bestd>=0。因此最优解被更新的额次数为O(m)。更新最优解需要O(mn)时间。综上,解电路板排列问题的回溯算法Backtrack所需要的计算时间为O(mn!)。
程序运行结果为:
2、连续邮资问题
问题描述
假设国家发行了n种不同面值的邮票,并且规定每张信封上最多只允许贴m张邮票。连续邮资问题要求对于给定的n和m的值,给出邮票面值的最佳设计,在1张信封上可贴出从邮资1开始,增量为1的最大连续邮资区间。例如,当n=5和m=4时,面值为(1,3,11,15,32)的5种邮票可以贴出邮资的最大连续邮资区间是1到70。
问题分析
解向量:用n元组x[1:n]表示n种不同的邮票面值,并约定它们从小到大排列。x[1]=1是唯一的选择。
可行性约束函数:已选定x[1:i-1],最大连续邮资区间是[1:r],接下来x[i]的可取值范围是[x[i-1]+1:r+1]。
计算X[1:i]的最大连续邮资区间在本算法中被频繁使用到,因此势必要找到一个高效的方法。直接递归的求解复杂度太高,我们不妨尝试计算用不超过m张面值为x[1:i]的邮票贴出邮资k所需的最少邮票数y[k]。通过y[k]可以很快推出r的值。如果y[r]的值在上述动态规划运算过程中已赋值,则y[r]<maxint。语句while(y[r]<maxint) r++可以很快的计算出r值。关键是如何计算数组y,分析过程如下:
r表示由x[1…i]能贴出的最大连续区间,现在,要想把第i层的结点往下扩展,有两个问题需要解决:一,哪些数有可能成为下一个的邮票面值,即x[i+1]的取值范围是什么;二,对于一个确定的x[i+1],如何更新r的值让它表示x[1…i+1]能表示的最大连续邮资区间。
第一个问题很简单,x[i+1]的取值要和前面i个数各不相同,最小应该是x[i] + 1,最大就是r+1,否则r+1没有办法表示。我们现在专注第二个问题。
第二个问题自己有两种思路:一,计算出所有使用不超过m张x[1…i+1]中的面值能够贴出的邮资,然后从r+1开始逐个检查是否被计算出来。二,从r+1开始,逐个询问它是不是可以用不超过m张x[1…i+1]中的面值贴出来。
两种思路直接计算其计算量都是巨大的,需要借助动态规划的方法。模仿0-1背包问题,假设S(i)表示x[1…i]中不超过m张邮票的贴法的集合,这个集合中的元素数目是巨大的,例如,只使用1张邮票的贴法有C(i+1-1,1)=C(i,1)=i种,使用2张邮票的贴法有C(i+2-1,2)=C(i+1,2)=i*(i+1)/2种,……,使用m张邮票的贴法有C(i+m-1, m)种,其中C(n,r)表示n个元素中取r个元素的组合数。于是,S(i)中的元素的数目总共有C(i+1-1, 1) + C(i+2-1,2)+ … + C(i+m-1,m)个。S(i)中的每个元素就是一种合法的贴法,对应一个邮资。当前最大连续邮资区间为1到r,那么S(i)中每个元素的邮资是不是也在1到r之间呢?不一定,比如{1,2,4},当m=2时,它能贴出来8,但不能贴出来7。总之,在搜索时,一定要保持状态的一致性,即当深度搜索到第i层时,一定要确保用来保存结点状态的变量中保存的一定是第i层的这个结点的状态。定义S(i)中元素的值就是它所表示的贴法贴出来的邮资,于是,可以把S(i)中的元素按照它们的值的相等关系分成k类。第j类表示贴出邮资为j的所有的贴法集合,用T(j)表示,T(j)有可能是空集,例如对于{1,2,4},T(7)为空集,T(8)={{4,4}}。此时有:S(i) = T(1) U T(2) U T(3) U … U T(k),U表示两个集合的并。
现在考虑x[i+1]加入后对当前状态S(i)的影响。假设s是S(i)中的一个元素,即s表示一种合法的贴法,x[i+1]对s能贴出的邮资的影响就是x[i+1]的多次重复增加了s能贴出的邮资。x[i+1]对s的影响就是,如果s中贴的邮票不满m张,那就一直贴x[i+1],直到s中有m张邮票,这个过程会产生出很多不同的邮资,它们都应该被加入到S(i+1)中。因为s属于S。
综上分析,考虑如果使用动态规划方法计算数组y的值,状态转移过程:将x[i-1]加入等价类集S中,将会引起数组x能贴出的邮资范围变大,对S的影响是如果S中的邮票不满m张,那就一直贴x[i-1],直到S中有m张邮票,这个过程会产生很多不同的邮资,取能产生最多不同邮资的用邮票最少的那个元素。
例如:如下图所示,设m=4,n=5。当x[1]=1时,2张{1,1}可以贴出邮资2。这时,设x[2]=3。将3往{1,1}中添加,产生新的邮资贴法:5:{3,1,1},8:{3,3,1,1}。这时,程序需要更新数组y的值。如果新的贴法比y[5],y[8]已有的贴法所用的张数更少,则更新之。
算法具体实现如下:
//连续邮资问题 回溯法求解 #include "stdafx.h" #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; }程序运行结果如图: