0-1背包
二维实现
有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
由于每个物体只有两种可能的状态(取与不取),对应二进制中的 0 和 1,这类问题便被称为「0-1 背包问题」
借用《算法图解》书中示例:
你是一个小偷,携带一个能装4磅的背包,有如下三样东西,选择那些能偷的价值最多
- 确定dp数组以及下标含义
最普遍的解法是创建一个二维dp数组
int dp[weightSize(物品数量)][bagWeight(背包容量) + 1]; // weight数组的大小 就是物品个数
我们的行表示不同的物品,列表示不同的背包容量。
dp[i][j]
表示从第1行到当前行[0-i]
所对应的物品中任意取,放进容量为j
的背包,价值总和最大是多少。
我们来看dp
数组来逐行分析理解:
dp[0][0]
表示给出1磅的空间,在吉他中选择,所能获得的最大收益,显然是吉他的价值;
dp[0][1]
dp[0][2]
dp[0][3]
则表示了给出2,3,4磅空间,在吉他中选择,所能获得的最大收益,此时我们定义只能选择当前行的物品,所以无论多大容量,最大收益都是1500;
dp[1][0]
表示给出1磅的空间,在吉他和音响中选择,所能获得的最大收益,1磅无法装下音响,此时最佳选择仍是偷吉他
dp[1][1]
dp[1][2]
dp[1][3]
则表示了给出2,3,4磅空间,在吉他(1磅),音响(4磅)中选择,所能获得的最大收益,在4磅之前,我们都只能偷吉他,当有了4磅空间后,显然直接偷音响是最划算的选择。
-
在明白每格的定义后,我们来看看如何确定递推公式,得到数据
对于每件物品我们有不取和取两种状态,
-
对于不取当前行物品,此时值为当前单元格上边的一格(
dp[i - 1][j]
表示的是不考虑本行物品时的最大价值) -
对于取当前行物品,此时值为当前行对应商品的价值 + 剩余空间的价值(
dp[i - 1][j - weights[i]] + costs[i]
表示的时如果优先放入本行对应物品后,考虑放入后剩余空间(j - weights[i])
能放的最大价值,最大价值需要从上一层查找)
我们可以得到递推公式
dp[i][j] = fmax(dp[i - 1][j], dp[i - 1][j - weights[i]] + costs[i]);
-
-
初始化
第一列都是0:背包容量为0,所得最大收益均为0。
第一行表示只选取0号物品最大价值:小于0号物品质量也为0,大于后为0号物品价值
memset(dp, 0, sizeof(int) * weightSize * (bagWeight + 1)); for (int j = weight[0]; j <= bagweight; j++) { dp[0][j] = value[0]; }
-
遍历顺序
-
先遍历物品还是先遍历背包重量都可行
-
每行物品的顺序位置也不影响最终的结果。
-
如果我们有第四件可以偷的东西,我们也只需添加到最后一行就行了,因为我们每个数据的得出只依靠上一行的数据
-
代码如下:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int knapsack(int weightSize, int bagWeight, int weights[], int values[]) {
int dp[weightSize][bagWeight + 1];
memset(dp, 0, sizeof(int) * weightSize * (bagWeight + 1));
for (int j = weights[0]; j <= bagWeight; j++) {
dp[0][j] = values[0];
}
// 内层循环遍历背包的不同容量,外层循环遍历可选的物品。
//在每次迭代中,我们都尝试将当前物品放入背包,以获得背包在当前容量下的最大价值。
for (int i = 1; i < weightSize; i++) {
for (int j = 0; j <= bagWeight; j++) {
if (weights[i] > j) {
// 当前物品超过当前背包容量,无法放入,保持上一行的状态
// 最大价值就是拿第i-1个物品的最大价值
dp[i][j] = dp[i - 1][j];
} else {
// 考虑放入或不放入当前物品,取最大值
dp[i][j] = MAX(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]);
}
}
}
return dp[weightSize - 1][bagWeight];
}
一维滚动数组
二维代码可以进行优化,简化为一维背包。
思路是将dp[i - 1]
那一层拷贝到dp[i]
下来,并对数据进行覆盖
-
确定dp数组含义
dp[j]
表示的是容量为j的背包,所能得最大价值 -
递推公式
dp[j] = fmax(dp[j], dp[j - weight[i]] + value[i]);
有不取当前物品和取当前物品两种状态:
-
不取:
dp[j]
,拷贝上一层数据不变,相当于二维数组方法中的dp[i - 1]
-
取:
dp[j - weight[i]] + value[i]
,考虑当前的第i
个物品,优先放入该物品后所剩空间为j - weight[i]
找到对应
dp[j - weight[i]]
加上value[i]
就是取当前物品的价值
将两个数据比较后,取最大即可
-
-
初始化
dp[0]
一定是0,因为容量为0装不下任何物品除了下标0的位置,也初始为0,dp数组每次遍历是取价值最大的数,所以我们初始化为0
-
遍历顺序
虽然是一维,但是要进行两层遍历,这是为了模拟不同物品的选择。出于此我们只能先外层遍历不同物品,
内层遍历背包的不同容量。
同时我们要对内层背包容量进行倒序遍历:
- 如果正序遍历
dp[j - weight[i]]
的值将不是上一层的拷贝,而是本层遍历时所改变的值。
i = 0
(外层第一层遍历)j = 1
时dp[1] = max(dp[1], dp[1 - 1] + value[0]) = 1500
j = 2
时dp[2] = max(dp[2], dp[2 - 1] + value[0]) = 3000
此时dp[1]
的值为1500说明当前物品被计算了两次。
- 如果倒序遍历
i = 0
(外层第一层遍历)j = 4
时dp[4] = max(dp[4], dp[4 - 1] + value[0]) = 1500
j = 3
时dp[3] = max(dp[3], dp[3 - 1] + value[0]) = 1500
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
对于二维dp,
dp[i][j]
都是通过上一层即dp[i - 1][j]
计算而来,本层的dp[i][j]
并不会被覆盖。 - 如果正序遍历
//倒序遍历
for(int i = 0; i < weightSize; i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
代码如下:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int knapsack(int weightSize, int bagWeight, int weights[], int values[]) {
int dp[bagWeight + 1];
for (int i = 0; i <= bagWeight; i++) {
dp[i] = 0;
}
for (int i = 0; i < weightSize; i++) { // 遍历物品
for (int j = bagWeight; j >= weights[i]; j--) { // 逆序遍历背包容量
dp[j] = MAX(dp[j], dp[j - weights[i]] + values[i]); // 不取或者取第i个
}
}
return dp[bagWeight];
}
完全背包
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
0-1背包滚动数组部分我们提到如果不进行倒序遍历物品会被多次计算,这正好符合完全背包的需求
//0-1背包
for(int i = 0; i < weightSize; i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//完全背包
for(int i = 0; i < weightSize; i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
对于上述代码不难发现相比于0-1背包,我们此时正序遍历,从当前物品重量开始考虑背包容量,直到背包容量达到最大值为止。
- 我们dp数组的推导依靠每次从左侧遍历
dp[j]
的数据,这样dp[j - weight[i]]
为本行左侧的数据 - 而从右往左倒序遍历,
dp[j - weight[i]]
为上一行的数据
对于完全背包问题,我们先遍历物品还是先遍历背包重量都可行
for (int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for (int i = 0; i < weightSize; i++) { // 遍历物品
if (j - weights[i] >= 0) {
dp[j] = MAX(dp[j], dp[j - weights[i]] + values[i]);
}
}
}
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
这道题求排列数求组合数
class Solution {
public int change(int amount, int[] coins) {
int i, j;
int[] dp = new int[amount + 1];
dp[0] = 1;
for (i = 0; i < coins.length; i++) {
for (j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
}
这道题求排列数
class Solution {
public int combinationSum4(int[] nums, int target) {
int i, j;
int[] dp = new int[target + 1];
dp[0] = 1;
// 外层容量内存物品
for (i = 0; i <= target; i++) {
for (j = 0; j < nums.length; j++) {
if (i >= nums[j])
dp[i] += dp[i - nums[j]];
}
}
return dp[target];
}
}
参考:
感谢您的阅读
如有错误,烦请指正