P9744 「KDOI-06-S」消除序列

发布时间 2024-01-07 19:40:44作者: SunsetLake

CSP前的晚自习本来想打板子的,但是没有做题的欲望,就来写题解了。

小清新 dp。

思路

先观察每一个操作,发现操作一最特殊,思考下它有什么性质。如果我们进行了一次以上的操作一,一定不是最优的。因为每次都会把前 \(i\) 个数都变成零,重复之后就会覆盖原来的操作,回到第一次操作的初始状态,这样前 \(i\) 个数数中存在的一些 \(1\) 就全没了,一定不优。于是整个序列我们最多会进行一次操作一。

这样就得到了一个很直接的思路:枚举在哪个位置进行操作一,更新答案。复杂度 \(\mathcal{O(qn)}\)

然而 \(q,n\) 都到达了 \(5 \times 10^5\) 级别,不可取。注意到 \(\sum m \leq 5 \times 10^5\),于是可以考虑从 \(m\) 入手。

每次只有 \(m\) 个位置需要为 \(1\),我们可以只枚举需要为 \(1\) 的位置,因此可以考虑设计dp状态:\(f_i\) 表示操作完前 \(p_i\) 个数的最小代价。思考如何转移:因为有操作一的存在,所以可以在当前位置的前一位(它自己可以留到后面进行操作)执行操作一,让前面的数先全部变为 \(0\),再把需要填 \(1\) 的地方填上 \(1\)。同时 \(f_i\) 也可以从 \(f_{i-1}\) 转移过来,因为前 \(p_{i-1}\) 个位置已经操作完了,只需要把 \(p_{i-1}+1\)\(p_i-1\) 变成 \(0\) 就好了。

故:\(f_i = \min \{g_{p_i-1}+sum_{i-1},f_{i-1}+num_{p_i-1}-num_{p_{i-1}}\}\)

其中 \(sum_i\) 表示把 \(p\) 中前 \(i\) 个位置执行操作三以此变为 \(1\) 的代价,\(num_i\) 表示在序列 \(v\)只执行操作二把前 \(i\) 个数全部变为 \(0\) 的代价,\(g_i\) 表示在序列 \(v\) 中把前 \(i\) 个数全部变为 \(0\)最小代价。

\(sum\)\(num\) 都是好求的,直接前缀和即可。而 \(g\) 的话可以dp,要么直接操作一,要么先把前 \(i-1\) 个变为 \(0\),再操作二把第 \(i\) 个变为 \(0\)。故:\(g_i=\min \{a_i,g_{i-1}+b_i\}\)

同时还要注意,我们的 \(f_i\) 考虑的是操作完前 \(p_i\) 个数,只枚举了 \(p\) 中的下标。有可能 \(p_m\) 并没有到 \(v\) 序列的末尾,这会导致最后的那些 \(0\) 的代价并未计入答案,因此还需要往 \(p\) 的最后加入一个 \(n+1\) 以此保证答案正确性。

于是我们就在 \(\mathcal{O(\sum m)}\) 的时间内解决了此题。

code

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n,m,q;
int p[500005];
ll a[500005],b[500005],c[500005];
ll g[500005],f[500005];
ll sum[500005],num[500005];
void solve(){
	scanf("%d",&m);
	for(int i=1;i<=m;++i)scanf("%d",&p[i]),sum[i]=sum[i-1]+c[p[i]];//前缀和
	p[++m]=n+1;//保证答案正确
	for(int i=1;i<=m;++i)f[i]=min(g[p[i]-1]+sum[i-1],f[i-1]+num[p[i]-1]-num[p[i-1]]);
	printf("%lld\n",f[m]);
}
int main(){
	cin>>n;
	for(int i=1;i<=n;++i)scanf("%lld",&a[i]);
	for(int i=1;i<=n;++i)scanf("%lld",&b[i]);
	for(int i=1;i<=n;++i)g[i]=min(a[i],g[i-1]+b[i]);//dp预处理g
	for(int i=1;i<=n;++i)num[i]=num[i-1]+b[i];//前缀和
	for(int i=1;i<=n;++i)scanf("%lld",&c[i]);
	cin>>q;
	while(q--)solve();
	return 0;
}