CF1661D Progressions Covering 题解
前言
本篇题解亮点在于解释如何一遍从后往前扫一边双重差分。
解法
贪心,从右往左依次满足每个数。至于贪心的正确性其他题解已经讲得很清楚了,不再赘述。 而题目中将数组 $a$ 加到 $a_i\ge b_i$ 等价于把 $b_i+\Delta b_i\le0$。
重点是如何维护贪心过程中进行的区间减等差数列。 暴力减等差数列十分麻烦,有没有替代的方法?
先看这张图,数组 $\Delta b$ 初始时全是 $0$,我们减去一个等差数列(用大括号括起来的)后变成:$$ \cdots~0~\overbrace{-d~{-2d}~{-3d} ~{-4d}}^{[l,i]}~0\cdots $$
对它进行差分两次(从右往左):
下标 | $l-2$ | $l-1$ | $l$ | $l+1$ | $i-1$ | $i$ | $i+1$ |
---|---|---|---|---|---|---|---|
数组 $\Delta b$ | $0$ | $0$ | $-d$ | $-2d$ | $-3d$ | $-4d$ | $0$ |
差分数组 | $0$ | $+d$ | $+d$ | $+d$ | $+d$ | $-4d$ | $0$ |
双重差分数组 $c$ | $-d$ | $0$ | $0$ | $0$ | $+5d$ | $-4d$ | $0$ |
我们发现,只要在 $c$ 上标记 $3$ 个位置即可:$l-2,i-1,i$。 我们可以用 $c$ 对应 $\Delta b$,需要 $\Delta b$ 时进行对 $c$ 进行两次前缀和还原出 $\Delta b$。
但是麻烦还没完,我们要一边维护 $c$,一边算出 $\Delta b_i$ 的值。
将操作分为三部分:
- 进行还原,通过 $c$ 算出 $\Delta b_i$,即代码中的
sum2
。 - 计算 $b_i+\Delta b_i$ 和应该贪心地减去几个等差数列。
- 如果要减,那么对 $c$ 进行标记,表示出减去的等差数列,并修正一些中间变量如代码中的
sum1
和sum2
。因为如果要减,就要更新 $c_i$,而在第 $1$ 步中根据 $c_i$ 算的sum1
和sum2
(即 $\Delta b$)也要修正。
具体见代码。
代码
#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const int N=300005;
int n,k;
ll ans,sum1,sum2;
ll b[N],c[N];// 原数组和双重差分数组。注意双重差分数组两次前缀和(“解码”)后
// 得出的是原数组要减去的值(即差值)。
int main()
{
scanf("%d %d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%lld",b+i);
}
for(int i=n;i>0;i--)// 从后往前扫
{
// 还原
sum1+=c[i];// 一次前缀和
sum2+=sum1;// 两次前缀和
// 计算要减去多少个等差数列(用 d 表示)。注意不要越界。
ll l=max(1,i-k+1),len=i-l+1,d=(b[i]+sum2+len-1)/len;
if(d>0)// 如果要减
{
ans+=d;// 记录答案
// 标记 c 数组。
if(l-2>0)
{
c[l-2]-=d;
}
c[i-1]+=(len+1)*d;
// 修正两个前缀和。可以看上面的表格理解。
sum1-=len*d;
sum2-=len*d;
}
}
printf("%lld",ans);
return 0;
}