1. 三种背包问题
1.1 0-1背包
有N件物品和一个容量为V的背包,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品可以使得背包中总价值最大。
1.2 完全背包:
有N种物品和一个容量为V的背包,每种物品都有无限件可用,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品可以使得背包中总价值最大。
1.3 多重背包:
有N种物品和一个容量为V的背包,第i种物品最多有Mi件可用,每件物品消耗的容量为Ci,价值为Wi,求解入哪些物品可以使得背包中总价值最大。
三种背包问题都有一个共同的限制,那就是背包容量,背包的容量是有限的,这便限制了物品的选择,而三种背包问题的共同目的,便是让背包中的物品价值最大。
不同的地方在于物品数量的限制,01背包问题中,每种物品只有一个,对于每种物品而言,便只有选和不选两个选择。完全背包问题中,每种物品有无限多个,所以可选的范围要大很多。在多重背包问题中,每种物品都有各自的数量限制。
三种背包问题虽然对于物品数量的限制不一样,但都可以转化为01背包问题来进行思考。在完全背包问题中,虽然每种物品都可以选择无限个,但由于背包容量有限,实际上每种物品可以选择的数量也是有限的,那么将每种物品都看做是 V/Ci 种只有一件的不同物品,不就成了01背包问题吗?对于多重背包也是如此,只是每种物品的膨胀数量变成了 min{Mi, V/Ci}。
所以说,01背包问题是所有背包问题的基础,弄懂了01背包问题后,完全背包和多重背包就没有什么难的地方了。
下面我们来对比一下三种背包问题的状态转移方程,以便更好的理解它们之间的联系:
01背包
F[i,V] = max{F[i-1,V], F[i-1,V-vi] + wi}
完全背包
F[i,V] = max{F[i-1,V-kvi] + kwi | 0 <= kvi <= v}
多重背包
F[i,V] = max{F[i-1,V-kvi] + k*wi | 0 <= k <= Mi}
2. 0-1背包
N代表物品数量,
vi代表第i个物品占用的容量,
V代表背包总容量,
wi代表第i个物品的价值
f [ i , j ] :从前 i 件物品中选择放入容量为 j 的背包最大的价值。
状态转移方程为:
f[i][V]=max{ f[i-1][V],f[i-1][V-v[i]]+w[i] }。
// 朴素
f[i][j]
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
f[i][j] = f[i - 1][j];
if (j >= v[i])
f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
}
}
一维优化——对代码做等价变形
分析:f[j] = max(f[j], f[j-Ci)+Wi)要使右边的f表示的是i-1时的值,只需要逆序遍历j,这样右边的f还没被更新,表示的是循环i-1时的值,因此可以等价变形
//倒序
f[j]
for (int i = 1; i <= N; i++) {
for (int j = V; j >= v[i]; j--) { // 倒序
f[j] = max(f[j], f[j - v[i]] + w[i]);
// 朴素 f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
}
}
def ZeroOneKnapsack(F,V, C,W)
for v = V to C
F[v] = max{F[v],F[v-C] + W}
for i = 1 to N
ZeroOneKnapsack(F,V,vi,wi)
3. 完全背包
N代表物品数量,
vi代表第i个物品占用的容量,
V代表背包总容量,
wi代表第i个物品的价值
f [ i , j ] :从前 i 件物品中选择放入容量为 j 的背包最大的价值。
状态转移方程为:
F[i,V] = max{F[i-1,V-kvi] + kwi | 0 <= k*vi <= v}
def CompleteKnapsack(F,V,C,W)
for v = C to V
F[v] = max{F[v],F[v-C] + W}
for i = 1 to N
CompleteKnapsack(F,V, vi,wi)
常见的背包问题有1、组合问题。2、True、False问题。3、最大最小问题。
以下题目整理来自大神CyC,github地址:
github
我在大神整理的基础上,又做了细分的整理。分为三类。
1、组合问题:
377. 组合总和 Ⅳ
494. 目标和
518. 零钱兑换 II
2、True、False问题:
139. 单词拆分
416. 分割等和子集
3、最大最小问题:
474. 一和零
322. 零钱兑换
组合问题公式. dp[i] += dp[i-num]
True、False问题公式. dp[i] = dp[i] or dp[i-num]
最大最小问题公式dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)
以上三组公式是解决对应问题的核心公式。
当然拿到问题后,需要做到以下几个步骤:
1.分析是否为背包问题。
2.是以上三种背包问题中的哪一种。
3.是0-1背包问题还是完全背包问题。也就是题目给的nums数组中的元素是否可以重复使用。
4.如果是组合问题,是否需要考虑元素之间的顺序。需要考虑顺序有顺序的解法,不需要考虑顺序又有对应的解法。
接下来讲一下背包问题的判定
背包问题具备的特征:给定一个target,target可以是数字也可以是字符串,再给定一个数组nums,nums中装的可能是数字,也可能是字符串,问:能否使用nums中的元素做各种排列组合得到target。
背包问题技巧:
1.如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序;
for num in nums:
for i in range(target, nums-1, -1):
2.如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。
for num in nums:
for i in range(nums, target+1):
3.如果组合问题需考虑元素之间的顺序,需将target放在外循环,将nums放在内循环。
for i in range(1, target+1):
for num in nums:
代码
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
if not nums:
return 0
dp = [0] * (target+1)
dp[0] = 1
for i in range(1,target+1):
for num in nums:
if i >= num:
dp[i] += dp[i-num]
return dp[target]