你说得对,但是 CF 标签就图一乐
声明:本题解中认为各个变量及 dp 数组属于代码片段,所以用代码块包裹且不使用 latex。
好的。首先我们应该先看题。
题意
对题意可能会出现的一个误解是:认为一次检查失败,整个过程立刻停止,然后使用贪心。手摸样例就可以发现这个错误。
正确的题意是:任意检查失败之后依然可以继续获取后面的点数和进行检查,仅仅是这一次检查不计入通过的检查总数。
然后,想想怎么做。
思路 && 算法选择
对于这种题,我们一时难以想到一个最优的分配点数的策略,无法使用贪心。
对于这种复杂的多阶段决策,一般可以考虑动态规划。
一个简单的想法是,设 dp[i][j] 为现在已经进行了前 i 个操作,将 j 个点数分配在智力(也可以是力量,但是这里以智力为例)上通过的最多测试数目。
考虑状态转移方程。
显然的,对于一次获得点数的操作,立刻分配点数一定不劣于之后分配点数。而分配点数可以分到智力或力量上,我们可以取最大值。
于是就有这个转移方程:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]);
//when r[i] == 0
而当操作是一次检测时,分配了足够点数的状态通过的检测次数会加一。
方程是这个:(以智力检测为例,力量检测同理)
//cnt 是之前获得的总点数
for (int j = r[i]; j <= cnt; ++j) {
dp[i][j] = dp[i - 1][j] + 1;
}
//when r[i] > 0
上面的做法显然是无法通过的,时间和空间都会超限,我们需要优化。
空间优化
空间的优化是平凡的。用滚动数组或降维优化可以轻易地优化到
注意降维优化时 r[i] == 0 的操作 j 的枚举要反向(类似于 01 背包),否则前面的转移会改掉后面转移依赖的数据,就会 WA 掉。
然后,我们就要考虑优化时间。
经典错解一则
如果你像我一样没有完全想明白状态转移方程就动手,容易忽略获得点数时也要进行状态转移,然后就会想到用树状数组优化。
但是这是错的。树状数组无法简单的维护 chechmax 操作。吉司机线段树属于山本
真正的优化
注意到 m 的数据范围很小。所以获得点数的操作数量也很少。对于这种操作,我们可以使用
对于另外两种操作,使用差分维护即可。在获得点数的操作时,先将差分推平算出原数组,然后暴力做即可。
代码
#include <bits/stdc++.h>
using namespace std;
int dp[5005], d[5005], n, m;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
fill(dp + 1, dp + m + 1, -1e9);
for(int i = 1, s = 0; i <= n; ++i) {
int x;
cin >> x;
if (x > 0) {
if (x <= s) {
++d[x], --d[s + 1];
}
} else if(x < 0) {
if (0 <= s + x) {
++d[0], --d[s + x + 1];
}
} else {
++s;
dp[0] += d[0];
for (int j = 1; j <= s; ++j) {
d[j] += d[j - 1];
dp[j] += d[j];
}
for (int j = s; j; --j) {
dp[j] = max(dp[j], dp[j - 1]);
}
fill(d, d + s + 2, 0);
}
}
dp[0] += d[0];
for(int j = 1; j <= m; ++j) {
d[j] += d[j - 1];
dp[j] += d[j];
}
cout << *max_element(dp, dp + m + 1);
return 0;
}