ZOJ 3740 —— Water Level(DP+线段树)

12 篇文章 0 订阅

题目链接

题目的大概意思是,首先给一个正整数N,给一个序列ai,这个序列共N个数字,并且每个数字都在[-N,N]这个区间上。

一次针对ai的操作的作用是,使得ai到aN所有数字都加上c,c是任意并且可正可负。

问最多执行两次操作,也可以不操作,操作的对象也是任意的,问这个序列最多可以有多少个数字落在[1,N]这个区间上。


先说明我的做法的复杂度是O(N*N*logN),跑了1700+MS水过;

不知道跑了200+MS的大牛是怎么做的,Orz


为了方便计算,我在输入的时候先将每个数字加上N+1,这样数字的范围就变成了[1,2N+1]。

对于不操作的情况,那就是直接统计。

只操作一次的话,就是分两段,前面一段没改变的和后面一段被改变的,然后枚举断点取最大值即可。

而操作两次,设两次操作的位置分别是i和j,i<j,因为c是可以任意的,那么无论前面i操作的c是多少,j位置的c都可以根据前面的情况进行调整。

所以这种情况分为三段:前面未被改变的 、 ai到aj-1 和 aj到aN。然后还是枚举断点取最大值。

剩下的问题就是怎么求某个区间的最优值。

因为一次操作使得某个区间上的数字的改变量是一样的,我们的目标是使得较多的数字落在[1,N]上,其实也就是把ai到aj上的数字全部放到数轴上,然后在数轴上找一段长度为N-1的,并且落在上面的数字最多,通过c使得它平移到跟[1,N]重合。

这里我用线段树做统计。

解释下线段树中结点o的定义:

l[o]和r[o],是区间的左右端点;

这里要解释的是,对于这里的线段上的点,代表的是一个长度为N-1的区间。

比如N=2的时候,经过前面的修正,ai的范围应该是[1,5],假设当前数字是1和4

那么,我们要找的答案的范围应该就是[1,2]、[2,3]、[3,4]和[4,5]这样四个区间,这四个区间对应的个数就是1,0,1,1;

取它们的左端点的值1,2,3,4作为端点保存到线段树中,这样就可以通过左端点记录对应的区间的个数,从而求最值,对应的线段树的结构如下,右边是对应结点的最大值:


s[o]保存当前结点的最值,f[o]是懒惰标记。

这样当插入一个新的ai时,因为ai可以落到很多个长度为N的区间上,比如增加一个数字是3,3落在[2,3]和[3,4]两个区间,相当于对应的起点都加1,这些起点还是连续的,这就是个区间修改问题(上面的例子就是线段树区间[2,3]加1),修改子节点维护好父节点信息,就可以直接从根节点取最大值。

只是我的做法必须枚举所有的i和j,

具体处理是,可以先从右边往左扫,可以先计算出所有的ai到aN的值,保存到dp[i]中。

然后就是for循环扫过去枚举中间的ai和aj,每次都固定ai,清空线段树之后,不断增加新的aj进去。

所以复杂度是O(N*N*logN),N只有3000,估着大概卡着时间能过吧,实际运行也差不多,2000MS的题目跑了1700+MS。高效的就不知道怎么写了。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define MAXN 3010
#define LEFT(o) ((o)<<1)
#define RIGHT(o) (((o)<<1)|1)
#define MID(a,b) (((a)+(b))>>1)
int n, i, j, ans, cur, re, dp[MAXN], a[MAXN], r[MAXN<<2], l[MAXN<<2], s[MAXN<<2], f[MAXN<<2];
inline void getnum(int& x){
    x=0;
    bool mk=0;
    char c=getchar();
    while(c<48 || c>57){
        if(c=='-') mk=1;
        c=getchar();
    }
    while(c>=48 && c<=57){
        x=x*10+c-48;
        c=getchar();
    }
    if(mk)  x=-x;
}
void build(int o, int ll, int rr){
    l[o]=ll; r[o]=rr;
    if(ll<rr){
        int m=MID(ll,rr);
        build(LEFT(o),ll,m);
        build(RIGHT(o),m+1,rr);
    }
}
void maintain(int o){
    s[o] = max(s[LEFT(o)], s[RIGHT(o)]);
}
void update(int o, int ll, int rr, int v){
    if(l[o]==ll && r[o]==rr){
        f[o]+=v;
        s[o]+=v;
    }
    else{
        int lc=LEFT(o), rc=RIGHT(o);
        int m=MID(l[o],r[o]);
        if(f[o]){
            update(lc,l[o],m,f[o]);
            update(rc,m+1,r[o],f[o]);
            f[o]=0;
        }
        if(rr<=m)   update(lc,ll,rr,v);
        else if(ll>m)   update(rc,ll,rr,v);
        else{
            update(lc,ll,m,v);
            update(rc,m+1,rr,v);
        }
        maintain(o);
    }
}
int main(){
    while(~scanf("%d", &n)){
        memset(s,0,sizeof(s));
        for(i=1; i<=n; i++){
            getnum(a[i]);
            a[i]+=n+1;
        }
        build(1,1,n+1);
        memset(s,0,sizeof(s));
        memset(f,0,sizeof(f));
        for(i=n; i>=1; i--){
            update(1, max(1,a[i]-n+1), min(n+1, a[i]),1);
            dp[i]=s[1];
        }
        re=0;
        ans = 0;
        for(i=1; i<=n; i++){
            memset(s,0,sizeof(s));
            memset(f,0,sizeof(f));
            ans = max(ans, re+dp[i]);//只操作一次
            for(j=i+1; j<=n; j++){//枚举操作两次
                update(1, max(1,a[j-1]-n+1), min(n+1, a[j-1]),1);
                ans = max(ans, re+dp[j]+s[1]);
            }
            re += (a[i]>n+1?1:0);
        }
        ans = max(ans, re);//不操作
        printf("%d\n", ans);
    }
    return 0;
}




评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值