CDQ分治与整体二分学习笔记

 CDQ分治部分

     CDQ分治是用分治的方法解决一系列类似偏序问题的分治方法,一般可以用KD-tree、树套树或权值线段树代替。

     三维偏序,是一种类似LIS的东西,但是LIS的关键字只有两个,数组下标和权值,三维偏序问题的权值有两个,且必须A[I]<A[J]且B[I]<B[j]。

     把这个问题放到平面上,就是一个点在另一个点的左下方。

     那么如何求?

     CDQ分治的主要过程是二分整个区间,把左区间看成产生贡献的区间,于是我们在左区间进行操作,在右区间统计答案,用归并排序的方法求解。

     对于这道题,我们二分整个区间,对A做归并排序,归并时对B维护一个树状数组,在左区间更改,右区间查询就可以了。

     例题1

     陌上花开(bzoj3262)裸的三维偏序

     我们按照a为第一关键字,b为第二关键字,c为第三关键字排序,其实相当于把a去掉了,换成了数组下标,因为本质相同的元素答案是相同的,所以手动去重后直接CDQ分治求解。

    Code

    

#include<iostream>
#include<cstdio>
#include<algorithm>
#define low (x&(-x))
#define N 200009
#define M 100009
using namespace std;
typedef long long ll;
int k,n,ans2[M],be[M];
ll f[M],tr[N],ans[M];
struct zzh
{
    int a,b,c,id,size;
    bool operator < (const zzh &u)const
    {
        if(b!=u.b)return b<u.b;
        if(c!=u.c)return c<u.c;
        return id<u.id;
    } 
}ji[M],a[M],t[M];
bool cmp(zzh a,zzh b)
{
        if(a.a!=b.a)return a.a<b.a;
        if(a.b!=b.b)return a.b<b.b;
        if(a.c!=b.c)return a.c<b.c;
        return a.id<b.id;
}
void mem(int x){while(x<=k)tr[x]=0,x+=low;}
void gai(int x,int y){while(x<=k)tr[x]+=y,x+=low;}
int query(int x)
{
    int ans=0;
    while(x){ans+=tr[x];x-=low;}
    return ans;
}
void cdq(int l,int r)
{
    if(l==r)return;
    int mid=(l+r)>>1;
    cdq(l,mid);cdq(mid+1,r);
    int p=l,q=mid+1,o=l-1;
    while(p<=mid&&q<=r)
    {
        if(t[p]<t[q])
          gai(t[p].c,t[p].size),ji[++o]=t[p++];
        else ans[t[q].id]+=query(t[q].c),ji[++o]=t[q++];
    }
    while(p<=mid)ji[++o]=t[p++];
    while(q<=r)ans[t[q].id]+=query(t[q].c),ji[++o]=t[q++];
    for(int i=l;i<=r;++i)
      mem(ji[i].c),t[i]=ji[i];
}
bool pd(int x,int now){
    if(!now)return 0;
    return ((a[x].a==a[now].a)&&(a[x].b==a[now].b)&&(a[x].c==a[now].c));
}
int main()
{
//    freopen("233.out","w",stdout);
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;++i)
      scanf("%d%d%d",&a[i].a,&a[i].b,&a[i].c),a[i].id=i,a[i].size=1;
    sort(a+1,a+n+1,cmp);int now=0;
    for(int i=1;i<=n;++i)
    {
        if(!pd(i,i-1))
          t[++now]=a[i],t[now].id=now;
        else t[now].size++;
        be[i]=now;
    }
    cdq(1,now);
    for(int i=1;i<=now;++i)ans[t[i].id]+=t[i].size-1;
    for(int i=1;i<=n;++i)f[i]=ans[be[i]];
    for(int i=1;i<=n;++i)ans2[f[i]]++;
    for(int i=0;i<n;++i)printf("%d\n",ans2[i]);
    return 0;
}

 

     bzoj4553 [Tjoi2016&Heoi2016]序列

     也是一个类LIS问题,只不过条件有些特殊。

     观察题目可知,i和j都在LIS中需满足a[i]<=a[j]&&a[i]<=l[j]&&r[i]<=a[j],其中l[i]为a[i]的最小值,r[i]为其最大值,第一个条件可省略,再加上下标,正好是三个。

     我们先CDQ左区间,再把左区间按照a排序,右区间按照l排序,归并时将左区间的r加入树状数组,右区间用a查询,做完后在CDQ右区间就行了。

      f数组记得取max。

