1 0-1背包

题目:有N件物品和一个容量为V的背包。第i建物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大(每件物品只能被使用一次

根据题目,可以确定使用动态规划思路进行求解,定义一个数组$dp$,$dp[i][j]$表示有$i$件物品,背包容量是$j$所能装入的物品的最大价值。

则有状态转移方程如下:$dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i])$

其中,每次选取都有两种选择:

  • 装入背包(背包能放得下该物品):则问题就转化成前$i-1$件物品装入容量为$j$的背包的最大价值,则表示为$dp[i-1][j-c[i]]$,再加上当前物品$i$的价值$w[i]$,
  • 不装入背包:那么问题就转换成前$i-1$件物品放入容量为$j$的背包的最大价值,则$dp[i-1][j]$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
*
* @param n 物体的数量
* @param w 背包所能放物体的最大重量
* @param wt 每件物体的重量
* @param val 每件物体的价值
* @return
*/
public int dpSolution(int n, int w, int[] wt, int[] val) {
int[][] dp = new int[n + 1][w + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= w; j++) {
if (j >= wt[i - 1])//背包所能承受的重量大于物体的重量,就放进去
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - wt[i - 1]] + val[i - 1]);
else
dp[i][j] = dp[i -1][j];
}
}
return dp[n][w];
}

0-1背包的空间优化

回顾 01 背包的基本解法的状态转移方程,如下:

1
2
dp[i][j] = dp[i - 1][j] 当前物品不选
dp[i - 1][j - v[i]] + w[i] 当前物品选,j - v[i] 要大于等于 0

空间复杂度为 $O(NV)O(NV)$,状态转移中 i 这一维只跟 i - 1 有关系,因此 i 这一维用滚动数组至少可以将 N 行优化为 2 行。这本来是动态规划的基本操作,在线性动态规划中很常见。这里特别提空间优化是因为 01 背包的 i 这一维用 1 行就可以解决。

1
假设状态只有一行,即 dp[j] := 占用了 j 空间的情况下可以取到的最大价值, 在推第 i 行的时候,dp 数组中存的是第 i - 1 行的信息。

看状态的两个转移方向,第一个是 $dp[i - 1][j]$,这刚好就是当前 $dp $数组在 $j $位置保存的数据,因此不用动,比较麻烦的是另一个,就是 $dp[i - 1][j - v[i]] + w[i]$。这里要用到第 $i - 1 $行的 $dp[j - v[i]]$,但是如果按照正常的 $j $从 $0$ 到 $V$ 推的话,计算 $dp[j]$ 的时候,$dp[j - v[i]]$ 保存的已经是第 $i $行信息了。

因此这里需要转换一下,从大往小推,推到 $dp[j]$ 时,$dp[j+1], dp[j+2],…,dp[V]$ 都已经是第$i$行的信息了,但是它们对 $dp[j] $的计算没有影响,有影响的 $dp[j-v[i]]$ 此时还是第 $i - 1$ 行的信息,可以满足转移方程$dp[i - 1][j - v[i]] + w[i] $的需要。因此当空间这一维的状态从大往小推的时候,$i$ 这一维状态可以优化到一维。

这就是 01 背包的终极形态了。

1
dp[j] = max(dp[j], dp[j - v[i]] + w[i]) // j 从大往小推

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
*
* @param n 物体的数量
* @param w 背包所能放物体的最大重量
* @param wt 每件物体的重量
* @param val 每件物体的价值
* @return
*/
public int dpSolution2(int n, int w, int[] wt, int[] val){
int [] dp=new int[w+1];
for (int i = 1; i < n+1; i++) {
for (int j=w;j>0;j--) {
if (j>=wt[i-1])
dp[j] = Math.max(dp[j],dp[j-wt[i-1]]+val[i-1]);
}
}
return dp[w];
}

2 完全背包

