一.问题描述
电路板排列问题是大规模电子系统设计中提出的实际问题.
该问题是: 将n块电路板以最佳排列方案插入带有n个插槽的机箱中. n块电路板的不同的排列方式对应于不同的电路板插入方案.
将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,5。
比如:
对于第一幅图,可以看到跨越插槽2,3的有N2,N3两根导线(注意:一个连接块上的电路板由一根导线连接而成)
跨越插槽4,5的有N4,N1两根导线,同理可得跨越插槽5,6的也是N4,N1两根导线.
其余所有的相邻两个插槽都只跨越了一根导线,
故最终得到的密度就为2.
对于第二幅图,跨越插槽4,5的有N1,N2,N3,N4,N5五条导线.
跨越插槽3,4的有N3,N4,N1,N5四条导线,其它的就不举例了.最终密度为5
这样大家对于密度的定义是不是有了清晰的理解.
二.解题思路
电路板排列问题是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间的连线密度。
划重点!!!
对于这个条件的理解至关重要,我想了很久.
我们可以拿上面第一幅图来看:
先看now[j]>0,(now[j]表示的是x[1:i]中所包含的Nj的电路板数),now[j]大于零表示前面的i个插槽中有连接块j的电路板存在,很好理解.
now[j] != total[j]这里,如果当前面的i个插槽中已经用去了连接块j中的所有电路板,那么就是说剩下的插槽不可能再有连接块j的电路板了,也就不可能再被跨越!
同样的,一个插槽最多被一条导线跨越一次,比如上图1中,对于5,6号插槽来说,N1只跨越5,6插槽一次!故最大我们能够得到的密度为m,也就是每根导线都跨越了某个相邻槽,比如图2.
代码如下:
// 电路板排列问题
#include<bits/stdc++.h>
using namespace std;
class Board
{
friend int Arrangement(int **, int, int, int*);
private:
void Backtrack(int i, int cd);
int n, //电路板数
m, //连接块数
*x, //当前解
*bestx, //当前最优解
bestd, //当前最优密度
*total, //total[j]为连接块j的电路板数
*now, //now[j]为当前解中所含连接块j的电路板数
**B; //连接块数组
};
void Board::Backtrack(int i, int cd) //回溯搜索排列树,cd表示已经确定的x[1:i]个插槽中相邻两个插槽被跨越数最大的(就是密度)
{
static int k = 1;
if(i == n) //由于算法仅完成那些比当前最优解更好的排列,故cd肯定优于bestd,直接更新
{
cout<<"当前已经确定下来最后一个插槽,我们选择"<<x[n]<<endl;
cout<<"第"<<k++<<"个方案为: ";
for(int j=1; j<=n; j++)
{
bestx[j] = x[j];
cout<<x[j]<<" ";
}
bestd = cd;
cout<<"获得的密度为: "<<bestd<<endl<<"到达最后一层,回溯一层到达第"<<n-1<<"层"<<endl;
}
else
{
for(int j=i; j<=n; j++) //选择x[j]为下一块电路板
{
int ld = 0; //新的排列部分密度
for(int k=1; k<=m; k++) //遍历连接块1~m,并且计算得到跨越插槽i和i+1的导线数ld
{
now[k] += B[x[j]][k];
if(now[k]>0 && total[k]!=now[k]) //判断是否发生了跨越(左边有,右边也有)
ld++;
}
cout<<"当前位于第"<<i<<"层,我们选择电路板"<<x[j]<<", 通过计算得到跨越相邻插槽"<<i<<"和"<<i+1<<"的导线数为:"<<ld<<", 目前得到的最大数值为:"<<cd<<endl;
cout<<"当前构造出的now[]数组为(now[j]表示当前解所含连接块j的电路板数): ";
for(int j=1; j<=m; j++) cout<<now[j]<<" ";
cout<<endl;
if(cd > ld) //更新ld,cd为原来的最大密度,ld为当前的最大密度,哪个大取哪个 为什么要这么做?因为每一层我们只可以算出跨越插槽i和i+1的导线数,所以我们必须要和之前的最大值进行比较,取较大者(这是密度的定义)
{
ld = cd;
cout<<"ld<cd, ld已经被更新为"<<cd<<endl;
}
if(ld < bestd) //满足剪枝函数,搜索子树
{
swap(x[i], x[j]);
cout<<"满足剪枝函数,递归深入一层,将到达第"<<i+1<<"层"<<endl;
Backtrack(i+1, ld);
cout<<"当前第"<<i+1<<"层,递归回退一层,将到达第"<<i<<"层"<<endl;
swap(x[i], x[j]);
for(int k=1; k<=m; k++) //恢复状态
now[k] -= B[x[j]][k];
cout<<"第"<<i<<"层撤销选择电路板"<<x[j]<<",恢复now[]数组为(now[j]表示当前解所含连接块j的电路板数): ";
for(int j=1; j<=m; j++) cout<<now[j]<<" ";
cout<<endl;
}
else cout<<"目前获得的密度已经大于最优值,故直接剪枝."<<endl;
if(j==n) cout<<"当前层所有情况遍历完,回溯"<<endl;
}
}
}
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];
}
cout<<"total数组为: ";
for(int i=1; i<=m; i++) cout<<X.total[i]<<" ";
cout<<endl;
X.Backtrack(1, 0);
delete[] X.x;
delete[] X.total;
delete[] X.now;
return X.bestd;
}
int main()
{
cout<<"请输入电路板个数和连接块个数:";
int n, m;
while(cin>>n>>m && n && m)
{
cout<<"输入连接块矩阵"<<endl;
int **B = new int*[n+1];
for(int i=0; i<=n; i++) B[i] = new int[m+1];
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
cin>>B[i][j];
int *bestx = new int[n+1];
for(int i=1; i<=n; i++) bestx[i] = 0;
int ans = Arrangement(B, n, m, bestx);
cout<<"得到的最小密度为:"<<ans<<endl;
for(int i=0; i<=n; i++) delete[] B[i];
delete[] B;
delete[] bestx;
cout<<"请输入电路板个数和连接块个数";
}
system("pause");
return 0;
}
//使用的B数组
//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
针对第一个图演示所得的结果:

运行结果:



我想清楚这题,人没了家人们,/(ㄒoㄒ)/~~
上课没听懂,放了好久才回来写题解.排列树我就不画了,画出来人要没了.
这题也可以将cd作为类的数据成员,从而减少Backtrack函数的传入参数,不过对效率并没有什么优化.
大伙应该能够看懂吧...
欢迎大家访问我的个人博客 --- 乔治的编程小屋,和我一起努力进步吧!