bzoj 1901: Zju2112 Dynamic Rankings(整体二分)

1901: Zju2112 Dynamic Rankings

Time Limit: 10 Sec  Memory Limit: 128 MB

Description

给定一个含有n个数的序列a[1],a[2],a[3]……a[n],程序必须回答这样的询问:对于给定的i,j,k,在a[i],a[i+1],a[i+2]……a[j]中第k小的数是多少(1≤k≤j-i+1),并且,你可以改变一些a[i]的值,改变后,程序还能针对改变后的a继续回答上面的问题。你需要编一个这样的程序,从输入文件中读入序列a,然后读入一系列的指令,包括询问指令和修改指令。对于每一个询问指令,你必须输出正确的回答。 第一行有两个正整数n(1≤n≤10000),m(1≤m≤10000)。分别表示序列的长度和指令的个数。第二行有n个数,表示a[1],a[2]……a[n],这些数都小于10^9。接下来的m行描述每条指令,每行的格式是下面两种格式中的一种。 Q i j k 或者 C i t Q i j k (i,j,k是数字,1≤i≤j≤n, 1≤k≤j-i+1)表示询问指令,询问a[i],a[i+1]……a[j]中第k小的数。C i t (1≤i≤n,0≤t≤10^9)表示把a[i]改变成为t。

Input

对于每一次询问,你都需要输出他的答案,每一个输出占单独的一行。

Output

Sample Input

5 3
3 2 1 4 7
Q 1 4 3
C 2 6
Q 2 5 3

Sample Output

3
6

HINT

20%的数据中,m,n≤100; 40%的数据中,m,n≤1000; 100%的数据中,m,n≤10000。

 

上图摘自 许昊然《浅谈数据结构题的几个非经典解法》http://www.docin.com/p-950607443.html

个人理解:

假设有一个操作,可以直接二分一个判定标准,这个标准仅适用于这一个操作

那么现在有一堆操作,也是直接二分判定标准

而二分出来的标准,如果只有一个操作,非A即B

对于一堆操作,每个操作对于判定标准也是非A即B

如果只有一个操作,那就可以直接舍去不合要求的那个

但一堆操作,会出现,操作1符合A,操作2符合B

所以,我们就根据这个判断标准,将操作序列划分为2组

注意:划分依据是操作对象相对于判断标准的关系

对于划分出来的2组,同样的方法可以继续二分

直至将判断标准划分至最底层

所以整体二分每次操作序列是原序列截取的一段,它保证了时间复杂度

 

一篇很好的CDQ分治、整体二分总结:http://blog.csdn.net/hbhcy98/article/details/50642773

本题:整体二分+树状数组

