题解:P11069 「QMSOI R1」 生熏鱼
本题解把本人思考抄题解后复盘思路的过程完整呈现了出来,希望能对大家有帮助。
题目大意
有
不管题目怎么复杂,只要是有“物品”“背包容量”的,全部转换成背包问题的形式化题面,对思考一定有帮助。
状态定义
题目翻译到这里,这题肯定是背包 DP 了。那么怎么 DP 呢?初次看到这种按顺序取的问题,我们肯定是按照常规背包思路,一次 DP 直接出答案。常规地设
- 如果把
dp 数组与n 扯上关系,那么就设dp_j 为丢弃j 类物品的第一个后的最大价值。照着这条思路下去,好像跟算法标签里的“前缀和”“二分”对应上了,我们把a_{c_i} 和b_{c_i} 做前缀和,每次通过二分判断在哪个位置背包满……经过尝试,连个合理的状态转移都写不出来,于是放弃该方案。 - 如果把
dp 数组与k 扯上关系,那么设什么呢?设dp_j 为丢弃j 个物品后的最大价值?不就跟上一种情况一致了吗?设dp_j 为取到第j 个物品的最大价值?这样就有后效性了,因为我们并不知道前面已经扔掉了哪些物品,而且也写不出状态转移,于是放弃该方案。
至此,常规方法已经全部尝试完了,依旧毫无结果。于是我们把目光转向数组的数据范围:
看着这不大的
状态转移
本题的一个难点就是状态的定义,状态转移倒不是很难。假设现在正在取第
这里的数组名有点多,如果一遍没看懂可以重温一下题目里的定义。
程序过程
现在有了这个
接下来根据贪心,如果背包容量足够,我们就无脑取当前物品,直到背包剩余容量(假设为
代码实现与细节
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int M = 1e9, C = 1e5+5, N = 30, K = 2e7+5, inf = 0x3f3f3f3f3f3f3f3f;
int n, m, k, s, a[N], b[N], c[K];
bool vis[N];
int dp[N*C], mn[N*C], ans;
signed main()
{
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m >> k >> s;
mt19937 rand(s);
for (int i = 1; i <= n; i++) a[i] = rand() % M + 1, b[i] = rand() % C + 1;
for (int i = 1; i <= k; i++) c[i] = rand() % n + 1;
memset(dp, 0x3f, sizeof dp);
dp[0] = 0;
memset(mn, 0x3f, sizeof mn);
int now = m; // 当前剩余容量
int sum = 0; // 当前最多可能省出的容量,即 MaxJ
int man = 0; // 当前价值之和
for (int i = 1; i <= k; i++) {
man += a[c[i]];
if (!vis[c[i]]) {
sum += b[c[i]];
vis[c[i]] = true;
for (int j = sum; j >= b[c[i]]; j--) {
dp[j] = min(dp[j], dp[j - b[c[i]]] + a[c[i]]);
}
for (int j = sum; j >= 0; j--) { // 后缀最小值
mn[j] = min(mn[j + 1], dp[j]);
}
}
int tmp = man;
if (now <= 0) { // 需要丢掉物品
int l = 1 - now; // 至少要丢掉多少容量的物品
if (l <= sum && mn[l] < inf) { // 如果能丢掉这么多
tmp -= mn[l]; // 损失价值
} else { // 没有足够的物品来丢,结束
break;
}
}
now -= b[c[i]];
ans = max(ans, tmp); // 现在的 tmp 就是丢掉物品过后剩下的价值
}
cout << ans;
return 0;
}
- 注意,这个如果
now\le0 就丢东西的过程是每次都要做的,而且丢完以后不用把剩余背包容量变为正数。这样每次算出来的tmp 就是只看前i 个物品的最大价值。换个方面想,如果每次都要改背包容量,那就会产生后效性,即后面丢物品的时候不知道之前有没有丢过这个物品,DP 的正确性就失效了。 - 记得开
long long。 - 边界是
dp_0=0 ,其他的dp_i=+\infty ,因为求的是最小值。
AC 记录,完结撒花!如果这篇题解对你有帮助,或者你觉得讲得很详细,那就给一个免费的赞吧!
管理员大大求通过!