upd
- 2023-10-3 对二次差分进行补充解释。感谢 pig_pig_ 的指正。
题意
给定长度为 n n n 的数组 a a a 和 b b b, a a a 数组初始为 0 0 0,每次可以执行一个操作:选定一段长度为 k k k 的区间,区间左端点大于 0 0 0,右端点小于等于 n n n,将区间内第一个元素加 1 1 1,第二个元素加 2 2 2,以此类推,给定 n n n,数组 b b b 和 k k k,求满足 a i ≥ b i a_i\ge b_i ai≥bi 的最小操作次数,其中 1 ≤ i ≤ n 1\le i\le n 1≤i≤n。
思路
考虑贪心算法。
如果 a i < b i a_i<b_i ai<bi,即没有满足条件,则最优的操作方案显然为将 a i a_i ai 放在长度为 k k k 的操作区间尽量末尾处。这样操作可以使操作区间产生的影响最大化,且 a i a_i ai 的增加程度最大化。
做法就出来了。考虑从后往前做,假设当前做到了第 i i i 个元素,且该元素没有满足条件, [ i + 1 , n ] [i+1,n] [i+1,n] 的元素都满足条件了。我们选择一个前面所说的最优区间进行操作,直到该元素满足条件。
维护区间加的过程考虑差分。我们将对于数组 a a a 的区间加转化为 对于数组 b b b 的区间减,当 b i ≤ 0 b_i\le 0 bi≤0 时满足题意,其中 1 ≤ i ≤ n 1\le i\le n 1≤i≤n 。
因为前后元素增加的量差总为 1 1 1,所以我们考虑将修改量数组进行二次差分:
-
假设当前增加量数组为
0 , 0 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 0 , 0 , 0 0,0,0,1,2,3,4,5,6,0,0,0 0,0,0,1,2,3,4,5,6,0,0,0 -
对原数组进行第一次差分,得到
0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , − 6 , 0 , 0 0,0,0,1,1,1,1,1,1,-6,0,0 0,0,0,1,1,1,1,1,1,−6,0,0 -
再进行一次差分,得到
0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , − 7 , 6 , 0 0,0,0,1,0,0,0,0,0,-7,6,0 0,0,0,1,0,0,0,0,0,−7,6,0 -
因为我们是从后往前做的, − 7 -7 −7 后的 6 6 6 不会对当前和还未被扫描的元素产生影响,所以我们不用太在意这个 6 6 6。
-
此时发现我们只需要修改两处二次差分后的增加量数组,就可以实现区间加公差为一的等差序列。
注意到我们是从后往前进行操作,所以我们可以动态的计算当前元素增加的量。
时间复杂度 O ( n ) O(n) O(n)。具体实现可以看代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = 3e5 + 5;
int n, k, a[maxn], c[maxn]; // c数组用于记录操作的次数
int now, sum; // now表示当前操作的次数,sum表示未执行操作的总次数(也就是对当前元素产生的影响大小)
int ans; // ans表示最终的答案
signed main(){
scanf("%lld%lld",&n,&k);
for(int i = 1; i <= n; i++) scanf("%lld",&a[i]);
for(int i = n; i >= 1; i --){ // 从后往前扫
int pos = max(i - k + 1ll, 1ll); // 操作区间的左端点
int len = i - pos + 1; // 操作区间的长度
a[i] -= sum;
int tot = (a[i] + len - 1) / len; // 还需要的操作次数
if(tot > 0){ // 如果仍需要操作
ans += tot;
sum += len * tot; // 更新操作的总次数
now += tot; // 更新当前操作的次数
c[pos] += tot; // 记录操作次数到c数组中
}
sum -= now; // 减去已经执行的操作的总次数
now -= c[i]; // 减去当前操作的次数
}
printf("%lld", ans);
}
- Codeforces 测评结果: