前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【动态规划/背包问题】详解「完全背包」问题 & 三种背包问题之间的内在关系

【动态规划/背包问题】详解「完全背包」问题 & 三种背包问题之间的内在关系

作者头像
宫水三叶的刷题日记
发布2021-06-23 10:48:11
1.1K0
发布2021-06-23 10:48:11
举报

前言

今天是我们讲解「动态规划专题」中的 「背包问题」的第八篇

今天我们将学习第三种背包问题:多重背包。

另外,我在文章结尾处列举了我所整理的关于背包问题的相关题目。

背包问题我会按照编排好的顺序进行讲解(每隔几天更新一篇,确保大家消化)。

你也先可以尝试做做,也欢迎你向我留言补充,你觉得与背包相关的 DP 类型题目 ~

题目描述

N

种物品和一个容量为

C

的背包,每种物品「数量有限」

i

件物品的体积是

v[i]

,价值是

w[i]

,数量为

s[i]

问选择哪些物品,每件物品选择多少件,可使得总价值最大。

其实就是在 0-1 背包问题的基础上,增加了每件物品可以选择「有限次数」的特点(在容量允许的情况下)。

示例 1:

代码语言:javascript
复制
输入: N = 2, C = 5, v = [1,2], w = [1,2], s = [2,1]   

输出: 4

解释: 选两件物品 1,再选一件物品 2,可使价值最大。

朴素二维

在之前的章节我们说过,几乎所有的「背包问题」都是基于「01 背包」演变而来。

因此,当我们确定一个问题是背包问题之后,可以根据其物品的「数量限制」来判别是何种背包问题,然后套用「01 背包」的思路来求解。

具体的,我们可以套用「01 背包」的「状态定义」来进行分析:

dp[i][j]

代表考虑前

i

件物品,且所选物品总体积不超过

j

时获得的最大价值。

由于每件物品可以被选择「有限次」,因此对于某个

dp[i][j]

而言,其值应该为以下所有可能方案中的最大值:

  • 选择
0

件物品

i

的最大价值,即

dp[i-1][j]
  • 选择
1

件物品

i

的最大价值,即

dp[i-1][j-v[i]]+w[i]
  • 选择
2

件物品

i

的最大价值,即

dp[i-1][j-2*v[i]]+2*w[i]

...

  • 选择
s[i]

件物品

i

的最大价值,

dp[i-1][j-s[i]*v[i]]+s[i]*w[i]

由此我们可以得出「状态转移方程」为:

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - k * v[i]] + k * w[i]), 0 < k \leqslant s[i], 0 < k * v[i] \leqslant j

可以发现其状态转移方程与 完全背包 完全一致,只是多了

0 < k \leqslant s[i]

的条件。

也好理解,毕竟「完全背包」不限制物品数量,「多重背包」限制物品数量。

代码:

代码语言:javascript
复制
class Solution {
    public int maxValue(int N, int C, int[] s, int[] v, int[] w) {
        int[][] dp = new int[N][C + 1];
        
        // 先处理第一件物品
        for (int j = 0; j <= C; j++) {
            // 显然当只有一件物品的时候,在容量允许的情况下,能选多少件就选多少件(不超过限制数量)
            int maxK = Math.min(j / v[0], s[0]);
            dp[0][j] = maxK * w[0];
        }
        
        // 处理剩余物品
        for (int i = 1; i < N; i++) {
            for (int j = 0; j <= C; j++) {
                // 不考虑第 i 件物品的情况
                int n = dp[i- 1][j];
                // 考虑第 i 件物品的情况
                int y = 0;
                for (int k = 1; k <= s[i]; k++) {
                    if (j < k * v[i]) {
                        break;
                    }
                    y = Math.max(y, dp[i - 1][j - k * v[i]] + k * w[i]);
                }
                dp[i][j] = Math.max(n, y);
            }
        }
        
        return dp[N - 1][C];
    }
}
  • 时间复杂度:共有三层循环的运算量。由于每种情况都需要被考虑到,所以各维度是累乘关系,即
N * C * S

,其中

N * S = \sum_{i = 0}^{N - 1}s[i]

。整体复杂度为

O(\sum_{i = 0}^{N - 1}s[i] * C)
  • 空间复杂度:
O(N * C)