#include<cstdio>
using namespace std;
#define N 10001
int n,m,tot,t;
int ans[N*2],a[N],k[N*3];
int c[N];
struct node
{
    int posx,posy,key,kind,bl,cur;
}q[N*3],tmp1[N*3],tmp2[N*3];
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int y)
{
    while(x<=n)
    {
        c[x]+=y;
        x+=lowbit(x);
    }
}
int sum(int x)
{
    int b=0;
    while(x)
    {
        b+=c[x];
        x-=lowbit(x);
    }
    return b;
}
void solve(int head,int tail,int l,int r)
//当前二分到的答案区间[l,r] 
//[head,tail]是操作序列,包括对答案区间[l,r]有影响的修改操作  和  答案在[l,r]内的查询操作
//这里二分区间是[l,r],[head,tail]标记的是根据而分出的判断标准,划分出的区间范围 
{
    if(head>tail) return;
    if(l==r)
    {
        for(int i=head;i<=tail;i++) 
         if(q[i].kind==3) ans[q[i].bl]=l;
        return;
    }
    int mid=l+r>>1;//设定判定标准
//在本题中,判定标准=mid,那么判定标准对应区间为[1,mid],即实际的判定标准是区间[1,mid],这一点相当重要 
//统计符合标准的修改对各个操作的贡献 
    for(int i=head;i<=tail;i++)
    {
        if(q[i].kind==1&&q[i].key<=mid) add(q[i].posx,-1);
        else if(q[i].kind==2&&q[i].key<=mid) add(q[i].posx,1);
        else if(q[i].kind==3) k[i]=sum(q[i].posy)-sum(q[i].posx-1);
//这里用k数组记录的数据相当于前缀和 
    }
//整体二分每次对一段新的序列操作,这就需要清空一些数据
//本题要清空树状数组,据说下面这4行比memeset快 
    for(int i=head;i<=tail;i++) 
    {
        if(q[i].kind==1&&q[i].key<=mid) add(q[i].posx,1);
        else if(q[i].kind==2&&q[i].key<=mid) add(q[i].posx,-1);
    }
    int ll=0,rr=0;
    for(int i=head;i<=tail;i++)
    {
        if(q[i].kind==3)//查询操作 
        {
//如果累计贡献+当前贡献>=目标贡献,即当前判定标准过于宽松,那么就要缩小累计贡献范围,将它归入答案区间[l,mid]
//此时不更新累计贡献,保证当前贡献仍是[1,l-1]里的 
            if(q[i].cur+k[i]>=q[i].key) tmp1[++ll]=q[i];
//累计贡献+当前贡献<目标贡献,即当前判定标准不能满足要求,就要夸大范围,把它归入答案区间[mid+1,r]
//更新累计贡献,使当前贡献是[1,mid]里的 
            else 
            {
                q[i].cur+=k[i];
                tmp2[++rr]=q[i];
            }
        }
        else//修改操作 
        {
//因为二分的是答案(判定标准)
//所以直接根据修改后的值与判定标准的关系划分即可
//注意是修改后的值,即把位置h改为g,判断的是g,而不是位置h 
//为什么?

//如果g满足判定标准,那么他就已经统计了一次贡献,它对于答案区间[mid+1,r]没有意义了,
//想想判定标准实际是[1,mid],他一定会对[1,[mid+1......r]]产生贡献,这个贡献我们已经累计更新了cur 
            if(q[i].key<=mid) tmp1[++ll]=q[i];
//g不符合标准,意思是如果放宽标准可能会有贡献,而且这个贡献还没有统计过 
            else tmp2[++rr]=q[i];
        }
    }
    for(int i=1;i<=ll;i++) q[head+i-1]=tmp1[i];
    for(int i=1;i<=rr;i++) q[head+ll+i-1]=tmp2[i];
//都是开区间,所以要-1 
    solve(head,head+ll-1,l,mid);solve(head+ll,tail,mid+1,r);
//看完这儿,我们可以对tmp1,tmp2感性的认知 
//tmp1 满足要求或过于满足,要进一步限制 
//tmp2 不满足要求,放宽限制可能会满足要求 
}
int main()
{
    scanf("%d%d",&n,&m);
    int x,y,z;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&x);
        q[++tot]=(node){i,0,x,2};
        a[i]=x;
    }
    char ch[2];
    for(int i=1;i<=m;i++)
    {
        scanf("%s",ch);
        if(ch[0]=='Q') 
        {
            scanf("%d%d%d",&x,&y,&z);
            q[++tot]=(node){x,y,z,3,++t,0};
        }
        else 
        {
            scanf("%d%d",&x,&z);
            q[++tot]=(node){x,0,a[x],1,0,0};
            q[++tot]=(node){x,0,z,2,0,0};
//修改操作,先将原来的信息去除,再添加新的信息 
            a[x]=z;
        }
    }
    solve(1,tot,0,1e9);
//这里是0,不是1,因为二分答案,题目修改值可能为0 
    for(int i=1;i<=t;i++) printf("%d\n",ans[i]);
}

刚开始的时候,为了减少潜在的时间消耗,把mid,ll,rr3个变量放到全局变量里了

然后WAWAWAWA

递归局部变量可以保持本证递归回溯是变量还是原来那个值,全局变量改了就是改了

posted @ 2017-03-14 21:12  TRTTG  阅读(348)  评论(0编辑  收藏  举报