动态规划dp问题之背包问题(0-1 完全)
动态规划(dp)
背包问题
0-1背包
0-1背包问题是一种组合优化的 NP
完全问题:有 N
个物品和容量为 W
的背包,每个物品都有自己的体积 w
和价值 v
,求拿哪些物品可以使得背包所装下物品的总价值最大。如果限定每种物品只能选择 0
个或 1
个,则问题称为 0-1
背包问题;如果不限定每种物品的数量,则问题称为无界背包问题或完全背包问题。
解决办法:
用动态规划来解决背包问题。以 0-1
背包问题为例。我们可以定义一个二维数组 dp
存储最大价值,其中 dp[i][j]
表示前 i 件物品体积不超过 j
的情况下能达到的最大价值。在我们遍历到第 i
件物品时,在当前背包总容量为j
的情况下,如果我们不将物品 i
放入背包,那么 dp[i][j]= dp[i-1][j]
,即前 i
个物品的最大价值等于只取前 i-1
个物品时的最大价值;如果我们将物品 i
放入背包,假设第 i
件物品体积为 w
,价值为 v
,那么我们得到 dp[i][j] = dp[i-1][j-w] + v
。我们只需在遍历过程中对这两种情况取最大值即可,总时间复杂度和空间复杂度都为 O(NW)
。
代码:
int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0)); //dp空间二维vector
for (int i = 1; i <= N; ++i) //遍历N件物品
{
int w = weights[i-1], v = values[i-1]; //初始化从0
for (int j = 1; j <= W; ++j)
{
if (j >= w) //容量能装下体积w这个物品 能放入不代表放入性价比最高
{
// 不放入 放入之后(这里j-w是放入这个w后其余东西所占位置,答案不一,存的是max)
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v);
}
else //装不下
{
dp[i][j] = dp[i-1][j]; //只能是放入前的dp
}
}
}
return dp[N][W];
}
可以进一步对 0-1 背包进行空间优化,将空间复杂度降低为 O(W)
。如图所示,假设我们目前考虑物品 i = 2
,且其体积为 w = 2
,价值为 v = 3
;对于背包容量 j
,我们可以得到 dp[2][j]= max(dp[1][j], dp[1][j-2] + 3)
。这里可以发现我们永远只依赖于上一排 i = 1
的信息,之前算过的其他物品都不需要再使用。因此我们可以去掉 dp
矩阵的第一个维度,在考虑物品 i
时变成 dp[j]= max(dp[j], dp[j-w] + v)
。
这里需要注意的是第二层for循环
遍历背包时需要反向遍历
,因为我们在二维dp的方法中正向遍历没问题的原因是我们有第一个维度i
来存储上一层的状态,而优化为一维之后,我们要通过状态不断覆盖来实现二维的操作,如果是正向遍历, dp[j-w]
的值在遍历到j
之前就已经被更新成物品 i
的值了。(即在处理dp[j-w]+v)
时dp[j-w]
里已经存在i
的一次放入了,那么就会重复再放一次,如果反向遍历那么就会解决这个问题,代码如下:
int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
vector<int> dp(W + 1, 0);
for (int i = 1; i <= N; ++i)
{
int w = weights[i-1], v = values[i-1];
for (int j = W; j >= w; --j)
{
dp[j] = max(dp[j], dp[j-w] + v);
}
}
return dp[W];
}
完全背包
假设我们遍历到物品 i = 2,且其体积为 w = 2,价值为 v = 3;对于背包容量 j = 5,最多只能装下 2 个该物品。那么我们的状态转移方程就变成了 dp[2][5] = max(dp[1][5], dp[1][3] + 3, dp[1][1] + 6)。如果采用这种方法,假设背包容量无穷大而物体的体积无穷小,我们这里的比较次数也会趋近于无穷大,远超 O(NW) 的时间复杂度。
怎么解决这个问题呢?我们发现在 dp[2][3] 的时候我们其实已经考虑了 dp[1][3] 和 dp[2][1]的情况,而在时 dp[2][1] 也已经考虑了 dp[1][1] 的情况。因此,如图下半部分所示,对于拿多个物品的情况,我们只需考虑 dp[2][3] 即可,即 dp[2][5] = max(dp[1][5], dp[2][3] + 3)。这样,我们就得到了完全背包问题的状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v),其其与 0-1 背包问题的差别仅仅是把状态转移方程中的第二个 i-1 变成了 i
int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; ++i)
{
int w = weights[i-1], v = values[i-1];
for (int j = 1; j <= W; ++j)
{
if (j >= w)
{
dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v);
}
else
{
dp[i][j] = dp[i-1][j];
}
}
}
return dp[N][W];
}
同样可以压缩空间,但是要注意我们在遍历每一行的时候必须正向遍历,因为我们需要利用当前物品在第 j-w 列的信息。代码恰好是0-1背包的二层for变为正序
int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
vector<int> dp(W + 1, 0);
for (int i = 1; i <= N; ++i)
{
int w = weights[i-1], v = values[i-1];
for (int j = w; j <= W; ++j)
{
dp[j] = max(dp[j], dp[j-w] + v);
}
}
return dp[W];
}