题解: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;
}

posted @ 2026-01-15 08:18  Zwi  阅读(3)  评论(0)    收藏  举报