动态规划-背包问题
Posted sesiria
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划-背包问题相关的知识,希望对你有一定的参考价值。
背包问题是一种组合优化的NP完全问题。有N个物品和容量为W的背包,每个物品都有自己的体积w和价值v,求拿哪些物品可以使得背包所装下的物品的总价值最大。如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题;如果不限定物品的数量,则问题称为“无界背包或完全背包”问题。
一、0-1背包问题
可以用动态规划来解决背包问题。以0-1背包为例。我们可以定义一个二维数组dp存储最大值,其中dp[i][j]表示前i件物品体积不超过j的情况下能达到的最大价值;如果我们将物品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));
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 - 1][j - w] + v);
}
else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[N][W];
}
0-1背包问题-状态转移矩阵样例
可以进一步对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)。 这里注意需要逆向便利,这样才能够调用上一行物品i-1时 dp[j-w]的值;若按照从左往右的顺序进行正向遍历,则dp[j-w]的值在遍历到j之前就已经背更新成物品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];
}
同样的,我们也可以利用空间压缩将时间复杂度降低为O(W)。这里注意哦我们在便利每一行的时候必须正向便利,因为我们要利用当前物品在第j-w列的信息。
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];
}
注意:压缩空间到底需要正向还是逆向遍历呢?物品和体积哪个放在外曾,哪个放在内层呢?这取决与状态转移方程的依赖关系。在思考空间压缩之前,不妨将转移矩阵画出来,方便思考如何进行空间压缩。
如果实在不想仔细思考,有个简单的口诀:0-1背包对物品的迭代放外层,里层的体积或值逆向遍历;完全背包对物品的迭代放里层,外层的体积或价值正向遍历。
以上是关于动态规划-背包问题的主要内容,如果未能解决你的问题,请参考以下文章