POJ3709_K-Anonymous Sequence_双端队列优化dp

题意

  给出一个长度为 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值