题解:CF933E A Preponderant Reunion
题目链接:洛谷 Codeforces
感觉洛谷的题解都讲得不是很清楚(也可能是我太笨了看不懂。),我去 CF 翻了官方题解,又和 GPT 老师“讨论”了很久才理解了思路。因此我也来水写一篇题解。
思路
先考虑钦定一些位置最终为 \(0\),则一定有一种使答案最小的钦定方案满足:
-
任意两个相邻位置至少有一个为 \(0\)。这是题目条件。
-
不存在连续三个 \(0\)。因为是“钦定”,所以把中间位置的 \(0\) 去掉一定不劣。
这样就只有孤立的 \(0\) 和相邻两个 \(0\) 了,再考虑如何算这两种情况的代价:对于孤立的 \(0\),设位置为 \(i\),则至少要花 \(p_i\) 的代价减到 \(0\),而更多操作也不会造成改变,所以代价就是 \(p_i\);对于相邻两个 \(0\),设位置为 \(i\) 和 \(i+1\),先进行一次操作 \(i\) 一定是优的,因为这一次操作能同时让两个数相减,操作后小的数已经变成 \(0\),再把大的数和外面的消就行,总代价是 \(\max(p_i,p_{i+1})\)。
然后就可以 dp 了。设 \(f_i\) 表示考虑完 \(1\) 到 \(i\),且钦定 \(i\) 位置为 \(0\) 的最小代价。分别考虑 \(i\) 是孤立的 \(0\) 和 \(i-1,i\) 是相邻两个 \(0\) 的情况,转移如下:
初状态是 \(f_0=0\);不强制 \(p_n=0\),所以末状态不能为 \(f_n\),可以设 \(p_{n+1}=0\),则末状态就是 \(f_{n+1}\)。
在 dp 的同时记录每个状态从哪里转移过来,就可以从末状态回溯,找到所有被钦定的位置。找到位置后,先把相邻两个 \(0\) 一起做一次操作,再对每个 \(0\) 考虑能不能和左边的、右边的一起操作,即可得到操作序列。只在两个数都不为 \(0\) 时操作,就可以防止重复操作。
此时还有一个小小的问题:如果钦定了 \(i\) 位置为 \(0\),而 \(p_i>p_{i-1}+p_{i+1}\),那 \(p_i\) 永远无法被减成 \(0\)。但这其实不是问题,因为如果改成钦定 \(i-1\) 和 \(i+1\) 为 \(0\),总代价会减少,且还是符合题目条件,也就是说这种 \(i\) 肯定不会在最优解中被选到。
dp 和还原操作的时间复杂度都是 \(O(n)\)。
代码
理解了思路后,代码是很好写的。
#include <bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define int long long
const int N=3e5+5,P=1e9+7;
int n,a[N],f[N],fr[N];
signed main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
n++;
for(int i=1;i<=n;i++)
{
f[i]=f[max(0ll,i-2)]+a[i],fr[i]=max(0ll,i-2);
if(i>=3&&f[i-3]+max(a[i-1],a[i])<f[i])
f[i]=f[i-3]+max(a[i-1],a[i]),fr[i]=i-3;
}
vector<int> ans;
for(int i=n;i;i=fr[i])
{
int d;
d=min(a[i-1],a[i]);
if(d)
{
ans.push_back(i-1);
a[i-1]-=d;
a[i]-=d;
}
d=min(a[i],a[i+1]);
if(d)
{
ans.push_back(i);
a[i]-=d;
a[i+1]-=d;
}
if(fr[i]==i-3)
{
d=min(a[i-2],a[i-1]);
if(d)
{
ans.push_back(i-2);
a[i-2]-=d;
a[i-1]-=d;
}
}
}
printf("%lld\n",(int)ans.size());
for(int x:ans)
printf("%lld\n",x);
return 0;
}

浙公网安备 33010602011771号