背包问题
参考:代码随想录
¶一、01背包
¶楔子
有 N 件物品和一个最多能被重量为 W 的背包。第 i 件物品的重量是 weight[i],得到的价值 value[i]。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
一个例子如下:(背包最大容量 4 )
重量 | 价值 | |
---|---|---|
物品 0 | 1 | 15 |
物品 1 | 3 | 20 |
物品 2 | 4 | 30 |
¶🔹 二维dp数组01背包
使用二维数组,即 dp[i][j]
表示从下标为 [0 - i] 的物品里任意取,放进容量为 j 的背包, 价值总和最大 是多少。
递推公式 :(有两个方向推出来 dp[i][j]
)
由
dp[i - 1][j]
推出,即背包容量为 j ,里面不放物品 i 的最大价值,此时dp[i][j]
为dp[i - 1][j]
由
dp[i - 1][j - weight[i]]
推出,dp[i - 1][j - weight[i]]
为背包容量 j - weight[i] 的时候不放物品 i 的最大价值,那么dp[i - 1][j - weight[i]] + value[i]
,就是背包放物品 i 得到的最大价值。
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
初始化 :
- 如果背包容量 j = 0 ,即
dp[i][0]
,无论是选取哪些物品,背包价值总和一定为 0。 dp[0][j]
为存放编号 0 的物品的时候,各个容量的背包能存放的最大价值。
1 | vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1, 0)); |
遍历顺序 :(两个遍历的维度:物品与背包重量)
先遍历物品,再遍历背包重量,还是先遍历背包重量,再遍历物品都是可以的。(没有覆盖问题,所以都可以)
- 先遍历物品,再遍历背包重量
1 | void test() { |
- 先遍历背包重量,再遍历物品
1 | void test() { |
¶🔸 一维dp数组01背包(滚动数组)
在一维dp数组中, dp[j]
表示:容量为 j 的背包,所背的物品价值可以最大为 dp[j]
。
递推公式 :(有两个方向推出来 d[j]
)
dp[j] 有两个选择,一个是取自己 dp[j] ,一个是取 dp[j - weight[i] + value[i]]
,指定是取最大的,毕竟是求最大价值,所以递归公式:
1 | dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); |
初始化:
dp[j]
表示容量为 j 的背包,所背的物品价值可以最大为 dp[j]
,那么 dp[0] = 0
,因此背包容量为 0 所背的物品的最大价值就是 0;当 j != 0
时候,为了让 max 函数中, dp[j]
被覆盖,需要让 dp[j]
取最小值即可,如果 dp[j] >= 0
,所以 j != 0
时, dp[j] = 0
;
遍历顺序 :(两个遍历的维度:物品与背包重量)
🙋 必须先遍历物品,后遍历背包重量,且背包重量一定是从大到小
1 | for(int i = 0; i < weight.size(); i++) { // 遍历物品 |
倒序遍历是为了保证物品 i 只被放入一次!但如果一旦正序遍历,那么 物品 0 就会被重复加入多次。
也正是因为倒序遍历,如果背包重量放在上一层,那么每个 dp[j]
就只会放入一个物品,即:背包里只放入了一个物品。
测试代码 ⭐️
1 | void test_1_wei_bag_problem() { |
¶416. 分割等和子集
Difficulty: 中等
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
1 | 输入:nums = [1,5,11,5] |
示例 2:
1 | 输入:nums = [1,2,3,5] |
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
Solution
1 | // 将 nums 等分,即按照 sum(nums)/2 为背包容量,nums[i]为 weight[i] 和 values[i] |
时间复杂度 : $O(n^2)$
空间复杂度 : $O(n)$
¶1049. 最后一块石头的重量 II
Difficulty: 中等
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
。
示例 1:
1 | 输入:stones = [2,7,4,1,8,1] |
示例 2:
1 | 输入:stones = [31,26,33,21,40] |
示例 3:
1 | 输入:stones = [1,2] |
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 100
Solution
1 | // 尽量将石头堆分为尽量相同重量的两堆 A B (A >= B) |
- 时间复杂度:$O(m * n)$,m 是石头总重量(准确的说是总总量的一半),n 为石头块数
- 空间复杂度:$O(m)$
¶494. 目标和
Difficulty: 中等
给你一个整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
示例 1:
1 | 输入:nums = [1,1,1,1,1], target = 3 |
示例 2:
1 | 输入:nums = [1], target = 1 |
提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
Solution
1 | class Solution { |
- 时间复杂度:$O(m * n)$,n 是正数个数,m 为背包容量
- 空间复杂度:$O(m)$
¶474. 一和零
Difficulty: 中等
请你找出并返回 strs
的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。 如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。
请你找出并返回 strs
的最大子集的大小,该子集中 最多 有 m
个 0
和 n
个 1
。
如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集 。
示例 1:
1 | 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 |
示例 2:
1 | 输入:strs = ["10", "0", "1"], m = 1, n = 1 |
提示:
1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i]
仅由'0'
和'1'
组成1 <= m, n <= 100
Solution
1 | // 01 背包问题 |
时间复杂度: $O(l* m * n)$,其中 $l$ 为 strs 长度
空间复杂度: $O(m * n)$
¶二、完全背包
¶楔子
有 N 件物品和一个最多能背重量为 W 的背包。第i件物品的重量是 weight[i] ,得到的价值是 value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
一个例子如下:(背包最大容量 4 )
重量 | 价值 | |
---|---|---|
物品 0 | 1 | 15 |
物品 1 | 3 | 20 |
物品 2 | 4 | 30 |
1 | for(int i = 0; i < weight.size(); i++) { // 遍历物品 |
01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:
1 | // 先遍历物品,再遍历背包 |
注意 🙋♂ :在完全背包中,对于一维dp数组来说,for 循环嵌套顺序无所谓。
1 | // 先遍历背包,再遍历物品 |
测试代码如下 ⭐️
1 | // 先遍历物品,在遍历背包 |
更需要注意 🙋
对于纯完全背包问题,其 for 循环的先后循环是可以颠倒的!但如果题目稍稍有点变化,就会体现在遍历顺序上。
¶518. 零钱兑换 Ⅱ
Difficulty: 中等
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
1 | 输入:amount = 5, coins = [1, 2, 5] |
示例 2:
1 | 输入:amount = 3, coins = [2] |
示例 3:
1 | 输入:amount = 10, coins = [10] |
提示:
1 <= coins.length <= 300
1 <= coins[i] <= 5000
coins
中的所有值 互不相同0 <= amount <= 5000
Solution
1 | // 注意此时的遍历顺序,先遍历物品,后遍历背包重量 |
时间复杂度: $O(m * n)$,其中 m 为 硬币规格数, n 为 总金额
空间复杂度: $O(n)$
¶377. 组合总和 Ⅳ
Difficulty: 中等
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
1 | 输入:nums = [1,2,3], target = 4 |
示例 2:
1 | 输入:nums = [9], target = 3 |
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 1000
nums
中的所有元素 互不相同1 <= target <= 1000
**进阶:**如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件?
Solution
1 | // 注意此时的遍历顺序,先遍历背包重量,后遍历物品 |
时间复杂度: $O(m * n)$,其中 m 为 nums 长度, n 为目标整数
空间复杂度: $O(n)$
¶70. 爬楼梯
Difficulty: 简单
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
**注意:**给定 n 是一个正整数。
示例 1:
1 | 输入: 2 |
示例 2:
1 | 输入: 3 |
Solution
1 | // 完全背包问题,应该按照排列来求结果,因为 1 2 和 2 1 是两种可能 |
时间复杂度: $O(m * n)$,其中 m 为每次可以跳跃的最大阶数,此题 m = 2, n 为目标阶数
空间复杂度: $O(n)$
¶322. 零钱兑换
Difficulty: 中等
给定不同面额的硬币 coins
和一个总金额 amount
。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
示例 1:
1 | 输入:coins = [1, 2, 5], amount = 11 |
示例 2:
1 | 输入:coins = [2], amount = 3 |
示例 3:
1 | 输入:coins = [1], amount = 0 |
示例 4:
1 | 输入:coins = [1], amount = 1 |
示例 5:
1 | 输入:coins = [1], amount = 2 |
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 2<sup>31</sup> - 1
0 <= amount <= 10<sup>4</sup>
Solution
1 | // 完全背包问题 |
时间复杂度: $O(m * n)$,其中 m 为 coins
长度, n 为目标总金额
空间复杂度: $O(n)$
¶279. 完全平方数
Difficulty: 中等
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...
)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n
,返回和为 n
的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 1:
1 | 输入:n = 12 |
示例 2:
1 | 输入:n = 13 |
提示:
1 <= n <= 10<sup>4</sup>
Solution
1 | // 与零钱兑换基本相同 |
时间复杂度: $O(n * \sqrt{n})$,其中 n 为给定的正整数。
空间复杂度: $O(n)$
¶139. 单词拆分
Difficulty: 中等
给定一个 非空 字符串 s 和一个包含 非空 单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例 1:
1 | 输入: s = "leetcode", wordDict = ["leet", "code"] |
示例 2:
1 | 输入: s = "applepenapple", wordDict = ["apple", "pen"] |
示例 3:
1 | 输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] |
Solution
1 | // 完全背包问题: |
时间复杂度:$O(n^3)$,因为 substr 的时间复杂度是 $O(n)$ (n 为 substring 的长度)
空间复杂度:$O(n)$
¶三、多重背包
¶楔子
有 N 种物品和一个容量为 V 的背包。第i种物品最多有 Mi 件可用,每件耗费的空间是 Ci ,价值是 Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。多重背包和 01背包非常像,只需要将 Mi 摊开,其实就是一个 01背包问题。
一个例子如下:(背包最大容量 10 )
重量 | 价值 | 数量 | |
---|---|---|---|
物品 0 | 1 | 15 | 2 |
物品 1 | 3 | 20 | 3 |
物品 2 | 4 | 30 | 2 |
摊开后:
重量 | 价值 | 数量 | |
---|---|---|---|
物品 0 | 1 | 15 | 1 |
物品 0 | 1 | 15 | 1 |
物品 1 | 3 | 20 | 1 |
物品 1 | 3 | 20 | 1 |
物品 1 | 3 | 20 | 1 |
物品 2 | 4 | 30 | 1 |
物品 2 | 4 | 30 | 1 |
如此就转化为 01 背包问题,且每个物品只用一次。
测试程序如下 ⭐️
1 |
|