Code

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 100009
#define shi 100000
#define lowbit(x) (x&(-x))
using namespace std;
int tr[N],n,m,f[N]; 
inline int rd()
{
    int x=0;
    char c=getchar();
    while(!isdigit(c))c=getchar();
    while(isdigit(c))
    {
        x=(x<<1)+(x<<3)+(c^48);
        c=getchar();
    }
    return x;
}
struct zzh
{
    int l,r,a,id;
}ji[N];
void add(int x,int y){while(x<=shi){tr[x]=max(tr[x],y);x+=lowbit(x);}}
void clear(int x){while(x<=shi){tr[x]=0;x+=lowbit(x);}}
int query(int x){
  int ans=0;
  while(x){
  ans=max(tr[x],ans);
  x-=lowbit(x);
  }
  return ans;
}
bool cmp1(zzh a,zzh b){return a.a<b.a;}
bool cmp2(zzh a,zzh b){return a.l<b.l;}
bool cmp3(zzh a,zzh b){return a.id<b.id;}
void cdq(int l,int r)
{
    if(l==r)return;
    int mid=(l+r)>>1;
    cdq(l,mid);
    sort(ji+l,ji+mid+1,cmp1);sort(ji+mid+1,ji+r+1,cmp2);
    int p=l,q=mid+1,o=l-1;
    while(p<=mid&&q<=r)
    {
        if(ji[p].a<=ji[q].l)add(ji[p].r,f[ji[p].id]),p++; 
        else f[ji[q].id]=max(query(ji[q].a)+1,f[ji[q].id]),q++;
    }
    while(q<=r)f[ji[q].id]=max(query(ji[q].a)+1,f[ji[q].id]),q++;
    for(int i=l;i<=mid;++i)clear(ji[i].r);
    sort(ji+l,ji+r+1,cmp3);
    cdq(mid+1,r);
}
int main()
{
    n=rd();m=rd();
    for(int i=1;i<=n;++i)
      ji[i].l=ji[i].r=ji[i].a=rd(),ji[i].id=i;
      int x,y;
    for(int j=1;j<=m;++j)
      {
          x=rd();y=rd();
          ji[x].l=min(ji[x].l,y);
          ji[x].r=max(ji[x].r,y);
      }
    for(int i=1;i<=n;++i)f[i]=1;    
    cdq(1,n);int ans=0;
    for(int i=1;i<=n;++i)ans=max(ans,f[i]);
    cout<<ans;
    return 0;
} 

 整体二分部分

      整体二分本质和CDQ差不多,都是基于分治的算法,实现起来比较简单(只要能够深入理解)。

     例题(就写过一道)

     [ZJOI2013]K大数查询

     有N个位置,M个操作。操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少。

     这题什么神奇分块树套树的都能过,整体二分也是一个很经典的解法。

     我们现在有一个操作和询问混合的序列,我们需要通过一些操作把它分成两个序列,在这两个序列在时间维度上是单调递增的(在原序列中的相对位置不变),但答案的值域分别小了一半,虽然序列并没有严格二分,但答案值域减小了一半,保证了我们分治的复杂度。

     考虑我们有一个序列,里面有在l-r内每个位置插入一个c,还有询问一个区间的第K大,目前的值域为-inf---inf.

     我们按照时间的顺序把这个序列扫一遍,遇到操作大于等于mid就把这个l---r+1,当我们遇到一个询问时,我们就查l---r有多少数,实际就是查l---r有多少数是大于等于mid,如果已经够K个了,说明我的第K大一定是>=mid的(显然),所以我们把它放到右序列,否则,它就该在左边,同时我们要把K减去我们查询的结果,意思就是说我们已经数了这么多个了。

      同理,对于每个操作,如果>=mid就放到右序列,否则就放到左序列。

      然后我们完成了一项使命,把答案值域缩小了一半,接下来继续分治就好了。

      边界l==r,此时我的所有询问的答案都是这个值。

      时间复杂度,分治nlogn,加上线段树维护操作一个log,总复杂度nlog2n。

     Code