滚动数组

通过观察我们的「状态转移方程」可以发现,我们在更新某个

dp[i][x]

的时候只依赖于

dp[i-1][y]

因此我们可以像 01 背包那样使用「滚动数组」的方式将空间优化到

O(C)

代码:

代码语言:javascript
复制
class Solution {
    public int maxValue(int N, int C, int[] s, int[] v, int[] w) {
        int[][] dp = new int[2][C + 1];
        
        // 先处理第一件物品
        for (int j = 0; j <= C; j++) {
            // 显然当只有一件物品的时候,在容量允许的情况下,能选多少件就选多少件(不超过限制数量)
            int maxK = Math.min(j / v[0], s[0]);
            dp[0][j] = maxK * w[0];
        }
        
        // 处理剩余物品
        for (int i = 1; i < N; i++) {
            for (int j = 0; j <= C; j++) {
                // 不考虑第 i 件物品的情况
                int n = dp[(i- 1)&1][j];
                // 考虑第 i 件物品的情况
                int y = 0;
                for (int k = 1; k <= s[i]; k++) {
                    if (j < k * v[i]) {
                        break;
                    }
                    y = Math.max(y, dp[(i - 1)&1][j - k * v[i]] + k * w[i]);
                }
                dp[i&1][j] = Math.max(n, y);
            }
        }
        
        return dp[(N - 1)&1][C];
    }
}
  • 时间复杂度:共有三层循环的运算量。由于每种情况都需要被考虑到,所以各维度是累乘关系,即
N * C * S

,其中

N * S = \sum_{i = 0}^{N - 1}s[i]

。整体复杂度为

O(\sum_{i = 0}^{N - 1}s[i] * C)
  • 空间复杂度:
O(C)

一维空间优化

在之前的「01 背包」和「完全背包」都可以进行「一维空间优化」。

其中「完全背包」的「一维空间优化」还能有效降低时间复杂度。

那么「多重背包」是否也能通过「一维空间优化」来降低时间复杂度呢?

答案是可以优化空间,但是不能降低时间复杂度。

因为当我们像「完全背包」那样只保留「容量维度」,并且「从小到大」遍历容量的话,我们在转移

f[j]

时是无法直接知道所依赖的

f[j - v[i]]

到底使用了多少件物品

i

的。

这个问题在「完全背包」里面无须关心,因为每件物品可以被选择无限次,而在「多重背包」则是不能忽略,否则可能会违背物品件数有限的条件。

因此,「多重背包」问题的「一维空间优化」并不能像「完全背包」那样使复杂度降低。

代码:

代码语言:javascript
复制
class Solution {
    public int maxValue(int N, int C, int[] s, int[] v, int[] w) {
        int[] dp = new int[C + 1];
        for (int i = 0; i < N; i++) {
            for (int j = C; j >= v[i]; j--) {
                for (int k = 0; k <= s[i] && j >= k * v[i]; k++) {
                    dp[j] = Math.max(dp[j], dp[j - k * v[i]] + k * w[i]);
                }
            }
        }
        return dp[C];
    }
}
  • 时间复杂度:共有三层循环的运算量。由于每种情况都需要被考虑到,所以各维度是累乘关系,即
N * C * S

,其中

N * S = \sum_{i = 0}^{N - 1}s[i]

。整体复杂度为

O(\sum_{i = 0}^{N - 1}s[i] * C)
  • 空间复杂度:
O(C)

与其他背包的内在关系

至此,三类传统背包问题的「一维空间优化」方式都已经讲过了。

我们发现,只有「完全背包」的「一维空间优化」是存在数学意义上的优化(能够有效降低算法时间复杂度)。

「01 背包」和「多重背包」的「一维空间优化」其实只是基于「朴素二维」解法做单纯的「滚动」操作而已(利用状态之间的依赖关系,配合遍历顺序,使得不再需要参与转移的空间能够被重新利用)。

因此,一定程度上,可以将「多重背包」看做是一种特殊的「01 背包」。

对「01 背包」中具有相同的价值 & 成本的物品进行计数,就成了对应物品的「限制件数」,「01 背包」也就转换成了「多重背包」。

同理,将「多重背包」的多件物品进行「扁平化展开」,就转换成了「01 背包」。

