最优装载问题
问题描述
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且集装箱的重量和<=c1+c2。装载问题要求确定,是否有一个合理的装载方案可将这n个集装箱装上这2艘轮船,如果有,找出一种装载方案。
例如,当n=3,c1=c2=50,且w=[10,40,40]时,可将集装箱1和2装上第1艘轮船,而将集装箱3装上第2艘轮船;如果w=[20,40,40],则无法将这3个集装箱都装上轮船。
当集装箱重量和=c1+c2时,装载问题等价于子集和问题。当c1=c2且重量和=2c1时,装载问题等价于集合划分问题。即使限制wi为整数,c1和c2也是整数。子集和问题与划分问题都是NP难的。由此可知,装载问题也是NP难的。
容易证明,如果一个给定的装载问题有解,则采用下面的策略可以得到最优装载方案:
先将第一艘轮船尽可能装满,然后将剩余的集装箱装上第二艘轮船。将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近c1。
所以现在问题转变为一个特殊的0-1背包问题。
算法设计
用回溯法求解装载问题,用子集树表示其解空间。
可行性约束函数:剪去不满足重量和<=c1的子树
上界函数:剪去不含最优解的子树,此处使用cw+r即当前重量加剩余集装箱的重量,如果≤bestw则剪去
算法Backtrack(i):当i>n时,算法搜索至叶结点,其相应的装载重量为cw。如果cw>bestw,则表示当前解优于当前最优解,更新bestw;当i<=n时,扩展结点是Z中的内部节点。左子树检查可行性,可行则进入;右子树检查是否有最优解。
代码示例
//回溯法常规类
template<class Type>
class Loading{
friend Type MaxLoading(Type [],Type,int);
private:
void Backtrack(int i);
int n;
Type *w, //集装箱数目
c, //限重
cw, //当前重量
bestw, //最优重量
r; //重量上界
};
template<class Type>
void Loading<Type>::Backtrack(int i){
if(i>n){
bestw=cw;
return;
}
//搜索子树
r-=w[i];
if(cw+w[i]<=c){
cw+=w[i];
Backtrack(i+1);
cw-=w[i];
}
if(cw+r>bestw)
Backtrack(i+1);
r+=w[i];
}
测试推荐
洛谷真题:https://www.luogu.com.cn/problem/P1049
AC代码
#include<iostream>
#define maxsize 31
using namespace std;
template<class Type>
class Loading{
friend int main(void);
private:
void Backtrack(int i);
int n;
Type w[maxsize],
c,
cw,
bestw,
r;
};
template<class Type>
void Loading<Type>::Backtrack(int i){
if(i>n){
bestw=cw;
return;
}
r-=w[i];
if(cw+w[i]<=c){
cw+=w[i];
Backtrack(i+1);
cw-=w[i];
}
if(cw+r>bestw)
Backtrack(i+1);
r+=w[i];
}
int main()
{
Loading<int> box;
cin>>box.c;
cin>>box.n;
box.r=0;
for(int i=1;i<=box.n;i++){cin>>box.w[i]; box.r+=box.w[i];}
box.cw=0;
box.bestw=0;
box.Backtrack(1);
cout<<box.c-box.bestw;
}
算法分析
解空间树中共2n个结点,因此时间复杂度为O(2n)