题意
给出一个长度为 n 的非严格单调递增数列,每一次操作可以使数列中的任何一项 -1。问至少需要多少次操作,使得任意一项都与其他至少 k - 1 项相等。
思路
首先想到DP。
dp[i]:考虑前 i 项,满足要求的最小操作数。
dp[i] = min( dp[j] + sigma( dp[i] - dp[j] ) ) k <= j <= i
用这个式子,复杂度为 O(n^3),相当糟糕。
sigma 的部分实际上是部分和,可以通过预处理前缀和优化。
dp[i] = min( dp[j] + psum[i] - psum[j] - A[j] * (i - j) )
这样就把复杂度降成了 O(n^2)。然而还是过不了。
求 dp[i] 的过程中 psum[i] 是一个定值,可以把它提出来,并将式子作简单变换。
dp[i] = psum[i] + min( -A[j] * i + dp[j] - psum[j] + A[j] * j) )
可以看到剩余的式子实际上变成了一个关于 i 的线性函数。
f(x) = -A[j] * x + dp[j] - psum[j] + A[j] * j
dp[i] = psum[i] + min( f(i) )
也就是说,从若干条直线 f(x) 中找到 f(i) 最小的那一条。再注意到 A[i] 是非严格单调递增的,也就是说直线的斜率 -A[i] 是递减的。所以我们可以用一个双端队列维护最小的直线。
链接
http://poj.org/problem?id=3709
代码
#include<iostream>
#include<cstdio>
#include<deque>
using namespace std;
const int maxn = 5e5 + 10;
typedef long long LL;
int T;
int n, k;
LL A[maxn];
LL psum[maxn], dp[maxn];
int dequ[maxn];
bool check(int f1, int f2, int f3){
LL a1 = -A[f1], b1 = dp[f1] - psum[f1] + A[f1] * f1;
LL a2 = -A[f2], b2 = dp[f2] - psum[f2] + A[f2] * f2;
LL a3 = -A[f3], b3 = dp[f3] - psum[f3] + A[f3] * f3;
return (a2 - a1) * (b3 - b2) >= (b2 - b1) * (a3 - a2);
}
LL f(int j, int x){
return -A[j] * x + dp[j] - psum[j] + A[j] * j;
}
int main(){
scanf("%d", &T);
while(T--){
scanf("%d %d", &n, &k);
for(int i = 0; i < n; i++){
scanf("%lld", &A[i]);
}
for(int i = 0; i < n; i++){
psum[i + 1] = psum[i] + A[i];
}
//一开始把 0 直线加入双端队列
int s = 0, t = 1;
dequ[0] = 0;
dp[0] = 0;
for(int i = k; i <= n; i++){
//把 i - k 直线加入双端队列
if(i - k >= k){
while(s + 1 < t && check(dequ[t - 2], dequ[t - 1], i - k)) t--;
dequ[t++] = i - k;
}
//如果队首直线已经不是最小了,就把它删除
while(s + 1 < t && f(dequ[s], i) >= f(dequ[s + 1], i)) s++;
//计算dp[i]
dp[i] = psum[i] + f(dequ[s], i);
}
printf("%lld\n", dp[n]);
}
return 0;
}