7-3 拼题A打卡奖励
拼题 A 的教超搞打卡活动,指定了 N 张打卡卷,第 i 张打卡卷需要 mi 分钟做完,完成后可获得 ci 枚奖励的金币。活动规定每张打卡卷最多只能做一次,并且不允许提前交卷。活动总时长为 M 分钟。请你算出最多可以赢得多少枚金币?
输入格式:
输入首先在第一行中给出两个正整数 N(≤103) 和 M(≤365×24×60),分别对应打卡卷的数量和以“分钟”为单位的活动总时长(不超过一年)。随后一行给出 N 张打卡卷要花费的时间 mi(≤600),最后一行给出 N 张打卡卷对应的奖励金币数量 ci(≤30)。上述均为正整数,一行内的数字以空格分隔。
输出格式:
在一行中输出最多可以赢得的金币数量。
输入样例:
5 110
70 10 20 50 60
28 1 6 18 22
输出样例:
40
样例解释:
选择最后两张卷子,可以在 50+60=110 分钟内获得 18+22=40 枚金币。
思路
普通背包的话,\(dp[i][j]\)表示考虑前\(i\)个打卡卷,当时间为j时的最大价值,时间总长大概为5e4,而打卡卷数量为1e2, 复杂度上界\(\Theta(n^2)\) ≈ \(5 \times 10^{7}\) 会超时
我们可以换个思路,设\(dp[i][j]\)表示考虑前i个打卡卷,当价值为j时的最小时间,那么有以下等式(w[i]表示价值,v[i]表示容量):
\(dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i])\)
值得注意的地方:
-
考虑j的合法范围,应满足\(w[i] \le j \le tot\), 对于第i次循环(是否考虑将i放进背包)
-
考虑数组存不下,应该使用滚动数组,保证\(j-w[i]\)应该为\(i\)层的状态,所以我们从后向前枚举
-
dp数组不满足二段性在求的时候不可以二分找到最大价值,应考虑从最大奖励开始枚举,枚举到第一个便是答案
Code
#include <iostream>
#include <cstring>
using namespace std;
const int M = 525600, N = 3e4 + 10;
// dp[i][j] 为考虑前i个打卡卷,所需时间为j时的最大奖励数量 dp 物品和容量
// dp[i][j] dp 物品和价值
// dp[i][j] 表示考虑前i个物品,总价值为j时的最小时间
// dp[i][j] = min(dp[i-1][j], d[i][j-w[i]] + v[i])
int v[N], w[N], dp[N];
int main() {
int n, m, ans, tot = 0; cin >> n >> m;
for (int i = 1; i <= n; i ++) cin >> v[i];
for (int j = 1; j <= n; j ++) cin >> w[j], tot += w[j];
memset(dp, 0x3f, sizeof dp);
dp[0] = 0;
for (int i = 1; i <= n; i ++) {
for (int j = tot; j >= w[i]; j --) {
dp[j] = min(dp[j], dp[j-w[i]] + v[i]);
}
}
int k = 1;
while (dp[tot] > m) tot --;
cout << tot;
}