0032算法笔记——【回溯法】电路板排列问题和连续邮资问题

     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。

《0032算法笔记——【回溯法】电路板排列问题和连续邮资问题》       《0032算法笔记——【回溯法】电路板排列问题和连续邮资问题》

     左上图中,跨越插槽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!)。

     程序运行结果为:

《0032算法笔记——【回溯法】电路板排列问题和连续邮资问题》

     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]已有的贴法所用的张数更少,则更新之

《0032算法笔记——【回溯法】电路板排列问题和连续邮资问题》

    算法具体实现如下:

//连续邮资问题 回溯法求解
#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;
}

     程序运行结果如图:

《0032算法笔记——【回溯法】电路板排列问题和连续邮资问题》
 

    原文作者:回溯法
    原文地址: https://blog.csdn.net/liufeng_king/article/details/8898372
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