一.01背包
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
0<N,V≤10000
0<vi,wi≤1000
(一).详细分析“01背包”动态规划思路
1.问题本质
你有一个容量有限的背包,和一堆物品。每个物品有自己的体积和价值。你的目标是:在不超过背包容量的前提下,选择一些物品装入背包,使得这些物品的总价值最大。
关键在于“0-1”:每个物品只有两种命运,要么被选中 (1),要么不被选中 (0)。不能只装入一个物品的一部分。
2.动态规划(DP)的核心思想
动态规划的精髓在于将复杂问题分解为更小的、重叠的子问题,并通过存储子问题的解(记忆化)来避免重复计算,最终构建出原问题的解。
对于背包问题,我们定义的状态(子问题)是:
dp[i][j]
表示一个状态:只考虑前 i
个物品,在背包容量为 j
的情况下,可以获得的最大价值。
i
的范围是 0
到 N
(物品编号从1开始,i=0
表示不考虑任何物品)。
j
的范围是 0
到 V
(背包容量)。
我们的最终目标就是求出 dp[N][V]
的值。
3.状态转移方程(最关键的一步)
1.不选第i个物品:
如果我们决定不拿这个物品,那么情况就变得非常简单。
此时的最大价值,就等于只考虑前 i-1
个物品,且背包容量仍然为 j
时的最大价值。
也就是 dp[i][j] = dp[i-1][j]
。
2.选择第i个物品:
如果我们决定拿这个物品,那么首先有一个前提条件:背包的剩余容量必须至少能装下这个物品,即 j >= v[i]
。
如果我们拿了这个物品,我们会获得它的价值 w[i]
,但同时也会消耗掉 v[i]
的背包容量。
那么,剩下的背包容量就是 j - v[i]
。在这些剩余的容量里,我们依然要做出最优决策。“只考虑前 i-1
个物品,背包容量为 j - v[i]
时的最大价值” 就是我们在剩余容量上能获得的最大价值,即 dp[i-1][j - v[i]]
。
所以,选择第 i
个物品带来的总价值是:w[i] + dp[i-1][j - v[i]]
。
结论:我们的目标是最大化价值。因此,对于每一种状态 (i, j)
,我们都在上述两种选择中挑一个更好的(价值更大的)。
综合起来就是:
如果 j < v[i] (当前背包容量装不下第i个物品):
dp[i][j] = dp[i-1][j] // 只能不选
否则 (可以装下,进行决策):
dp[i][j] = max( dp[i-1][j], // 不选第i个物品
w[i] + dp[i-1][j - v[i]] // 选择第i个物品
)
4.初始化
我们需要一个初始状态来开始整个递推过程。
dp[0][0..V]
:考虑前 0
个物品(也就是没有任何物品可选)。那么无论背包容量有多大,能获得的最大价值都是 0
。
同样,dp[0..N][0]
:背包容量为 0
。那么无论有多少物品,你都装不下任何东西,最大价值也是 0
。
在实际代码中,我们通常会创建一个大小为 (N+1) x (V+1)
的二维数组,并将第一行 (i=0
) 和第一列 (j=0
) 全部初始化为 0
。
5.计算顺序与填表
有了初始状态和状态转移方程,我们就可以想填表一样计算出所有状态的值。
1.外层循环 i
从 1
到 N
(逐个考虑每个物品)。
2.内层循环 j
从 0
到 V
(逐个考虑每种可能的背包容量)。
3.对于每一个 (i, j)
,根据上面的状态转移方程,利用之前已经计算好的 dp[i-1][...]
的值来计算 dp[i][j]
。
最终,表格最右下角的值 dp[N][V]
就是我们要求的答案。
6.空间优化(滚动数组或一维数组)
仔细观察状态转移方程,你会发现:在计算 dp[i]
这一行的所有状态时,只依赖于上一行 dp[i-1]
的数据,而更早的数据就不再需要了。
这意味着我们并不需要存储整个 N x V
的二维数组。我们可以使用一个一维数组 dp[0..V]
来滚动更新。
这个一维数组 dp[j]
在计算过程中,实际表示的是上一轮(i-1
)的结果,即 dp[i-1][j]
。
当我们计算新的一轮 i
时,我们想要用上一轮的数据来覆盖它,得到 dp[i][j]
。
但是,这里有一个关键点:内层循环 j
必须从大到小(从 V
到 0
)进行遍历。
为什么?
因为状态转移方程需要的是 dp[i-1][j - v[i]]
,也就是上一轮、更小容量的状态。如果我们从小到大遍历 j
,那么当我们计算 dp[j]
时,dp[j - v[i]]
可能已经在本轮被更新过了(变成了 dp[i][j - v[i]]
),这就污染了我们需要使用的“上一轮”的数据。而从大到小遍历可以保证我们使用的 dp[j - v[i]]
仍然是上一轮的结果。
优化后的状态转移(一维数组):
for (int i = 1; i <= N; i++) {
for (int j = V; j >= v[i]; j--) { // 逆序更新容量
dp[j] = max(dp[j], w[i] + dp[j - v[i]]);
}
// j < v[i] 时,dp[j] 保留上一轮的值
}
(二).代码展示
二维数组:
#include <iostream>
#include <algorithm>
using namespace std;
const int MAX_N = 1010;
const int MAX_V = 1010;
int main() {
int N, V;
cin >> N >> V;
// 物品数组,下标从1开始使用
int v[MAX_N] = {0}; // 体积
int w[MAX_N] = {0}; // 价值
// 读取物品信息
for (int i = 1; i <= N; i++) {
cin >> v[i] >> w[i];
}
// DP数组:dp[i][j]表示考虑前i个物品,背包容量为j时的最大价值
int dp[MAX_N][MAX_V] = {0};
// 动态规划过程
for (int i = 1; i <= N; i++) { // 遍历每个物品
for (int j = 0; j <= V; j++) { // 遍历每种容量
// 默认不选第i个物品
dp[i][j] = dp[i - 1][j];
// 如果当前容量可以装下第i个物品,考虑选择它
if (j >= v[i]) {
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
}
}
// 输出结果:考虑前N个物品,容量为V时的最大价值
cout << dp[N][V] << endl;
return 0;
}
一维数组:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
int n, v;
cin >> n >> v;
vector<int> dp(v + 1, 0);
for (int i = 0; i < n; ++i) {
int vi, wi;
cin >> vi >> wi;
for (int j = v; j >= vi; --j) {
dp[j] = max(dp[j], dp[j - vi] + wi);
}
}
cout << dp[v];
}
二.完全背包
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
0<N,V≤10000
0<vi,wi≤1000
(一).问题本质
与0-1背包不同,完全背包中每种物品都有无限件可用。这意味着对于每种物品,你可以选择拿0件、1件、2件...直到背包容量装不下为止。
你的目标依然是:在不超过背包容量的前提下,选择物品(数量不限),使得总价值最大。
(二).动态规划思路
依然使用动态规划,定义的状态是:
dp[i][j]
表示考虑前 i
种物品,在背包容量为 j
的情况下,可以获得的最大价值。
1.状态转移方程分析
对于第 i
种物品,我们现在有多种选择:可以拿0件、1件、2件...k件(只要不超过背包容量)。
因此,状态转移方程需要遍历所有可能的数量:
dp[i][j] = max{
dp[i-1][j], // 不拿第i种物品
dp[i-1][j - v[i]] + w[i], // 拿1件第i种物品
dp[i-1][j - 2*v[i]] + 2*w[i], // 拿2件第i种物品
...
dp[i-1][j - k*v[i]] + k*w[i] // 拿k件第i种物品
}
其中 k 满足:k * v[i] <= j
2.优化状态转移方程
上面的方程需要遍历k,时间复杂度较高(O(NVV))。可以通过数学变换来优化:
观察 dp[i][j]
和 dp[i][j - v[i]]
的关系:
dp[i][j] = max( dp[i-1][j], dp[i-1][j-v[i]] + w[i], dp[i-1][j-2v[i]] + 2w[i], ... )
dp[i][j - v[i]] = max( dp[i-1][j-v[i]], dp[i-1][j-2v[i]] + w[i], dp[i-1][j-3v[i]] + 2w[i], ... )
可以发现:dp[i][j] = max( dp[i-1][j], dp[i][j - v[i]] + w[i] )
这个优化后的方程极其重要! 它的含义是:
要么不选第i种物品:dp[i-1][j]
要么至少选一件第i种物品:dp[i][j - v[i]] + w[i]
注意这里第二项是 dp[i][j - v[i]]
而不是 dp[i-1][j - v[i]]
,这是因为我们允许重复选择同一物品。
3.最终的状态转移方程
如果 j < v[i]:
dp[i][j] = dp[i-1][j]
否则:
dp[i][j] = max( dp[i-1][j], dp[i][j - v[i]] + w[i] )
4.初始化
与0-1背包相同:
dp[0][0..V] = 0
(没有物品可选)
dp[0..N][0] = 0
(没有容量可用)
5.空间优化(一维数组)
同样可以优化空间,使用一维数组 dp[j]
。但注意:这里内层循环需要从小到大遍历!
为什么?
因为状态转移方程是 dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i])
,我们需要的是本轮的 dp[i][j - v[i]]
,所以应该先计算小的j,再计算大的j。
优化后的代码结构:
for (int i = 1; i <= N; i++) {
for (int j = v[i]; j <= V; j++) { // 从小到大遍历
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
(三).代码展示
二维数组:
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, v;
cin >> n >> v;
vector<int> vi(n + 1, 0);
vector<int> wi(n + 1, 0);
vector<vector<int>> dp(n + 1, vector<int>(v + 1, 0));
for (int i = 1; i <= n; i++) cin >> vi[i] >> wi[i];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= v; j++) {
if (j < vi[i]) dp[i][j] = dp[i - 1][j];
else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - vi[i]] + wi[i]);
}
}
}
cout << dp[n][v] << endl;
}
一维数组:
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, v;
cin >> n >> v;
vector<int> vi(n + 1, 0);
vector<int> wi(n + 1, 0);
vector<int> dp(v + 1, 0);
for (int i = 1; i <= n; i++) cin >> vi[i] >> wi[i];
for (int i = 1; i <= n; i++) {
for (int j = vi[i]; j <= v; j++) {
dp[j] = max(dp[j], dp[j - vi[i]] + wi[i]);
}
}
cout << dp[v] << endl;
}