https://codeforces.com/contest/2025/problem/D
题目大意:n个操作(ri>0智力测试操作,ri=0发放属性点,ri<0力量测试操作),属性点发放操作为m个,问至多能通过过少个测试.
考虑一个朴素做法dp[i][j]表示前i个操作,智力点为j时能通过最多的测试.假设当前总属性点为s
- 如果ri==0,dp[i][j]=std::max(dp[i-1][j],dp[i-1][j-1]),这表示两个选择,当前是第i个操作,且为分配属性点操作,如果把当前属性点分配给智力,那么上一个智力应当是j-1(dp[i][j]规定了当前智力点为j,所以上一个它应当由j-1转移而来).如果把属性点分配给力量,智力属性点应当不变
- 如果ri>0,对于所有j>=ri都有dp[i][j]=dp[i-1][j]+1,否则为dp[i-1][j]
- 同理,力量测试也是,s-j>=|ri|时dp[i][j]=dp[i-1][j]+1,否则dp[i-1][j]
但是对于这道题来说,这样的做法不管是时间复杂度还是空间复杂度都是不被允许的.
不妨先降低空间复杂度,我们可以将dp前面一个维度删除掉,只保留后面一个维度,及dp[i]表示智力点为i时能通过最多的测试,新建一个f数组,f[i]用来记录到下一个加属性点操作的之前智力为i能通过多少测试的差分(只有这样才能在O1的时间复杂度之内完成增减的操作)
每次读入ri,如果ri不为0,这告诉我们的第一个信息是它是哪一种测试,第二个信息是当前属性点数固定,我们既知道是什么测试了又知道现在属性点有多少,还知道当前测试要求多少属性点,我们是不是知道哪些情况下可以通过这个测试,比如当前是智力测试,当分配给智力的属性点大于这个测试的要求就能通过吧.这些能通过测试的属性分配情况都是连起来的,我们可以直接用差分数组f在o1的时间复杂度内完成这个操作.
现在考虑ri为0的情况,在朴素做法中,我们分析了当ri为0的时候,我们要对dp[i-1][j]和dp[i-1][j-1]取max,而其他情况其实都是垂直向下递推的,现在我们只有一个数组dp[i],试想一下,如果正着递推会产生什么结果,
可以看到上面这张图,我们5=1,6=max(1,2),7=max(2,3),8=max(3,4),但是现在我们只有一个数组(当然你可以用两个数组),当我们求出6了以后,6是不是会覆盖掉2,那么再次求7的时候,就变成了max(6,3)这是不对的,我们想要的是max(2,3),怎么解决呢?我们可以倒着求,比如先求8=max(3,4),好了,现在4变成8了,然后在求7,可以发现7=max(2,3)它不会用到4,没有产生影响,所以我们倒着求就解决了这个问题.
下面贴出代码
cpp
using ll = long long;
//d[i][I]表示处理完前i个记录后,当前智力水平为I时的结果,力量水平S=P-I,P是总共获得的点数
//如果ri==0,dp[i][I]=std::max(dp[i-1][I-1],dp[i-1][I])
//如果最后一个记录ri>0,对于所有I>=ri的情况(智力指数大于ri的情况下)d[i][I]=d[i-1][I]+1,否则dp[i][I]=dp[i-1][I]
//
//如果ri<0对于所有I<=P+ri,dp[i][I]=dp[i-1][I]+1,否则dp[i][I]=dp[i-1][I]
//一共有n个记录,m次加点,dp[i]表示前i组记录的最大值,假设到第i组记录有j个属性点
//dp[i]应该表示智力点为i时的最大值即d[{1-n}][i]的最大值
//假设
//第二种情况和第三种情况是区间加法问题
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
//n个测试或给点,m个点
int n, m;
std::cin >> n >> m;
//
std::vector<int> dp {0};//dp[j]表示智力为j时获得的最大总点数
std::vector<int> f {0};//f作为辅助数组(差分数组)来记录懒标记,方便处理区间加法
//f的长度表示智力点数
int s = 0;//s表示点数
//输入每一个测试
for (int i = 0; i < n; i++) {
int r;
std::cin >> r;//输入测试
//如果是智力测试
if (r > 0) {
//
if (r < f.size()) {
f[r]++;//I<r的情况都不加,因为I=f.size()表示智力,智力如果小于测试点都过不了,所以加到大于测试点的地方
}
}//如果是力量测试
else if (r < 0) {
r = -r;//先换成正的
r = s - r;//现在是力量点数
//r是需要消耗的力量点数,那么s-r就是至少预留的力量点数
//这么思考,在[0,f.size()]之间的某个I,如果想要通过力量测试,那么s-I>=r,即s-r>=I,也就是I的最大取值就是s-r
//哦,f.size()是s的长度,所以r+1是不会超过数组长度的,
if (r >= 0) {
f[0]++;
f[r + 1]--;
}
}
else {
//如果智力点固定,那么能通过的测试数也是固定的,现在新增了一个属性点,那么就有两种情况了
//dp[j]表示dp[i][j]中取一个能通过点的最大情况
//如果是点数
//f[j]表示智力点j能通过的情况,
for (int j = 1; j <= s; j++) {
f[j] += f[j - 1];//将点数全部加起来
}
for (int j = 0; j <= s; j++) {
dp[j] += f[j];
}
dp.push_back(0);
s++;
for (int j = s; j >= 1; j--) {
//这个表示两种选择,当智力点为j时,要么从dp[i-1][j-1]转移过来,要么从将属性点加给力量则不转移
//
dp[j] = std::max(dp[j], dp[j - 1]);
}
f.assign(s + 1, 0);
}
}
for (int j = 1; j <= s; j++) {
f[j] += f[j - 1];
}
for (int j = 0; j <= s; j++) {
dp[j] += f[j];
}
int ans = *std::max_element(dp.begin(), dp.end());
std::cout << ans << "\n";
return 0;
}