c++ 多重背包状态转移方程_背包问题总结(下)

这篇笔记深入探讨了动态规划在背包问题中的应用,包括二维费用背包、分组背包和有依赖的背包问题。介绍了如何通过最优子结构、边界条件和状态转移方程来解决这些问题,并给出了具体的代码实现。同时,提到了如何处理特殊情况如物品的互斥性和依赖关系,以及在要求方案总数时的状态转移方程调整。
摘要由CSDN通过智能技术生成

976a2d85711c85138ff27cb86e8b39ec.png

这篇笔记写于2019-04-01,那时还为无法在最后的春招阶段进入游戏行业而焦头烂额。

同样,首先记住解决动态规划的三个基本要素:

  1. 最优子结构
  2. 边界条件
  3. 状态转移方程

现在开始介绍较为复杂的背包问题,这一类问题可能在笔试面试中出现,一定要烂熟于心。

二维费用的背包问题

二维费用是指除了物品的重量之外,还需要考虑另一种可消耗因素w2[i](比如体积),即需总重量不超过C1且总体积不可超过C2,这时我们的费用增加了一维,那状态转移方程也要相应的增加一维:

f[i][j][k] = max( f[i - 1][j][k], f[i - 1][j - w1[i]][k - w2[i]] + v[i])

然后我们再用前面的思路对它进行优化,将三维降为二维:

vector<vector<int> > f(C1 + 1, vector<int>(C2 + 1));
for(int i = 0; i < n; i++){
    for(int j = C1; j >= w1[i]; j--)
        for(int k = C2; k >= w2[i]; k--)
            f[j][k] = max(f[j][k], f[j - w1[i]][k - w2[i]] + v[i]);
}

这里假设是简单的0-1背包问题,如果是完全背包问题或多重背包问题,只需要按照之前的方法(从左至右、拆分系数)带入就可以了,最后f[C1][C2]便是答案。

补充一点,有时第二维的要求会给的比较隐晦,比如所给的物品只有重量,但是总共只能拿 N 件,那就可以理解为每个物品的另一个费用都是1,然后和不能超过 N。

分组(互斥)的背包问题

在给定的物品中,除了价值v[i]和重量w[i]外,还将它们分为 k 组,每组中的物品相互冲突,最多只能选一件,此时应该如何选择才能在不超过背包总容量的情况下得到最大价值呢?

我们先改写新的转移状态方程:

f[k][j] = max{ f[k - 1][j], f[k - 1][j - w[i]] + v[i] | i ∈ group k}

其中f[k][j]表示前 k 组物品在 j 的背包容量下所能取得的最大价值,那么此时有选第 k 组中的物品和不选两种情况,如果不选则完全根据前第 k - 1 组物品决定,如果选,那么需要对第 k 组中的所有物品进行遍历:

vector<vector<int> > items; // 储存物品的序号,横坐标为组号,纵坐标为组内元素的下标
...
for(int k = 0; k < items.size(); k++)
    for(int j = C; j >= 0; j--)
        for(int i = 0; i < items[k].size(); i++)
            int index = items[k][i];
            if(j >= w[index])
                f[j] = max(f[j], f[j - w[index]] + v[index]);

注意items储存的是第 k 组中的所有元素的下标,提取的时候要分两步,先输入组号,在依次提取下标。

例题请看另一篇友塔游戏笔试笔记第三题。

有依赖的背包问题

如果规定选第 i 件物品必须取第 j 件物品,就说背包中的物品有依赖关系。这里我们假设被依赖的物品不会再依赖其他物品,就是说不会出现多层依赖。

这个问题比前面的都要复杂,需要仔细分析。我们先把不依赖与别的物品的物品称为主件,把依赖其他物品的物品称为附件,由于一个附件只会依赖一个主件,因此问题可以看做是由若干主件和依赖每个主件的附件集合所组成。那么此时就得到了若干子问题,即给定一个主件和一个附件集合,应该如何从中选取,设集合中有 n 个附件,由于附件集合中每个附件都有选和不选两种选择,再加上只选主件不选一件附件的情况,一共有 2^n + 1 种情况要考虑。

进一步考虑,这 2^n + 1 种情况时互斥的,只能选其中一个,相当于前面的分组背包问题,这是组内的物品费用和价值变成了每种选择主件+若干附件的费用和价值的和,但是现在仍然受制于指数级的选择,但是,附件集合内的每个附件都是要么选要么不选,是不是听起来挺耳熟?没错,这就是0-1背包问题的条件,我们何不用0-1背包问题的思路去对每个附件集合求规定容量的最优解?

