BZOJ2086 POI2010 Blocks


POI2010 题解整理

Description

给出 N 个正整数a[1N],再给出一个正整数 k ,现在可以进行如下操作:每次选择一个大于k的正整数 ai ,将 ai -1,选择 ai1 ai+1 中的一个+1。

经过一定次数的操作后,问最大能够选出多长的一个连续子序列,使得这个子序列的每个数都不小于 k 。总共给出M次询问,每次询问给出的 k 不同,你需要分别回答。

Input

  • 第一行两个正整数N(N106) M(M50)

    • 第二行 N 个正整数,第i个正整数表示 ai(ai109)
    • 第三行 M 个正整数,第i个正整数表示第i次询问的 k(k109)

Output

  • 共一行,输出M个正整数,第i个数表示第i次询问的答案。

Sample Input

5 6
1 2 1 1 5
1 2 3 4 5 6

Sample Output

5 5 2 1 1 0


Solution

这道题目和牛宫一题非常像。

首先要得到这条式子: ans=max{RL:Ri=1aiLj=1aj(RL)k}

暂且设 sumi=ij=1ajik ,则对于每一个 R ,要找[0,R1]内最小的 L ,并且满足sumRsumL

有一个性质:

  • 对于两个点 k,j(k<j) ,如果 sumksumj ,那么这个j是一定用不到的。

所以我们可以维护一个单调栈,当满足 sumstk[top]>sumi 的时候才将i放入。那么我们就会发现这个单调栈呈现这样一种情况:下标在不断增大,但是 sumpos 在不断减小。于是我们找到栈中最小的 pos ,且满足 sumpossumi 即可。

#include<bits/stdc++.h>
#define M 1000005
using namespace std;
long long sum[M];
inline void Rd(int &res){
    res=0;char c;
    while(c=getchar(),c<48);
    do res=(res<<3)+(res<<1)+(c^48);
    while(c=getchar(),c>47);
}
int stk[M],top=0;
int main(){
    int n,m;Rd(n),Rd(m);
    for(int i=1,x;i<=n;i++){
        Rd(x);sum[i]=sum[i-1]+x;
    }
    for(int _=1;_<=m;++_){
        int k;Rd(k);
        top=0;
        int ans=0;
        stk[++top]=0;
        for(int i=1;i<=n;i++){
            if(sum[stk[top]]-1LL*stk[top]*k>sum[i]-1LL*i*k)stk[++top]=i;
            else{
                int L=1,R=top,res=-1;
                while(L<=R){
                    int mid=L+R>>1;
                    if(sum[stk[mid]]-1LL*stk[mid]*k<=sum[i]-1LL*i*k){
                        R=mid-1;
                        res=mid;
                    }else L=mid+1;
                }
                if(~res)ans=max(ans,i-stk[res]);
            }
        }
        printf("%d%c",ans,_==m?'\n':' ');
    }
}

(16/11/17更新)为了将二分的 O(logn) 复杂度转化为线性复杂度,我们需要回到最开始的模拟中——我们枚举当前的右端点,然后将左端点的位置不断向左滑动。由于我们不能确定,在当前的 sumL>sumR 的前方,是否有比 sumL 更小的值可以转移,所以单次询问我们暴力的复杂度为 O(N2)

但是对于每个位置,前面最小的值可以利用动态规划在 O(n) 时间内计算。于是我们上述缺陷的判断就可以被填补,我们在滑动L的时候,只要判断前面最小的值是否 sumR 即可。不能滑动时,我们不必撤销当前的最优解并且下一轮从头滑动L,我们保持这个区间向右滑动即可。这样此处均摊的复杂度仍为 O(n)

综上,最后总复杂度为 O(nm)

#include<bits/stdc++.h>
#define M 1000005
using namespace std;
long long sum[M];
inline void Rd(int &res){
    res=0;char c;
    while(c=getchar(),c<48);
    do res=(res<<3)+(res<<1)+(c^48);
    while(c=getchar(),c>47);
}
int Min[M];
int main(){
    int n,m;Rd(n),Rd(m);
    for(int i=1,x;i<=n;i++){
        Rd(x);
        sum[i]=sum[i-1]+x;  
    }
    for(int _=1;_<=m;++_){
        int k;Rd(k);
        int ans=0;
        for(int i=1;i<=n;i++)
            if(sum[Min[i-1]]-1ll*Min[i-1]*k<=sum[i]-1ll*i*k)Min[i]=Min[i-1];
            else Min[i]=i;
        for(int i=1;i<=n;i++)
            while(i-ans>=0&&sum[Min[i-ans]]-1ll*Min[i-ans]*k<=sum[i]-1ll*i*k)ans++;
        printf("%d%c",ans-1,_==m?'\n':' ');
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值