背包问题详细解析
01背包问题
1.经典例题
有 \(N\) 件物品和一个容量是 \(V\) 的背包。每件物品只能使用一次。
第 \(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
2.解题思路
先给出一种较为暴力的二维动态规划做法:
用 \(dp_{(i,j)}\) 表示只看前 \(i\) 个物品,当前总体积为 \(j\) 时的最大总价值。
那么我们的答案就是选前 \(n\) 个物品,当前体积为 \(1\) 到 \(v\) 中的最大值,即 \(res = max \{dp_{(n,0-v)}\}\) 。
然后我们考虑 \(dp_{(i,j)}\) 的计算公式:对于第 \(i\) 个物品,我们有选与不选两种情况,那么分别写出公式:
-
1.不选第 \(i\) 个物品,那么其价值和体积与上一个物品时一样,即:\(dp_{(i,j)} = dp_{(i-1,j)}\)
-
2.选第 \(i\) 个物品,那么其价值就是只看前 \(i-1\) 个物品且当前体积为 \(j-v_i\) 时的价值加上当前物品的价值,即 \(dp_{(i,j)} = dp_{(i-1,j-v_i)} + w_i\)
那么我们的 \(dp_{(i,j)}\) 就是以上两种情况的较大值。
但是我们想一想,如果只有这两个公式,那么 \(dp\) 最开始什么也没有,用这个数组中的元素加减乘除也全都得0,所以我们还要考虑给数组赋初始值的问题。。。
然后我们不难发现,当一个物品也不考虑,且体积为 \(0\) 时,最大价值为 \(0\),即 \(dp_{(0,0)} = 0\)
代码如下
#include <iostream>
#include <algorithm>
#include <cstring>
//#define int long long
using namespace std;
#define N 1010
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()
int n, m, v[N], w[N], dp[N][N];
int main () {
IOS;
cin >> n >> m;
For (i, 1, n) cin >> v[i] >> w[i];
dp[0][0] = 0;
For (i, 1, n) {
For (j, 0, m) {
dp[i][j] = dp[i - 1][j];
if (j >= v[i])
dp[i][j] = max (dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
}
int ans = 0;
For (i, 0, m) {
ans = max (ans, dp[n][i]);
}
cout << ans << endl;
return 0;
}
然后我们再来考虑将二维改为一维去优化它:
我们发现在转移第 \(i\) 个物品时,其最大价值永远只与 \(i - 1\) 有关,所以我们其实不需要记录第二维,于是我们用 \(dp_i\) 表示体积为 \(i\) 时能得到的最大价值,然后我们来观察两个状态转移方程:
dp[i][j] = dp[i - 1][j];
dp[i][j] = max (dp[i][j], dp[i - 1][j - v[i]] + w[i]);
因为我们是消掉了数组的第一维,所以第一个方程可以直接去掉,但是对于第二个方程,如果强行滚掉第一维,肯定是不可取的。因为我们在计算时会用到 \(dp_{(i-1,j-v_i)}\) 这个东西,但是如果强行消掉了,就会变成 \(dp_{(i,j-v_i)}\),显然是不行的。
我们不难发现,在枚举 \(i\) 时,我们肯定是要保证 \(j-v_i\) 没有被用过,但是如果我们从小到大枚举,而 \(v_i\) 有肯定是个正数,所以当枚举到 \(j\) 时, \(j-v_i\) 肯定被使用过了。
所以我们可以倒着枚举,从大枚举到小,那样就能保证 \(j-v_i\) 没有被使用过了。
所以我们就只需要一个状态转移方程:
dp[j] = max (dp[j], dp[j - v[i]] + w[i]);
而最后的枚举答案的循环也可以被省掉,答案就是 \(dp_m\),因为 \(dp_i\) 指体积最大为 \(m\) 的最大价值,就将所有情况都包含在里面了。
代码如下
#include <iostream>
#include <algorithm>
#include <cstring>
//#define int long long
using namespace std;
#define N 1010
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()
int n, m, v[N], w[N], dp[N];
int main () {
IOS;
cin >> n >> m;
For (i, 1, n) cin >> v[i] >> w[i];
For (i, 1, n) {
for (int j = m; j >= 0; j --) {
if (j >= v[i])
dp[j] = max (dp[j], dp[j - v[i]] + w[i]);
}
}
cout << dp[m] << endl;
return 0;
}
2.完全背包问题
1.经典例题
有 \(N\) 种物品和一个容量是 \(V\) 的背包,每种物品都有无限件可用。
第 \(i\) 种物品的体积是 \(v_i\),价值是 \(w_i\)。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
2.解题思路
不难发现,完全背包问题其实就是在01背包问题的基础上将 最多选一次 变成了 可以选无限次。
那么我们仍然可以用 \(f_{(i, j)}\) 表示选前 \(i\) 个物品且总体积 \(\leq j\) 的所有选法的价值的最大值。
然后对于每个 \(f_{(i, j)}\),关于第 \(i\) 个物品我们可以不选,也可以选 \(1\) 个, \(2\) 个 ... \(k\) 个。
然后我们来分情况讨论这两种选法:
-
1.不选第 \(i\) 个,即 \(f_{(i - 1, j)}\)
-
2.选第 \(i\) 个且选择了 \(k\) 个,即 \(f_{(i - 1, j - k * v_i)} + k * w_i\)
最后答案就是两者的较大值。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m, v[N], w[N], f[N][N];
int main () {
cin >> n >> m;
for (int i = 1; i <= n; i ++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++)
for (int j = 0; j <= m; j ++)
for (int k = 0; k * v[i] <= j; k ++)
f[i][j] = max (f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
cout << f[n][m] << endl;
return 0;
}
但是很明显,代码最多有三层循环,时间复杂度 \(O (n * m ^ 2)\),肯定会超时。。。
所以我们要想办法优化掉一层循环。
我们将状态转移方程展开,发现:
然后我们把 \(f[i - 1][j - v]\) 展开,即:
所以我们发现,一式后面所有项的最大值实际上就是二式的 \(f[i][j - v]\) 再加上一个 \(w\)。。。
所以化简之后的方程式为:
这样一来时间久被优化到两层 \(O(nm)\),可以过了。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m, v[N], w[N], f[N][N];
int main () {
cin >> n >> m;
for (int i = 1; i <= n; i ++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++)
for (int j = 0; j <= m; j ++)
if (j - v[i] >= 0)
f[i][j] = max (f[i - 1][j], f[i][j - v[i]] + w[i]);
else f[i][j] = f[i - 1][j];
cout << f[n][m] << endl;
return 0;
}
然后和 \(01\) 背包一样,完全背包同样可以直接删掉一维空间,而且是不需要倒序模拟的。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m, v[N], w[N], f[N];
int main () {
cin >> n >> m;
for (int i = 1; i <= n; i ++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++)
for (int j = v[i]; j <= m; j ++)
//注意这里实际上是 j 从 0~m 循环,然后判断 j - v[i] 是否大于等于0,简写成上面这样是等效的
f[j] = max (f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}

浙公网安备 33010602011771号