本专栏已参加蓄力计划,感谢读者支持❤
往期文章
一. Java入门算法(贪心篇)丨蓄力计划
二. Java入门算法(暴力篇)丨蓄力计划
三. Java入门算法(排序篇)丨蓄力计划
四. Java入门算法(递归篇)丨蓄力计划
五. Java入门算法(双指针篇)丨蓄力计划
六. Java入门算法(数据结构篇)丨蓄力计划
七. Java入门算法(滑动窗口篇)丨蓄力计划
八. Java入门算法(动态规划篇1:初识动规)
九. Java入门算法(动态规划篇2:01背包精讲)
01背包
网上有非常多的文章对01背包进行讲解,变量名繁杂,对初学者不怎么友好。在这篇文章里,我尽量讲得简单,不作一些多余的赘述。
不会有人不知道背包是什么吧?
问题描述
把n种物品装进一个背包,物品 i 的重量是w[ i ],价值是c[ i ],背包的容量是M,求能装进背包的最大总价值。
注:w是存储n个物品的重量的数组,c是存储n个物品的价值的数组
分析
为啥叫01背包呢?因为对于每件物品,只有 不拿(0) 与 拿(1) 两种状态。
动态规划解01背包,关键是填二维表dp:
- 行 i 指的是我们当前要考虑装 i 个物品
- 列 j 表示的是背包剩余容量
- dp [i] [j] 的值是指 i 个物品装进容量为 j 的背包的最大总价值
当 i 等于物品总量n、j 等于背包容量M的时候,dp [i] [j] 的值就是问题的解。下面根据例子输入,边填表边分析。
输入:n = 4, M = 10
w[] = [2, 3, 4, 7]
c[] = [1, 3, 5, 9]
(4个物品装进容量为10的背包)
- 初始化第0行为全0,没有物品,不管容量多大,价值只能是0
- 初始化第0列为全0,容量为0,装不下任何物品,价值只能是0
现在只考虑装 1 件物品(i = 1)
- dp[1][1] = 0,容量为1的背包装不下第 1 件物品,因为它的重量为2
- dp[1][2] = 1,此时容量为2的背包可以装下物品1,而它的价值为1
- 第1行后面容量>1的背包,都可以装下物品1,因此它们的价值都为1
现在考虑的是装 2 件物品(i = 2)
- dp[2][1] = 0,容量为1的背包即装不下物品1(重量2)也装不下物品2(重量3)
- 容量为2的背包装不下物品2,但可以装下物品1
- 因此dp[2][2] = dp[1][2] = 1
此时引出第一种情况:
- 当背包容量小于物品 i 的重量 w[i] 时,拿不了物品 i ,所以考虑拿 i - 1个物品的最大总价值。
- 得到状态转移方程: dp[i][j] = dp[ i - 1][j] ( j < w[i])
if (j < w[i]) {
dp[i][j] = dp[i - 1][j];
}
- 容量为3的背包既可以装下物品2、也可以装下物品1。要使价值最大,我们肯定会选装物品2,这样就是dp[2][3] = 3。
- 此时的背包容量都用来装物品2,已经满了,装不下物品1(对应dp[1][0] = 0)。
- 但这只是我们主观得出的选择,计算机怎么知道是选装物品1还是物品2呢?
此时引出第二种情况:
- 当背包容量大于物品 i 的重量 w[i] 时,可以拿物品 i ,此时需要考虑拿了物品 i 的价值大,还是不拿物品 i 的价值大
- 不拿物品 i ,就是第一种情况,考虑拿 i - 1个物品的最大总价值
- 拿物品 i ,就需要把物品 i 装入到背包(+c[i])。但!还需要考虑装完物品 i 的背包还可以装多少(+ dp[ i - 1][ j - w[i]],即上述的 dp[1][0])
- 得到状态转移方程: dp[i][j] = c[i] + dp[ i - 1][ j - w[i]] ( j >= w[i])
如下,容量为5的背包,两个物品都可以装下
所以dp[2][5] = c[2] + dp[1][5 - 3] 即 4 = 3 + 1
if (j >= w[i]) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + c[i]);
}
综上,可以得到总的状态转移方程:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
,
(
j
<
w
[
i
]
)
dp[i][j] = dp[ i - 1][j] ,( j < w[i])
dp[i][j]=dp[i−1][j],(j<w[i])
d
p
[
i
]
[
j
]
=
c
[
i
]
+
d
p
[
i
−
1
]
[
j
−
w
[
i
]
]
,
(
j
>
=
w
[
i
]
)
dp[i][j] = c[i] + dp[ i - 1][ j - w[i]], ( j >= w[i])
dp[i][j]=c[i]+dp[i−1][j−w[i]],(j>=w[i])
状态转移方程代表了我们接下来的选择,根据它计算出表内的所有值,如下图
时间复杂度为O(n2),空间复杂度为O(M*n)
得到题目答案:12
完整代码
// 二维dp
public static int dp_2d(int n, int[] w, int[] c, int M) {
int[][] dp = new int[n + 1][M + 1];
for (int i = 1; i < n + 1; ++i) {
for (int j = 1; j < M + 1; ++j) {
if (j < w[i]) {
dp[i][j] = dp[i - 1][j];
}else{
dp[i][j] = Math.max(dp[i - 1][j], c[i] + dp[i - 1][j - w[i]]);
}
}
}
return dp[n][M];
}
进阶
滚动数组
- 由上述的填表顺序可以发现,每次 dp[i][j] 的值都是由 i - 1 行左上方或正上方的dp值得出。因此可以尝试把 [i] 这一维度去除,二维dp降成一维dp,通过不断刷新一维dp表的值,模拟上述二维dp的填表过程。时间复杂度仍为O(n^2^),空间复杂度降为O(M)。
- 但会发现若按照从左往右的顺序填表,会把需要用到的值覆盖,固填表方向需要反过来。
完整代码
// 二维降一维,滚动数组
public static int dp_1d(int n, int[] w, int[] c, int M) {
int[] dp = new int[M + 1];
for (int i = 1; i < n + 1; ++i) {
for (int j = 1; j < M + 1; ++j) {
if (j >= w[i]) {
dp[j] = Math.max(dp[j], dp[j - w[i]] + c[i]);
}
}
}
return dp[M];
}
END
参考资料:
https://www.bilibili.com/video/BV1C7411K79wfrom=search&seid=9137630755457139754