zoukankan      html  css  js  c++  java
  • [POI2010]KLO-Blocks——一道值得思考的题

    题目大意:

    给出N个正整数a[1..N],再给出一个正整数k,现在可以进行如下操作:每次选择一个大于k的正整数a[i],将a[i]减去1,选择a[i-1]或a[i+1]中的一个加上1。经过一定次数的操作后,问最大能够选出多长的一个连续子序列,使得这个子序列的每个数都不小于k。M组询问,每组给出一个k

    n<=1000000,m<=50,xi,k<=1e8

    分析:

    乍一看,有一些选择性,还有序列,像是dp,但是对于动规,因为可以经过无数次操作,而且数的范围也不小,完全没有想法。

    所以这个时候,我们要再分析一下题意。

    发现,无论怎么加减,这个序列的总和是不变的。而且,我们可以随意地进行无数次加减,可以将所有的高出k的数都可以填到低于k的数。

    又发现,最终的合法子序列,就是这个序列区间内,每个数都要大于等于k,也就是说,这个区间的平均值必须大于等于k。

    所以,我们用前缀和sum[i],表示原数列中前i个元素的和。

    这样,我们就相当于枚举点对i,j,j<=i,使得在sum[i]-sum[j]>=k*(i-j)的情况下,i-j最大。(这是等价的,因为无论如何,我们会有一个子区间内的总和是不变的。所以可以采用原始的序列进行操作)

    又,为了避免麻烦,我们可以将所有的数都减去k,这样就转化成了,找到最大的i-j,满足sum[i]-sum[j]>=0;

    思路一:

    枚举i,j点对。复杂度O(m*n^2) 直接慢的飞起。

    思路二:

    考虑优化,线性的东西,往往要考虑单调栈,或者是单调队列。

    发现,对于一个枚举的右端点i,我们要枚举左端点1~i,之后还要重复枚举,只为了取max。是否有单调性?

    发现,我们这里要考虑两个方面,一个是sum[i]-sum[j]>=0 一个是:i-j最大。所以从左端点的位置,和它的sum值关系入手。

    发现,当k<j,且sum[k]<sum[j]时,k作为左端点,一定比j作为左端点更优,这样的j就可以扔掉。

    于是,我们可以维护一个单调栈,正序加入,它从栈低到栈顶记录可能成为答案的左端点编号,从下往上,编号大小递增,sum值递减。

    这样对于一个新枚举的i,我们要找到sum[i]-sum[j]>=0且最小的j,栈内二分即可。

    复杂度O(m*n*logn) 可惜还是差一点。

    思路三:

    继续优化思路二。m、n看来是不太好优化了,logn的二分是否可以去掉呢?

    发现,即使思路二用了单调栈,但是我们有的时候,还是会用一些“其实一眼就可以看出来的”不能可能成为答案的右端点来进行二分。

    什么意思呢?

    我们必须考虑右端点枚举的方式,是否也有同样的单调性??

    发现,如果,j<i,并且sum[j]<sum[i],那么,j肯定也不会参与答案。

    嗯,,,两个单调性,,,

    我们可以开两个栈!!

    除了上一个栈,我们还可以开一个储存可能成为右端点的栈。

    其中,这个栈的更新,正序循环预处理。和上一个栈一样,从下到上,编号单增,sum单减。

    我们同理,正序循环预处理第一个左端点栈。

    然后进行答案选择,取出右端点的栈顶,不断pop左栈,直到sum[sta1[top1]]>sum[sta2[top2]],刚才最后pop的元素就可能成为答案。

    至于为什么能pop,因为后面的右栈中的sum越来越大,而要使左端点越小越好,所以它们一定能过sum小的那些关,就直接跳过了。

    思路四:

    其实不用那么麻烦开两个栈。

    我们初始化左端点栈之后,倒序循环右端点,同上不断pop,最后pop掉的左端点就用来尝试更新答案。

    每次当sum[右端点]是历史最大值的时候才操作。

    为什么是对的?

    因为,右端点即将变小,小了之后,必须sum更大才有可能继续突破更大的sum[左端点],,否则,i小了,sum不增大,答案必然不优。

    这样就避免了两个栈的麻烦。

    注意:

    开long long

    代码:(代码好抄,思维难移)

    #include<bits/stdc++.h>
    using namespace std;
    const int N=1000000+10;
    const int M=55;
    typedef long long ll;
    int a[N];
    ll sum[N];
    int top,sta[N];
    int n,m;
    ll aver;
    void work(int k)
    {
        top=0;
        for(int i=1;i<=n;i++)
        {
            sum[i]=sum[i-1]+a[i]-k;
            if(!top||sum[sta[top]]>sum[i]){
                sta[++top]=i;
            }
        }//预处理左端点 
        ll ans=0,mx=-2e18-1;
        for(int i=n;i>=1;i--){
            if(sum[i]>=0){
                ans=max(ans,(ll)i);
            }
            if(sum[i]>mx){
            for(;top&&sum[i]-sum[sta[top]]>=0;top--){
                ans=max(ans,(ll)i-sta[top]);
            }
             mx=sum[i];
            }
        }//倒序枚举右端点,尝试产生答案 
        printf("%lld ",ans);
    }
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++) {
            scanf("%d",&a[i]),aver+=a[i];
        }
        aver/=n;
        int k;
        for(int i=1;i<=m;i++){
            scanf("%d",&k);
            if(k<=aver){
                printf("%d ",n);continue;
            }//这里是个优化,如果k<=全集区间平均值,那么答案必然是n 
            work(k);
        }
    }

    总结:

    1.对于一个题目,我们应该去先想这是什么类型的题,但是更关键的是,挖掘题目中的隐含信息,比如这个题的平均值。可谓最厉害的算法,就是对症下药。

    2.对于线性问题的优化,可以想到的有:单调队列,单调栈,斜率优化,分块,莫队,决策单调性(不会),四边形不等式(??),可行性dp转化为最优性dp,前缀和也算。

    3.如果一步优化不完,一般不要轻易放弃这个思路,毕竟好题不是那么直白。考虑针对复杂度的哪个局限部位进行优化。继续挖掘信息。

    4.尽量深刻理解算法本质,比如这个题,就很好的用到了两次单调栈的特点。

  • 相关阅读:
    贝尔级数
    NOIP2018 退役记
    Codeforces1106F 【BSGS】【矩阵快速幂】【exgcd】
    codeforces1111 简单题【DE】简要题解
    BZOJ4836: [Lydsy1704月赛]二元运算【分治FFT】【卡常(没卡过)】
    BZOJ3771: Triple【生成函数】
    Codeforces 1096G. Lucky Tickets【生成函数】
    Codeforces1099F. Cookies【DP】【线段树】【贪心】【博弈】【沙比提(这是啥算法)】
    Codeforces gym101955 A【树形dp】
    BZOJ3551: [ONTAK2010]Peaks加强版【Kruskal重构树】【主席树】
  • 原文地址:https://www.cnblogs.com/Miracevin/p/9097901.html
Copyright © 2011-2022 走看看