目录
一、装载问题
1.问题概括:
有一批共n个集装箱要装上2艘载重量分别为c1和c1的轮船,其中集装箱i重量为wi,且
装载问题要求确定是否有一个合理的装载方案可将这一批集装箱装上这2艘轮船。
如果有,找出一种装载方案。
2.解决方案(思路):
- 首先将第一艘轮船尽可能装满。
- 将剩余的集装箱装上第二艘轮船。
将第一艘船尽可能装满,等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近c1。因此,装载问题等价于特殊的0-1背包问题:
由此可知,需要满足一下条件:
其中,
3.图片讲解(超详细):
假设一艘船能装载货物的重量为9,货物有三个,重量w(weight)分别为6,3,1。并且要求第一艘船重量最大(max),不能超重。
讲解:
设定第一艘为c1,
第一次在c1船上放置重量为6的货物,
此时c1已装入6,剩余3,
第二次在继续向c1船上放置重量为3的货物,
此时c1已装入6+3=9,剩余0,
第三次如过在继续向c1船上放置重量为1的货物,
此时c1船已装入6+3+1=10≥9,剩余-1,
说明c1船超载了,也就是说,第三次重量为1的货物应该装载在第二艘船c2,第一艘不装。
由此,我们将放入第一艘船的标为1,不放入第一艘船的标为0,构建二叉树,得到我们第一串存放方式的编码,110
以此类推,得出其他可能,但我们需在代码中设置bestw用于存储最优解,也就是哪一个线路或者说是方案策略,可以使得该线路或者该方案策略得出的c1接近最大值max。
其实该方法可以总结为:
++从一条路往前走,能进则进,不能进则退回来,换一条路再试。++
这种走不通就退回再走的技术为回溯法。
4.代码分析:
cpp
/*
cw:当前重量
bestw:最佳重量
x[]:当前调度
bestx[]:最有调度
c:容量
w[]:货物重量
*/
//搜索第i层节点
void backtrack(int i) {
//到达叶子节点
if (i > n) {
if (cw > bestw)
return;
}
//搜索左子树
if (cw + w[i] <= c) {
cw += w[i];
backtrack(i + 1);
cw -= w[i];
}
//不行就去下一级继续
backtrack(i+1);
}
在函数backtrack中,当到达叶子节点(即i > n)时,会检查当前的总重量cw是否大于已知的最佳重量bestw。如果是,就返回;否则,继续搜索。
在搜索左子树时,如果当前的总重量cw加上下一个物品的重量w[i]不超过背包的容量c,就将该物品加入当前调度,并递归地搜索下一个节点。在搜索右子树时,不将当前物品加入调度,而是直接递归地搜索下一个节点。
这样,每次搜索左子树时,都会尝试将物品i加入调度,然后继续搜索下一个节点。如果在某个节点处,发现无法将物品i加入调度(即cw + w[i] > c),就会转而搜索右子树,即不将物品i加入调度,而是直接递归地搜索下一个节点。这种搜索方式保证了所有可能的调度都被尝试过了,从而可以找到最优解。
二、算法改进:引入上界函数
1.问题概念:
设r是剩余集装箱的重量,即
定义上界函数为cw+r。若cw+r≤bestw,即当前重量+剩余集装箱重量比最佳重量还小,则可将当前结点的右子树减去,即不装的情况不用考虑。从而节省内存消耗以及代码计算时间。
还是上面那个例子:
假设一艘船能装载货物的重量为9,货物有三个,重量w(weight)分别为6,3,1。并且要求第一艘船重量最大(max),不能超重。
2.图片讲解:
3.升级代码:
cpp
/*
cw:当前重量
bestw:最佳重量
x[]:当前调度
bestx[]:最有调度
c:容量
w[]:货物重量
*/
//搜索第i层节点
void Backtrack(int i) {
//如果到达叶子结点
if (i > n) {
if (cw > bestw) {
for (int j = i; 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];
}