题解:P6631 [ZJOI2020] 序列
posted on 2025-02-21 11:17:36 | under | source
闲话:初看此题,以为是神秘 dp,感觉很难优化。看了贪心解法后大受震撼,真的很难想啊。
题意:给定序列 \(a_1\dots a_n\),有两种操作:区间减一、区间奇数位置或偶数位置减一。求消为 \(0\) 最小次数。\(n\le 10^5\)。
记操作一为“直线”、操作二为“跳线”。
考虑增量法构造。不妨先考虑消掉 \(a_1\):
- \(a_2=0\):只能用跳线。
- \(a_2>0\):用直线覆盖 \(a_1,a_2\) 直到归约为 \(a_2=0\)。原因也很简单,假如没有一条直线覆盖 \(a_1,a_2\),那么必然是若干跳线,注意到两条交织的跳线等价于一条直线加一条跳线,所以先用直线不劣。
对于一般情况,额外考虑一下前面接过来的线。记 \(z\) 为直线,\(t_0\) 为可跳到 \(i\) 的跳线,\(t_1\) 反之。
分讨一下:
- \(a_i\ge z+t_0\):直接全部保留即可。
- \(a_i<z+t_0\):必须放弃 \(k=z+t_0-a_i\) 条线。不好决定放弃哪些种类怎么办?类似反悔,令 \(z,t_0\) 均减去 \(k\),新增 \(k\) 条免费线即可。当 \(z<k\) 时必然 \(t_0\) 要舍弃 \(k-z\) 条,直接丢掉即可。所以不会出现负数情况。
最后的问题是怎么处理新增的免费线?让 \(ans\) 减去 \(k\),但是万一线没用完呢?可以令 \(a_i=k\),这样强制新增 \(k\) 条线。
\(O(n)\)。
代码
枚举 \((i-1,i)\),将 \(a_{i-1}\) 清空,分讨然后推到 \((i,i+1)\)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 5;
int T, n, a[N];
signed main(){
cin >> T;
while(T--){
scanf("%lld", &n);
for(int i = 1; i <= n; ++i) scanf("%lld", &a[i]);
a[n + 1] = 0;
int z = 0, t0 = 0, t1 = 0, ans = 0, free;
for(int i = 2; i <= n + 1; ++i){
free = 0;
if(z + t0 > a[i]){
int k = z + t0 - a[i];
if(k > z) t0 -= k - z, k -= k - z;
if(k > t0) z -= k - t0, k -= k - t0;
z -= k, t0 -= k, free = k, a[i] = 0;
}
else a[i] -= z + t0;
int add_z = min(a[i - 1], a[i]);
z += add_z, ans += add_z, a[i - 1] -= add_z, a[i] -= add_z;
t1 += a[i - 1], ans += a[i - 1], a[i - 1] = 0;
a[i] += free, ans -= free;
swap(t0, t1);
}
printf("%lld\n", ans);
}
return 0;
}

浙公网安备 33010602011771号