转换为 01 背包

扁平化需要遍历所有物品,枚举每件物品的数量,将其添加到一个新的物品列表里。

再套用「01 背包」的解决方案。

代码:

代码语言:javascript
复制
class Solution {
    public int maxValue(int N, int C, int[] s, int[] v, int[] w) {
        // 将多件数量的同一物品进行「扁平化」展开,以 [v, w] 形式存储
        List<int[]> arr = new ArrayList<>(); 
        for (int i = 0; i < N; i++) {
            int cnt = s[i];
            while (cnt-- > 0) {
                arr.add(new int[]{v[i], w[i]});
            }
        }
        
        // 使用「01 背包」进行求解
        int[] dp = new int[C + 1];
        for (int i = 0; i < arr.size(); i++) {
            int vi = arr.get(i)[0], wi = arr.get(i)[1];
            for (int j = C; j >= vi; j--) {
                dp[j] = Math.max(dp[j], dp[j - vi] + wi);   
            }
        }
        return dp[C];
    }
}
  • 时间复杂度:扁平化的计算量取决于最终能展开成多少个物品,复杂度为
O(\sum_{i = 0}^{N - 1}s[i])

。共有

\sum_{i = 0}^{N - 1}s[i] * C

个状态需要被转移,复杂度为

O(\sum_{i = 0}^{N - 1}s[i] * C)

,因此整体复杂度为

O(\sum_{i = 0}^{N - 1}s[i] * C)
  • 空间复杂度:
O(\sum_{i = 0}^{N - 1}s[i] + C)

可以发现,转换成「01 背包」之后,时间复杂度并没有发生变化。

因此将「多重背包」简单的转换成「01 背包」并不能带来效率的提升。

甚至说转换成「01 背包」之后效率上还要稍微差一点,因为额外增大了“常数”。

我们来可以分析一下为什么。

首先,扁平化操作并没有使得物品“变少”,我们仍然需要枚举所有的“组合”,从中选择最优,组合的数量没有发生变化,还额外增加了扁平化的操作。

这是将「多重背包」转换成「01 背包」进行求解没有“实际意义”的原因。

直接的转换并不能带来效率上的提升,但是可以让我们更加了解两者之间的关系。

总结

今天我们学习了【动态规划/背包问题】中的「多重背包」问题。

无论是「朴素二维」、「滚动数组」、「一维优化」还是「扁平化」都不能优化「多重背包」问题的时间复杂度。

在各维度数量级同阶的情况下,时间复杂度是

O(n^3)

的。

这意味着我们只能解决

10^2

数量级的「多重背包」问题。

同时,我们能总结出:在传统的三种背包问题的「一维空间优化」里,只有「完全背包」的「容量维度」是「从小到大」的,其他两种背包的「容量维度」都是「从大到小」的。

下一节,我们将会讲解如何降低「多重背包」问题的时间复杂度。

背包问题(目录)

  1. 01背包 : 背包问题 第一讲
    1. 【练习】01背包 : 背包问题 第二讲
    2. 【学习&练习】01背包 : 背包问题 第三讲
  2. 完全背包 : 背包问题 第四讲
    1. 【练习】完全背包 : 背包问题 第五讲
    2. 【练习】完全背包 : 背包问题 第六讲
    3. 【练习】完全背包 : 背包问题 第七讲
  3. 多重背包 : 本篇
    1. 【练习】多重背包
  4. 多重背包(优化篇)
    1. 【练习】多重背包(优化篇)
    2. 【练习】多重背包(优化篇)
  5. 混合背包
    1. 【练习】混合背包
  6. 分组背包
    1. 【练习】分组背包
  7. 多维背包
    1. 【练习】多维背包
  8. 树形背包
    1. 【练习篇】树形背包
  9. 背包求方案数
    1. 【练习】背包求方案数
  10. 背包求具体方案
    1. 【练习】背包求具体方案
  11. 泛化背包
    1. 【练习】泛化背包

最后

这是我们「刷穿 LeetCode」系列文章的第 No.* 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 宫水三叶的刷题日记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 题目描述
  • 朴素二维
  • 滚动数组
  • 一维空间优化
  • 与其他背包的内在关系
  • 转换为 01 背包
  • 总结
  • 背包问题(目录)
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档