题目
洛谷P2871
Bessie has gone to the mall’s jewelry store and spies a charm bracelet. Of course, she’d like to fill it with the best charms possible from the N (1 ≤ N ≤ 3,402) available charms. Each charm i in the supplied list has a weight Wi (1 ≤ Wi ≤ 400), a ‘desirability’ factor Di (1 ≤ Di ≤ 100), and can be used at most once. Bessie can only support a charm bracelet whose weight is no more than M (1 ≤ M ≤ 12,880).
Given that weight limit as a constraint and a list of the charms with their weights and desirability rating, deduce the maximum possible sum of ratings.
分析
经典的0-1背包板子题
递归表达式:
d
p
[
n
]
[
w
]
=
m
a
x
(
d
p
[
n
−
1
]
[
w
]
,
d
p
[
n
−
1
]
[
w
−
W
[
n
]
+
D
[
n
]
dp[n][w] = max(dp[n-1][w], dp[n-1][w-W[n]+D[n]
dp[n][w]=max(dp[n−1][w],dp[n−1][w−W[n]+D[n],其中
n
n
n 表示进行第
n
n
n 件物品的选择,
w
w
w 表示当前背包剩余容量,
W
[
n
]
W[n]
W[n] 表示第
n
n
n 件物品的重量,
D
[
n
]
D[n]
D[n] 表示第
n
n
n 件物品的价值。
递归方法
写个dp函数套递归表达式即可,记得使用记忆化搜索,否则复杂度炸。
非递归方法
二维数组
- 二维数组的解法比较直观好理解,学习非递归dp可以从此入手。
- 考察递归表达式中
m
a
x
max
max 的两项,发现
d
p
[
n
]
[
w
]
dp[n][w]
dp[n][w] 只和其左下方的元素存在依赖关系(如图),因此只要保证对
n
n
n 从小到大遍历即可,至于
w
w
w 的遍历顺序,以及
n
n
n 和
w
w
w 谁在内层循环谁在外层循环的问题,其实不会影响结果,读者可以自行推导。
// 二维解法,非常直观
#include <bits/stdc++.h>
using namespace std;
const int N_MAX = 4000;
const int W_D_MAX = 400;
const int M_MAX = 13000;
int N, M;
int W[N_MAX], D[N_MAX];
int dp[N_MAX][M_MAX];
int main()
{
scanf("%d%d", &N, &M);
for (int i = 1; i <= N; i++) {
scanf("%d%d", &W[i], &D[i]);
}
// 递归式:dp[n][w] = max(dp[n-1][w], dp[n-1][w-W[n]]+D[n]);
// 第一种遍历策略:外层遍历w,内层遍历n,从小到大,注意判断w的合法性
// for (int w = 1; w <= M; w++) {
// for (int n = 1; n <= N; n++) {
// if (W[n] <= w) dp[n][w] = max(dp[n-1][w], dp[n-1][w-W[n]]+D[n]);
// else dp[n][w] = dp[n-1][w];
// }
// }
// 第二种遍历策略:外层遍历n,内层遍历w,从小到大,注意判断w的合法性
for (int n = 1; n <= N; n++) {
for (int w = M; w >= 1; w--) { // 可以正着遍历也可以逆着遍历,因为对本层无依赖关系
if (W[n] <= w) dp[n][w] = max(dp[n-1][w], dp[n-1][w-W[n]]+D[n]);
else dp[n][w] = dp[n-1][w];
}
}
printf("%d", dp[N][M]);
return 0;
}
滚动数组优化的二维解法
- 纯二维解法会MLE,因此需要优化。从上面的手画图可以看到,其实每个dp只对上一层n的数据有依赖关系,之前的层其实是冗余数据,即我只需要暂存上一层的数据即可,不难想到滚动数组。
- 滚动数组思想比较easy,本质是模运算,直接看代码即可。
// 二维解法的优化:滚动数组——dp的二维信息往往生命周期并不长,比如这一题只有相邻的(之前的一行)有用
#include <bits/stdc++.h>
using namespace std;
const int N_MAX = 4000;
const int W_D_MAX = 400;
const int M_MAX = 13000;
int N, M;
int W[N_MAX], D[N_MAX];
int dp[2][M_MAX]; // 滚动数组,注意滚动的是外层遍历
int main()
{
scanf("%d%d", &N, &M);
for (int i = 1; i <= N; i++) {
scanf("%d%d", &W[i], &D[i]);
}
for (int n = 1; n <= N; n++) {
for (int w = 1; w <= M; w++) {
if (W[n] <= w) dp[n%2][w] = max(dp[(n-1)%2][w], dp[(n-1)%2][w-W[n]]+D[n]);
else dp[n%2][w] = dp[(n-1)%2][w];
}
}
printf("%d", dp[N%2][M]);
return 0;
}
一维数组方法
- 进一步思考,发现其实也不需要多存一层数组,只要计算顺序合适,可以只用一维数组完成遍历
- 这时候就需要留意 w w w 的依赖关系, w w w 依赖于上一层(n-1)比自己小的值,因此对 w w w 的遍历需要从大到小
#include <bits/stdc++.h>
using namespace std;
const int N_MAX = 4000;
const int W_D_MAX = 400;
const int M_MAX = 13000;
int N, M;
int W[N_MAX], D[N_MAX];
int dp[M_MAX]; // 滚动数组
int main()
{
scanf("%d%d", &N, &M);
for (int i = 1; i <= N; i++) {
scanf("%d%d", &W[i], &D[i]);
}
for (int n = 1; n <= N; n++) {
for (int w = M; w >= W[n]; w--) { // 注意依赖关系:依赖上一层(n-1)的自己和上一层比自己小的,因此需要从大到小遍历
dp[w] = max(dp[w], dp[w-W[n]]+D[n]);
}
}
printf("%d", dp[M]);
return 0;
}