由于附件集合对应的主件必选,我们需要考虑的容量范围是 [0, C - w[k]] ,其中w[k]是第 k 件主件的费用,由此可以得到一个大小为C - w[k] + 1数组,其中f[C - w[k]]表示容量为 C - w[k] 时这一组的最大价值,因此费用为 j 的价值为f[j - w[k]] + v[k],其中 j 的范围为 [w[k], C] ,那么此时指数级的问题就降到了 C - w[k] + 1 ,每个主件分成的组都会有一个自己的数组f[k][j - w[k]] + v[k],在遍历每个组时只要保证查过 [w[k], C] 中的所有元素就可以了,然后再将 k 组带入之前的分组的背包问题中就可以得出最优解。

例题

题目描述:有两排宝箱,第一排长 n ,第二排长 m ,每个宝箱装有一定数量的财宝,开宝箱有两条规则

  1. 宝箱必须间隔开,比如开了某排的第 i 个宝箱,那么就不能开第 i + 1 和第 i - 1 个
  2. 第一排宝箱和第二排宝箱有一个交点,给定 a 和 b ,如果开了第一排的第 a 个宝箱,那么必须开第二排的第 b 个宝箱

问如何选取宝箱才能获得最多的财宝?

这一题是间隔房子拿物品题目的扩展,我们先看一下这题的简单版本,即只有一排宝箱,只能间隔拿,如何拿才能得到最大价值的财宝。

这题我们可以很容易的写出状态转移方程:f[i] = max( f[i - 1], f[i - 2] + v[i] ),第 i 件物品无非拿与不拿,而如果第 i - 1 件物品被拿了第 i 件就一定不能拿,此时f[i] = f[i - 1],而如果拿则说明f[i] = f[i - 2] + v[i]。然后就是初始化,很明显f[0] = v[0]f[1] = max(v[0], v[1]),有了这些条件就可以写代码了:

int rob(vector<int>& nums) {
    if(nums.size() == 0)
        return 0;
    if(nums.size() == 1)
        return nums[0];
    vector<int> f(nums.size());
    f[0] = nums[0];
    f[1] = max(nums[0], nums[1]);
    if(nums.size() == 2)
        return f[1];
    for(int i = 2; i < nums.size(); i++){
        f[i] = max(f[i - 1], f[i - 2] + nums[i]);
    }
    return f[nums.size() - 1];
}

这里需要注意的几种边界条件的检测。

而如果第一排宝箱和第二排有依赖(待更新)

求方案总数

若题目要求将背包装满或达到一定容量,还可能追问可行方案的总数,对这一类问题一般只需要将之前的状态转移方程中的minmax改成sum就可以了:

f[i][j] = f[i - 1][j] + f[i][j - coins[i]]

注意这里右边第二项中还是 i,因为硬币数量不做限制,当用前 i 个硬币和大小为 j 的数额时,总方案数量应该是不用第 i 个硬币的方案数量加上用了一个第 i 个硬币的方案数量,这是我们只需要将其优化成一维,然后在第二层循环从左往右填表:

int change(int amount, vector<int>& coins) {
    vector<int> f(amount + 1);
    f[0] = 1;
    for(int i = 0; i < coins.size(); i++){
        for(int j = coins[i]; j <= amount; j++){
            f[j] = f[j] + f[j - coins[i]];
        }
    }
    return f[amount];
}

这里我们用第二种找钱问题来举例(LeetCode 518),题目要求输出所有找钱组合的总数,要注意记得初始化f[0] = 1,因为数额为0时不给任何找钱即是一种方案,否则输出永远是0。

求最优方案总数

上面只是单纯的求方案总数,其中有很多方案并不是最优,我们如果要求出最优方案总数,还需要一个辅助数组g[i][j]。以0-1背包问题为例:

g[0][0] = 1;
for(int i = 1; i <=n; i++){
    for(int j = w[i]; j <= C; j++){
        f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[i]);
        g[i][j] = 0;
        if(f[i][j] == f[i - 1][j])
            g[i][j] += g[i - 1][j];
        if(f[i][j] == f[i - 1][j - w[i]] + v[i])
            g[i][j] += g[i - 1][j - w[i]];
    }
}

最后g[n][C]就是最优解的总数。

至此,对于背包问题的总结就告一段落,还有一些更复杂的背包问题,恕鄙人能力有限,不过多展开了,如果总结的地方有什么不对,或者代码有bug,欢迎指出。

再次感谢《背包问题九讲》中的详细讲解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值