题目:有N件物品和一个容量为V的背包。第i建物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大(每件物品可以被无限次使用

与0-1背包不同的是,完全背包中的每个物品可以被无限次使用。

状态转移方程:$dp[i][j]=max(dp[i-1][j],dp[i][j-c[i]]+w[i])$

  • 不装入背包:则问题转化成从前$i-1$个物品所能用的容量是$j$,则有$dp[i-1][j]$
  • 装入背包:在0-1背包中,当取第$i$个物品时,可以知道前$i-1$个物品所能用的最大容量是$j-c[i]$,完全背包中每个物品可以取无限次,所以如果取第$i$个物品,那么前$i$个物品所能用的容量还是$j-c[i]$,$dp[i][j-c[i]]+w[i]$。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
*
* @param n 物体的数量
* @param w 背包所能放物体的最大重量
* @param wt 每件物体的重量
* @param val 每件物体的价值
* @return
*/
public int dpSolution(int n, int w, int[] wt, int[] val) {
int[][] dp = new int[n + 1][w + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= w; j++) {
if (j >= wt[i - 1])//背包所能承受的重量大于物体的重量,就放进去
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - wt[i - 1]] + val[i - 1]);
else
dp[i][j] = dp[i -1][j];
}
}
return dp[n][w];
}

完全背包的空间优化

考虑空间优化后的状态转移方程如下:在 01 背包中,由于推第 $i$ 行的 $dp[j]$ 时需要第 $i - 1$ 行的 $dp[j-v[i]] $,因此忌讳在推导 $dp[j]$ 时,$dp[j-v[i]]$ 已经更新过了, 这是 01 背包不希望发生的事,解决的办法就是 $j$ 从大往小推。

而上述 01 背包不希望发生的事正是完全背包希望发生的,即推导第 $i$ 行的 $dp[j]$ 时,用到第 $i$ 行的 $dp[j -v[i]]$ 。而这仅需要把 01 背包中的从大往小推 $j$ 改为从小往大推 $j$ 即可实现。这就是完全背包的优化巧妙的地方。

$dp[j] = max(dp[j], dp[j - v[i]] + w[i]) $

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/dynamic-programming-2-plus/5253i5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
*
* @param n 物体的数量
* @param w 背包所能放物体的最大重量
* @param wt 每件物体的重量
* @param val 每件物体的价值
* @return
*/
public int dpSolution2(int n, int w, int[] wt, int[] val){
int [] dp=new int[w+1];
for (int i = 1; i < n+1; i++) {
for (int j = 1; j <= w; j++) { //从小往大推
if (j>=wt[i-1])
dp[j] = Math.max(dp[j],dp[j-wt[i-1]]+val[i-1]);
}
}
return dp[w];
}

3 多重背包问题

有N种物品和一个容量为V的背包。第i种物品最多有n[i]件,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大

思路 1 :将物品展开,全拆成 01

这与完全背包的朴素思路一致。与完全背包的情况一样,这种方法是正确的,但是时间复杂度较高。

多重背包的朴素算法与完全背包的朴素算法没有区别,而多重背包的优化相对较难,并且力扣上面没有多重背包的题目。下面仅对多重背包的优化做简单的介绍。

思路 2 :2 进制分解

这是对思路 1 的优化。

有这样一个事实:任意一个数 n,它一定可以用 1,2,4,8,… $2^{k}$ ,以及 $2^{k}$到 $2^{k+1}$之间的某个数表示。例如 13 以内的所有数都可以用 1,2,4,6 表示。所以对于物品 $i$, 数量限制是$c_{i}$, 可以将其分成若干物品,它们的价值和体积为:($w_{i}$ ,$v_{i}$),($2w_{i}$,$2v_{i}$),…

然后对这些物品做 01 背包。这样 01 背包的物品数就比思路 1 少很多了。这可以理解为类似于倍增法的思想。倍增法超出了力扣的范围,感兴趣的话可以找相关的资料学习。

以上就是背包动态规划的基本内容。背包动态规划在力扣上题目不多,下一节整理了 8 道背包动态规划的练习题,通过这些题可以大致了解背包问题的一些经典问题和常见的问法。

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/dynamic-programming-2-plus/5253i5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

写在最后

欢迎大家关注鄙人的公众号【麦田里的守望者zhg】,让我们一起成长,谢谢。
微信公众号