《算法竞赛进阶指南》0x59单调队列优化DP AcWing 299裁剪序列

题目链接:https://www.acwing.com/video/864/

给定一个长度为n的序列,问将这个序列分成连续的若干段,每段不超过M的情况下,每段的最大值之和最小是多少?

如果数据范围比较小的话就可以不进行任何优化,用dp[i]表示将[1,i]分成若干段满足条件的情况下每段最大值最小的情况,那么dp[j]就容易从dp[i]进行转移,但是转移中存在一个max函数,不是单调的。

所以需要对决策集合进行考虑。

复杂度较高的情况:

第一次筛选:找到最小的j使得[j+1,i]之间的和是不超过M的。

第二次筛选:在左边决策变量j对应的值不超过右边序列的最大值的情况下尽量向左延伸。因为这样右侧的最大值不会发生变化,但是左侧的最大值和不会变大,所以总的最大值之和不会变大。容易知道这种情况下A[j]就是[j,i]序列的最大值。所以我们需要维护的就是满足这样一个性质的序列,如果有一个决策点j1和j2,满足j1<j2而且a[j1]<=a[j2],那么显然可以再向左延伸而不会使结果变的更差。

由于计算的是f[j]+max{A[k]}(j+1<=k<=i),不满足单调性,因此上述每一个决策j都需要进行计算,从中找到最小的就是f[i]。

通过单调队列就能维护这样一个决策变量j的可能取值,而f[i]最初的取值是最左端j对应的f[j]+q[l],其中q[l]是单调队列中的首元,也就是[j+1,i]中最大值对应的下标。

容易知道,对每个q[k],下一个数q[k+1]就是q[k]之后最大的数对应的下标。

证明:假设q[k]右侧的A的最大值对应的索引为s,如果A[s]<=A[q[k+1]],条件满足,如果s>q[k+1],根据单调队列的性质,应该去除q[k+1]点,并且加入s,但是队列当前已经满足性质,故s点一定是q[k+1]。

证毕。

该算法保证每个决策在队列中只被扫描一遍,但最坏的情况下可以达到O(n^2)。通过堆优化能保持O(nlogn)时间复杂度。堆用来在对数时间内查找最小值,要满足插入和删除操作,其中删除操作需要谨慎处理。

代码1:POJ能过3017

 

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<set>
using namespace std;
const int N = 100010;
typedef long long ll;
int n;
ll m;
ll a[N];
ll q[N];
ll f[N];
int main(){
    scanf("%d%lld",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
        if(a[i]>m){
            puts("-1");
            return 0;
        }
    }
    int l=0,r=0;
    ll sum = 0;
    for(int i=1,j=0;i<=n;i++){
        sum+=a[i];
        while(sum>m)sum-=a[++j];//初步筛选所有可能的j 
        while(l<=r && q[l]<=j)l++;
        while(l<=r && a[q[r]]<=a[i])r--;
        q[++r]=i;//二次筛选所有可能的j 
        
        f[i]=f[j]+a[q[l]];//第二种情况或者第一种情况中的边界 
        for(int k=l;k<r;k++)
            f[i]=min(f[i],f[q[k]]+a[q[k+1]]);
    }
    
    cout<<f[n]<<endl;
    
    return 0;
}

 

 

posted @ 2020-08-03 10:51  WA自动机~  阅读(183)  评论(0编辑  收藏  举报