装载问题
问题描述:
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且,装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。如果有,找出一种装载方案。
例如:当n=3,c1=c2=50,且w=[10,40,40]时,则可以将集装箱1和2装到第一艘轮船上,而将集装箱3装到第二艘轮船上;如果w=[20,40,40],则无法将这3个集装箱都装上轮船。
基本思路:
容易证明,如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近C1。由此可知,装载问题等价于以下特殊的0-1背包问题。
用回溯法设计解装载问题的O(2^n)计算时间算法。在某些情况下该算法优于动态规划算法。
算法设计:
用回溯法解装载问题时,用子集树表示其解空间显然是最合适的。用可行性约束函数可剪去不满足约束条件的子树。在子集树的第j+1层的结点z处,用cw记当前的装载重量,即cw=,则当cw>c1时,以结点z为根的子树中所有结点都不满足约束条件,因而该子树中的解均为不可行解,故可将该子树剪去。(该约束函数去除不可行解,得到所有可行解)。
可以引入一个上界函数,用于剪去不含最优解的子树,从而改进算法在平均情况下的运行效率。设z是解空间树第i层上的当前扩展结点。cw是当前载重量;bestw是当前最优载重量;r是剩余集装箱的重量,即r=。定义上界函数为cw+r。在以z为根的子树中任一叶结点所相应的载重量均不超过cw+r。因此,当cw+r<=bestw时,可将z的右子树剪去。
递归回溯具体代码如下:
#include <iostream>
using namespace std;
template <class Type>
class Loading
{
//friend Type MaxLoading(Type[],Type,int,int []);
//private:
public:
void Backtrack(int i);
int n, //集装箱数
*x, //当前解
*bestx; //当前最优解
Type *w, //集装箱重量数组
c, //第一艘轮船的载重量
cw, //当前载重量
bestw, //当前最优载重量
r; //剩余集装箱重量
};
template<class Type>
Type MaxLoading(Type w[], Type c, int n, int bestx[]);
int main()
{
int n=3,m;
int c=50,c2=50;
int w[4]={0,10,40,40};
int bestx[4];
m=MaxLoading(w, c, n, bestx);
cout<<"轮船的载重量分别为:"<<endl;
cout<<"c(1)="<<c<<",c(2)="<<c2<<endl;
cout<<"待装集装箱重量分别为:"<<endl;
cout<<"w(i)=";
for (int i=1;i<=n;i++)
{
cout<<w[i]<<" ";
}
cout<<endl;
cout<<"回溯选择结果为:"<<endl;
cout<<"m(1)="<<m<<endl;
cout<<"x(i)=";
for (int i=1;i<=n;i++)
{
cout<<bestx[i]<<" ";
}
cout<<endl;
int m2=0;
for (int j=1;j<=n;j++)
{
m2=m2+w[j]*(1-bestx[j]);
}
cout<<"m(2)="<<m2<<endl;
if(m2>c2)
{
cout<<"因为m(2)大于c(2),所以原问题无解!"<<endl;
}
return 0;
}
template <class Type>
void Loading <Type>::Backtrack (int i)// 搜索第i层结点
{
if (i > n)// 到达叶结点
{
if (cw>bestw)
{
for(int j=1;j<=n;j++)
{
bestx[j]=x[j];//更新最优解
bestw=cw;
}
}
return;
}
r-=w[i];
if (cw + w[i] <= c) // 搜索左子树
{
x[i] = 1;
cw += w[i];
Backtrack(i+1);
cw-=w[i];
}
if (cw + r > bestw)
{
x[i] = 0; // 搜索右子树
Backtrack(i + 1);
}
r+=w[i];
}
template<class Type>
Type MaxLoading(Type w[], Type c, int n, int bestx[])//返回最优载重量
{
Loading<Type>X;
//初始化X
X.x=new int[n+1];
X.w=w;
X.c=c;
X.n=n;
X.bestx=bestx;
X.bestw=0;
X.cw=0;
//初始化r
X.r=0;
for (int i=1;i<=n;i++)
{
X.r+=w[i];
}
X.Backtrack(1);
delete []X.x;
return X.bestw;
}
迭代回溯 具体代码如下:
#include <iostream>
using namespace std;
template<class Type>
Type MaxLoading(Type w[ ], Type c, int n, int bestx[ ]);
int main()
{
int n=3,m;
int c=50,c2=50;
int w[4]={0,10,40,40};
int bestx[4];
m=MaxLoading(w, c, n, bestx);
cout<<"轮船的载重量分别为:"<<endl;
cout<<"c(1)="<<c<<",c(2)="<<c2<<endl;
cout<<"待装集装箱重量分别为:"<<endl;
cout<<"w(i)=";
for (int i=1;i<=n;i++)
{
cout<<w[i]<<" ";
}
cout<<endl;
cout<<"回溯选择结果为:"<<endl;
cout<<"m(1)="<<m<<endl;
cout<<"x(i)=";
for (int i=1;i<=n;i++)
{
cout<<bestx[i]<<" ";
}
cout<<endl;
int m2=0;
for (int j=1;j<=n;j++)
{
m2=m2+w[j]*(1-bestx[j]);
}
cout<<"m(2)="<<m2<<endl;
if(m2>c2)
{
cout<<"因为m(2)大于c(2),所以原问题无解!"<<endl;
}
return 0;
}
template <class Type>
Type MaxLoading(Type w[],Type c,int n,int bestx[])//迭代回溯法,返回最优载重量及其相应解,初始化根结点
{
int i=1;//当前层,x[1:i-1]为当前路径
int *x=new int[n+1];
Type bestw=0, //当前最优载重量
cw=0, //当前载重量
r=0; //剩余集装箱重量
for (int j=1;j<=n;j++)
{
r+=w[j];
}
while(true)//搜索子树
{
while(i<=n &&cw+w[i]<=c)//进入左子树
{
r-=w[i];
cw+=w[i];
x[i]=1;
i++;
}
if (i>n)//到达叶结点
{
for (int j=1;j<=n;j++)
{
bestx[j]=x[j];
}
bestw=cw;
}
else//进入右子树
{
r-=w[i];
x[i]=0; i++;
}
while (cw+r<=bestw)
{ //剪枝回溯
i--;
while (i>0 && !x[i])
{
r+=w[i];
i--;
}
//从右子树返回
if (i==0)
{
delete []x;
return bestw;
}
x[i]=0;
cw-=w[i];
i++;
}
}
}
子集和问题
题目
给定有n个不同正整数的集合w=(w1,w2,… ,wn)和一个正数W,要求找出w的子集s,式该子集中所有元素的和为W。例如,当n=4时,w=(11,13,24,7),W=31,则满足要求的子集为(11,13,7)和(24,7)
分析
n=4时,解空间树如图(结点中的数字是结点的编号,例如结点18对应解向量为(1,1,0,1),选择的整数和=11+13+7=31),从i层到i+1层(1<=i<=n)的每一条边标有xi的值,xi或者为1或者为0,xi为1时表示取wi为整数,xi为0时表示不去wi为整数,从根节点到叶子结点的所有路径定义了解空间。
时间复杂度 O(2n),解空间树中有 2(n+1) -1个结点
求解该问题需要搜索整个解空间树,设解向量x=(x1,x2,…,xn),本问题是求所有解,所以一旦搜索到叶子结点(即i=n+1),如果相应的子集和为W,则输出x解向量。搜索到第i(1<=i<=n)层的某个结点时用tw表示选取的整数和,rw表示余下的整数和,rw表示余下的整数和,rw=w[j] (j从i+1到n)
(1)约束函数:检查当前整数w[i]加入子集和是否超过W,若超过,则不能选择该路径。用于左孩子结点剪枝。
(2)限界函数:一个结点满足tw+rw<W,即即使选择剩余的所有整数,也不可能找到一个解。用于右孩子剪枝。
代码
#include <iostream>
using namespace std;
#define MAXN 100
int n=5, c = 10;
int w[MAXN]={0,2,2,6,5,4}; //存放所有整数,不要下标为0的元素
int count = 0; //累计解个数
void print(int x[])
{
int i;
printf("第%d个解:\n", ++count);
for (int i = 1; i <= n; i++)
if (x[i] == 1)
printf("%d ", w[i]);
printf("\n");
}
void dfs(int tw, int rw, int x[], int i) //求解子集和
{
// tw 考虑第i个整数时选取的整数和,rw为剩下的整数和
if (i > n) //找到一个叶子结点
{
if (tw == c) //找到一个满足条件的解输出
print(x);
}
else //尚未找完所有整数
{
if (tw + w[i] <= c) //左孩子结点剪枝:选取满足条件的整数w[i]
{
x[i] = 1; //选取第i个整数
dfs(tw + w[i], rw - w[i], x, i + 1);
}
if (tw + rw > c) //右孩子结点剪枝:剪出不可能存在解的结点
{
x[i] = 0; //不选取第i个整数,回溯
dfs(tw, rw - w[i], x, i + 1);
}
}
}
int main()
{
// cin>>n>>c;
// for(int i=1;i<=n;i++){
// cin>>w[i];
// }
int x[MAXN]; //存放一个解向量
int rw = 0;
for (int j = 1; j <= n; j++) //求所有整数和
rw += w[j];
dfs(0, rw, x, 1); // i从1开始
}