装载问题 | 回溯:01选择(最大剪枝)

目录

1、与最优装载问题的对比

2、第一艘船的货物应该如何选择

3、选择树的回溯算法

4、剪枝操作 —— 回溯算法的优化


装载问题:有n个集装箱要装上 2 艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且∑wi <= c1 + c2。

问是否有一个合理的装载方案,可将这n个集装箱装上这2艘轮船。如果有,找出一种装载方案。

题目分析:其实就可以理解为,先装第一艘船,再装第二艘船,是否可以将货物全部装上,并给出解决方案。

主要待考虑的就是如何去装第一艘船?这个问题解决了后,剩下的都放入第二艘船即可。 


1、与最优装载问题的对比

首先我们先来看看另一个相似的问题:

最优装载问题: 有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为Wi。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船

       很明显,最优装载问题可以贪心求解贪心选择:每次选择重量最小的集装箱上船,直到放不下为止。 

       但是,装载问题是不可以像上面一样贪心的! 假如我们让第一艘船尽量装下更多的集装箱而使用上述的贪心选择策略,那么很容易对第一艘船造成空间浪费,从而结果不是最优的。可以看看如下反例:


2、第一艘船的货物应该如何选择

       既然上面已经说明第一艘船是不能贪心的,会造成空间浪费从而导致结果不是最优的!那么第一艘船的货物应该如何选择呢?

       不是应该使得第一艘船装的货物数量越多越好,而是应该考虑在载重范围内,第一艘船装的货物重量越大越好。即应该尽可能地装满第一艘船,剩余的货物全都交给第二艘船

       那么第一艘船的实现过程其实就是一个 背包DP | 01背包问题 :每个货物只有上船或不上船两种选择,在背包大小为 c1 的条件下,选择价值和重量均为 wi 的物品,使得在容量范围内尽可能价值最大!

       除了动态规划,其实还有另外一种方法,就是回溯算法。下面详细讲解!


3、选择树的回溯算法

        先看看暴力枚举解决:对于 n 件物品,我们将 n 位的二进制数全部列举出来,每一位对应次号集装箱是否上船,即包括了所有方式的枚举。在一一枚举的同时记录下最能装满船的选择!(cw 记录在该种选择下的总重量,bestw 记录在装载范围内最大的cw

  【以 n = 3 为例】 枚举的顺序可以选择:

  • 字典序:000,001,010,011,100,101,110,111
  • 逆子典序:与字典序相反
  • 格雷码序:000,001,011,010,110,111,101,100(减小 cw 的计算量)

       暴力枚举算法的缺点很明显:遍历了很多没有必要的选择,但是又不好剪枝。 


        回溯算法可以说是暴力枚举的合理化实现。首先将我们所有的枚举方案画成一棵选择决策树(如下图,树叶部分就是总的决策),每个决策对应树的一条边,树的节点是选择的结果。然后回溯算法本质就是深搜这棵树~

回溯算法的具体过程如下图,其实就是简单的 dfs 啦~ 

  • 初始化:cw = bestW = 0
  • 调用:backstrack(1)

       可以发现,其实代码中是没有体现树这个结构的。但是我们的决策本质就是可以用树来体现,所以整个代码的遍历就是对树的深搜。回溯算法很神奇吧!下面是一个例子,可以理解一下整个的回溯过程。条件:W[16,15,15], c = 30。


此回溯算法的代码实现:

int cw;  //当前重量
int bestW;  //最优重量
int c;   //船的最大承载量
int n;  //货物的数量
int w[100];   //对应 n 个集装箱的重量

/* 尽量装满第一艘船的回溯算法
 * step:层数 */
void backtrack(int step) {
    /* 到达了树叶 */
    if(step > n) {
        if(bestW < cw && cw <= c)
            bestW = cw;
        return;
    }

    cw += w[step];
    backtrack(step + 1);
    cw -= w[step];

    backtrack(step + 1);
}

4、剪枝操作 —— 回溯算法的优化

        上面的回溯算法是没有经过剪枝操作的,其实整个复杂度和暴力枚举是没有区别的。下面我们对回溯算法进行剪枝优化。


剪枝操作1:对于不符合我们最终的约束条件的子树,跳过遍历。【约束条件】


剪枝操作2:对于找不到最优解的子树,跳过遍历。【限界条件】

(下面的伪代码同时加入了 x 数组来记录具体的选择,在更新最优值的同时维护最优解)

通常,限界条件是需要我们自己去构造的,如本例就引入了 r 来记录 。


剪枝操作3:提前更新最优值。

       不再只在叶子节点上更新最优值,在其他每次节点里都进行更新,最后到了叶子节点上直接返回即可。这样可以让限界条件的剪枝范围更广。


有剪枝操作的回溯算法的代码实现:

        coding...

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
装载问题是一道经典的回溯算法问题。假设有一个货车,它的载重量为C,同时有n个货物,每个货物的重量为w1, w2, ..., wn,现在需要将这些货物装载到货车上,问最多可以装载多少重量的货物? 这个问题可以使用回溯算法来解决。回溯算法的基本思路是搜索所有可能的解,直到找到符合条件的最优解。在装载问题中,我们可以将每个货物看作一个节点,每个节点有两种状态,即选中和不选中。我们可以遍历所有的节点,对于每个节点,都尝试选中和不选中两种状态,然后依次递归下去,直到所有节点都被遍历过。如果当前货物被选中了,我们就将当前货物的重量加到当前的载重量中,如果当前的载重量大于货车的载重量,就表示当前方案不可行,需要回溯到上一个状态继续搜索。最终,我们可以得到符合条件的最优解。 下面是装载问题回溯算法实现的伪代码: ```python def load(w, c): n = len(w) best = 0 def backtrack(i, cw): if i == n: nonlocal best if cw > best: best = cw return backtrack(i + 1, cw) if cw + w[i] <= c: backtrack(i + 1, cw + w[i]) backtrack(0, 0) return best ``` 其中,w是一个列表,表示每个货物的重量;c是货车的载重量;best表示符合条件的最大载重量。回溯函数backtrack(i, cw)表示从第i个货物开始搜索,当前载重量为cw。在回溯函数中,首先判断是否到达了最后一个货物,如果是,则更新最优解;否则,依次尝试选中和不选中当前货物,然后递归下去。 使用回溯算法解决装载问题的时间复杂度为O(2^n),因为每个节点有两种状态,总共有n个节点。因此,当n比较大时,回溯算法不是很适合。可以考虑使用其他更高效的算法,如动态规划等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值