1. 装箱问题1
1.1. 问题描述
在装箱问题中,箱子的数量不限,每个箱子的容量为 binCapacity,待装箱的物品有 n 个,物品 i 需要占用的箱子容量为 $objectSize[i],0{\leq}objectSize[i]{\leq}binCapacity$。所谓 可行装载(feasible packing),是指所有物品都装入箱子而不溢出。所谓 最优装载(optimal packing) 是指使用箱子最少的可行装载。
1.2. 问题求解
近似算法:箱子装载问题和机器调度问题一样,是 NP-复杂问题,因此常用近似算法求解。求解所得的箱子树不是最少,但接近最少,有5中流行的近似算法:
- 最先适配法(First Fit, FF):物品按 $1,2,3…,n$ 的顺序装箱。假设箱子从左到右排列,每一物品 i 放入可装载它的最左面的箱子。
- 最优适配法(Best Fit, BF):令 $bin[j].unusedCapacity$ 为箱子 j 的可用容量。初始时,所有箱子的可用容量为 $binCapacity$。物品 i 放入可用容量 $unusedCapacity$ 最小但不小于 $object[i]$ 的箱子(此方法更能充分的利用空间)。
- 最先适配递减法(First Fit Decreasing, FFD):此方法与 FF 类似,区别在于,所有物品首先按所需容量的递减次序排列,即对于 $1{\leq}i<n$,有 $objectSize[i]{\geq}objectSize[i+1]$。
- 最优适配递减法(Best Fit Decreasing,BFD):此方法与 BF 类似,区别在于,所有物品首先按所需容量的递减的次序排列,即对于 $1{\leq}i<n$,有 $objectSize[i]{\geq}objectSize[i+1]$。
- 相邻适配法:为装载一件物品,首先在非空的箱子中循环搜索能够装载该物品的箱子,如果找不到这样的箱子,就启用一个空箱子。
定理:设 $I$ 为装在问题的任一实例,$b(I)$ 为最优装载所用的箱子数,FF 和 BF 所用的箱子数不会超过 $(17/10)b(I)+2$,而 FFD 和 BFD 所用的箱子数不会超过 $(11/9)b(I)+4$。
实例:假设有 4 件物品,所需容量分别为 $objectSize[1:4]=[3, 5, 2, 4]$,把他们放入容量均为 7 的箱子。
- 使用 FF 法:共用了三个箱子:物品 1 和 3 放入箱子 1;物品 2 放入箱子 2;物品 4 放入箱子 3.
- 使用 BF 法:只用了两个箱子:物品 1 和 4 放入箱子 1;物品 2 和 3 放入箱子 2.
- 如果使用 FFD 和 BFD 方法:物品按 2,4,1,3 排序,最后结果一样;物品 2 和 3 放入箱子 1,物品 1 和 4 放入箱子 2.
用最先适配法求解装箱问题:可用 赢者树 实现。具体参考《数据结构、算法与应用 C++语言描述》(第二版)书籍的第 13 章(竞赛树)第 13.5 节。
另一种用 二叉搜索树 实现的参考该书的第 14 章(搜索树)第 14.6.2 节。
2. 装箱问题2
2.1. 问题描述
有两艘船,$n$个货箱。第一艘船的载重重量是$c_1$,第二艘船的载重重量是$c_2$。$w_i$是货箱$i$的重量且${\sum}_{i=1}^{n}{w_i}{\leq}{c_1+c_2}$。希望确定是否有一种方法可以把$n$个货箱全部装上船。如果有,找出这种方法。
扩展:当${\sum}_{i=1}^{n}{w_i}={c_1+c_2}$时,两艘船的装载问题等价于 子集之和(sum-of-subset) 问题,即有$n$个数字,要求找到一个子集(如果存在的话),使它的和为$c_1$。当$c_1=c_2$且${\sum}_{i=1}^{n}{w_i}={2c_1}$时,两艘船的装载问题等价于 分割问题(partition problem),即有$n$个数字$a_i(1{\leq}i{\leq}n)$,要求找到一个子集(如果存在的话),使得子集之和为$({\sum}_{i=1}^{n}{a_i}){/2}$。
2.2. 问题分析
只要有一种可行的方案,就可以验证以下的装船策略行之有效:
- 尽可能将第一艘船装载到它的装载极限;
- 将剩余货箱装到第二艘船。
为了尽可能地将第一艘船装满,需要选择一个货箱的子集,它们的总重量尽可能接近于$c_1$,这个选择可通过0/1背包问题解决:
$${\max}{\sum}_{i=1}^{n}{w_i x_i}$$
限制条件是
$${\sum}_{i=1}^{n}{w_i x_i}{\leq}{c_i}{\;\;\;}{x_i}{\in}{0,1},1{\leq}i{\leq}n$$
可以使用动态规划法确定第一艘船的最佳装载。这里使用回溯法设计一个复杂度为$O(2^n)$的算法。
构建解空间树表示,以3个货箱的装箱问题为例,树形表示如下:1
2
3
4
5
6
7 A
/ \
B C
/ \ / \
D E F G
/ \ / \ / \ / \
H I J K L M N O
每个非叶子节点左子树表示1,右子树表示0。从$A$到$I$表示一种装箱策略,为$(1,1,0)$,即将第一和第二两个货箱装入第一艘船中。采用深度优先遍历该树即可得到货物装箱的所有可行解以及最优解。
可以看到,有些节点右子树不可能包含比当前最优解更好的解,则不需要移动到这种右子树中去。令$Z$为解空间树的第$i$层的一个节点。以$Z$为根的子树中没有叶节点的重量超过 weightOfCurrentLoading + remainingWeight(这两个变量的含义见代码1.3.1. 装载问题的回溯算法),其中remainingWeight = ${\sum}_{j=i+1}^{n}$weight[j]为剩余货箱的重量($n$是货箱的数量)。因此,当
weightOfCurrentLoading + remainingWeight ${\leq}$ maxWeightSoFar
没有必要搜索$Z$的右子树
实现代码为1.3.1. 装载问题的回溯算法。
确定第一艘船的最佳装载后,判断剩余货物的总重量是否不大于第二艘船的载重重量$c_2$,如果是,则找到一个可行装载。
2.3. 问题求解
2.3.1. 装载问题的回溯算法
1 | /* |
代码输出结果:1
1 1 0 0 1
2.3.2. 装载问题的迭代算法
1 | /* |
代码输出结果:1
1 1 0 0 1