#include<iostream>
#include<cstdio>
#define N 50009
using namespace std;
long long tr[N<<2];
int la[N<<2],n,m,ans[N];
struct ef
{
    int l,r,tag,id;
    long long v;
}a[N],q1[N],q2[N];
inline void pushdown(int cnt,int l1,int l2)
{
  tr[cnt<<1]+=la[cnt]*l1;tr[cnt<<1|1]+=la[cnt]*l2;
  la[cnt<<1]+=la[cnt];la[cnt<<1|1]+=la[cnt];la[cnt]=0;
}
long long query(int cnt,int l,int r,int L,int R)
{
    if(l>=L&&r<=R)return tr[cnt];
    int mid=(l+r)>>1;
    if(la[cnt])pushdown(cnt,mid-l+1,r-mid);
    long long ans=0;
    if(mid>=L)ans+=query(cnt<<1,l,mid,L,R);
    if(mid<R)ans+=query(cnt<<1|1,mid+1,r,L,R);
    return ans;
}
void add(int cnt,int l,int r,int L,int R,int tag)
{
    if(l>=L&&r<=R)
    {
       tr[cnt]+=(r-l+1)*tag;
       la[cnt]+=tag;
       return;
    }
    int mid=(l+r)>>1;
    if(la[cnt])pushdown(cnt,mid-l+1,r-mid); 
    if(mid>=L)add(cnt<<1,l,mid,L,R,tag);
    if(mid<R)add(cnt<<1|1,mid+1,r,L,R,tag);
    tr[cnt]=tr[cnt<<1]+tr[cnt<<1|1];
}
void solve(int st,int en,int l,int r)
{
    if(l==r)
    {
        for(int i=st;i<=en;++i)
          if(a[i].tag==2)ans[a[i].id]=l;
        return;
    }
    int mid=(l+r)>>1,ll=0,rr=0;
    for(int i=st;i<=en;++i)
        if(a[i].tag==1)
        {
            if(a[i].v<=mid)q1[++ll]=a[i];
            else
            {
                add(1,1,n,a[i].l,a[i].r,1);
                q2[++rr]=a[i];
            }
        }
        else
        {
            long long val=query(1,1,n,a[i].l,a[i].r);
            if(val>=a[i].v)
            {
                q2[++rr]=a[i];
            }
            else a[i].v-=val,q1[++ll]=a[i];
        }
    for(int i=st;i<=en;++i)
     if(a[i].tag==1&&a[i].v>mid)add(1,1,n,a[i].l,a[i].r,-1);
    for(int i=1;i<=ll;++i)a[st+i-1]=q1[i];
    for(int i=1;i<=rr;++i)a[st+ll+i-1]=q2[i];
    solve(st,st+ll-1,l,mid);solve(st+ll,en,mid+1,r);
}
int main()
{
    scanf("%d%d",&n,&m);int tot=0;
    for(int i=1;i<=m;++i)
    {
    scanf("%d%d%d%lld",&a[i].tag,&a[i].l,&a[i].r,&a[i].v);
    if(a[i].tag==2)a[i].id=++tot;
    }
    solve(1,m,-n,n);
    for(int i=1;i<=tot;++i)printf("%d\n",ans[i]);
    return 0;
} 

 Dynamic Rankings

给定一个含有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,然后读入一系列的指令,包括询问指令和修改指令。

对于每一个询问指令,你必须输出正确的回答。

Solution

区间待修第k大,经典整体二分题。

Code

#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 100002
using namespace std;
int tr[N<<1],ji[N<<1],top,tot,x,y,ans[N],n,m,aa,num[N];
char c;
inline void add(int x,int y){while(x<=top)tr[x]+=y,x+=x&-x;}
inline int query(int x){int ans=0;while(x)ans+=tr[x],x-=x&-x;return ans;}
struct sd{
    int x,y,tag,l,r,k;
}q[N*3],a[N*3],b[N*3];
void solve(int l,int r,int L,int R){
    if(L==R)
    {
      for(int i=l;i<=r;++i)
        if(q[i].l)ans[q[i].tag]=L;
      return;
    }
    int mid=(L+R)>>1,l1=0,l2=0;
    for(int i=l;i<=r;++i){
        if(q[i].x!=0){
            if(q[i].y<=mid)add(q[i].x,q[i].tag),a[++l1]=q[i];
            else b[++l2]=q[i];
        }
        else{
            int tmp=query(q[i].r)-query(q[i].l-1);
            if(tmp>=q[i].k)a[++l1]=q[i];
            else q[i].k-=tmp,b[++l2]=q[i];
        }
    } 
    for(int i=1;i<=l1;++i)if(a[i].x)add(a[i].x,-a[i].tag);
    for(int i=l;i<=l+l1-1;++i)q[i]=a[i-l+1];
    for(int i=l+l1;i<=r;++i)q[i]=b[i-l-l1+1];
    solve(l,l+l1-1,L,mid);solve(l+l1,r,mid+1,R);
}
char ge(){
    char c=getchar();
    while(c!='Q'&&c!='C')c=getchar();
    return c;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){
     scanf("%d",&x);ji[++top]=x;
     q[++tot].x=i;q[tot].y=x;q[tot].tag=1;
     num[i]=x;
   }
    for(int i=1;i<=m;++i){
     c=ge();
     if(c=='Q'){
         ++tot;
         scanf("%d%d%d",&q[tot].l,&q[tot].r,&q[tot].k);
         q[tot].tag=++aa;
     }
     else{
         scanf("%d%d",&x,&y);
         q[++tot].x=x;q[tot].y=num[x];q[tot].tag=-1;
         q[++tot].x=x;q[tot].y=y;q[tot].tag=1;
         num[x]=y;//care!!!!!
         ji[++top]=y;
     }
   }
     sort(ji+1,ji+top+1);
     top=unique(ji+1,ji+top+1)-ji-1;
     for(int i=1;i<=tot;++i)
       if(q[i].y)q[i].y=lower_bound(ji+1,ji+top+1,q[i].y)-ji;
     solve(1,tot,1,top);
     for(int i=1;i<=aa;++i)printf("%d\n",ji[ans[i]]);
     return 0;
} 

 

posted @ 2018-08-02 16:18  comld  阅读(311)  评论(0编辑  收藏  举报