分治挑战数据结构——小记整体二分和CDQ分治

整体二分

例题:bzoj3110/洛谷P3332
数据结构解决:线段树套splay
整体二分,顾名思义,就是把所有的东西拿来一起二分。在这道题里我们还要开一棵线段树。
1.把所有添加操作和询问顺序存进Q中。
2.二分一个答案,顺序处理所有操作
  2-1.对于查询操作,我们在线段树查询一下区间和(代表在这个区间里,小于mid的数的个数),依据这个个数进行分类。代码表示如下:

if(Q[i].bj==2) {
    LL kl=query(Q[i].l,Q[i].r,1,n,1);//在线段树里查询
    if(kl>=Q[i].v) Q1[++t1]=Q[i];
    else Q2[++t2]=Q[i],Q2[t2].v-=kl;//注意这个减少kl,思想类似于物理里的“转化参考系”(雾
}

  2-2.对于添加操作
    2-2-1.如果要添加的值小于等于mid,我们就在线段树里更新区间和,即增加“这个区间里,小于mid的数的个数”,并把该操作分进Q1类中。
    2-2-2.否则将该操作分进Q2类中。
3.清除在线段是里更新区间和后造成的影响
4.将Q1和Q2重新合并进Q中
5.递归进行二分(同时对操作也进行了二分)
如果哪一步不懂就看代码吧。

#include<bits/stdc++.h>
using namespace std;
int read() {
    int q=0,w=1;char ch=' ';
    while(ch!='-'&&(ch<'0'||ch>'9')) ch=getchar();
    if(ch=='-') w=-1,ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return w*q;
}
#define LL long long
const int N=50005;
int n,m,qs,laz[N<<2],ans[N];LL sum[N<<2];
struct node{int l,r,bj,id;LL v;}Q[N],Q1[N],Q2[N];
void pd(int i,int s,int t) {
    int l=(i<<1),r=(i<<1)|1,mid=(s+t)>>1;
    sum[l]+=laz[i]*(mid-s+1),sum[r]+=laz[i]*(t-mid);
    laz[l]+=laz[i],laz[r]+=laz[i],laz[i]=0;
}
void add(int l,int r,int s,int t,int i,int num) {
    if(l<=s&&t<=r) {laz[i]+=num,sum[i]+=(t-s+1)*num;return;}
    int mid=(s+t)>>1;
    if(laz[i]!=0) pd(i,s,t);
    if(l<=mid) add(l,r,s,mid,i<<1,num);
    if(mid+1<=r) add(l,r,mid+1,t,(i<<1)|1,num);
    sum[i]=sum[i<<1]+sum[(i<<1)|1];
}
LL query(int l,int r,int s,int t,int i) {
    if(l<=s&&t<=r) return sum[i];
    int mid=(s+t)>>1;LL re=0;
    if(laz[i]!=0) pd(i,s,t);
    if(l<=mid) re=query(l,r,s,mid,i<<1);
    if(mid+1<=r) re+=query(l,r,mid+1,t,(i<<1)|1);
    return re;
}
void binary(int ql,int qr,int l,int r) {
    if(ql>qr) return;
    //如果已经二分出了一个确切的答案,就更新答案
    if(l==r) {for(int i=ql;i<=qr;++i) if(Q[i].bj==2) ans[Q[i].id]=l;return;}
    int mid=(l+r)>>1,t1=0,t2=0;
    for(int i=ql;i<=qr;++i)
        if(Q[i].bj==2) {//步骤2-1
            LL kl=query(Q[i].l,Q[i].r,1,n,1);
            if(kl>=Q[i].v) Q1[++t1]=Q[i];
            else Q2[++t2]=Q[i],Q2[t2].v-=kl;
        }
        else if(Q[i].v<=mid) add(Q[i].l,Q[i].r,1,n,1,1),Q1[++t1]=Q[i];//步骤2-2-1
        else Q2[++t2]=Q[i];//步骤2-2-2
    for(int i=1;i<=t1;++i) if(Q1[i].bj==1) add(Q1[i].l,Q1[i].r,1,n,1,-1);//步骤3
    for(int i=1;i<=t1;++i) Q[ql+i-1]=Q1[i];//步骤4
    for(int i=1;i<=t2;++i) Q[ql+t1+i-1]=Q2[i];
    binary(ql,ql+t1-1,l,mid),binary(ql+t1,qr,mid+1,r);//步骤5
}
int main()
{
    n=read(),m=read();
    for(int i=1;i<=m;++i) {
        Q[i].bj=read(),Q[i].l=read(),Q[i].r=read(),Q[i].v=read();
        if(Q[i].bj==1) Q[i].v=-Q[i].v;//因为是查询第k大数,所以可以把所有数都取相反数
        else Q[i].id=++qs;
    }
    binary(1,m,-50000,50000);
    for(int i=1;i<=qs;++i) printf("%d\n",-ans[i]);
    return 0;
}

CDQ分治

例题:bzoj1176,是一道权限题,没有权限的同学可以看下面那道例题。
把一次询问拆成四次前缀和处理,然后使用CDQ分治即可(另外此题的s好像并没有用)。
1.将所有操作按照x为第一关键字,y为第二关键字,第三关键字为修改操作在查询操作前面的顺序排序。
2.对于时间(即是第几个操作)进行分治
3.使用树状数组维护y上的答案,由于已经以x为关键字排序了,所以计算x的前缀和这个条件已经满足了。
4.遍历当前时间区间的每个操作,如果这个修改操作的时间小于等于mid,就执行这一步操作。如果这个询问操作的时间大于mid,就先计算一下前mid个操作(也就是当前修改完成后的树状数组)对其答案造成的贡献。
5.清除所有修改操作的影响。
6.将当前区间时间在[l,mid]内的操作丢到前mid位,在[mid+1,r]的丢到后面,进行递归分治。
当然,CDQ分治的思想是这样的,算法执行顺序不一定如我所讲的这样。用某Cai的话来说,如果你不想动脑子,那么先cdq左半区间,再处理当前整个区间,再cdq右半区间这样的顺序比较好。如果你想写得简便一点,可以先进行递归执行,再处理现在的区间,不过需要动点脑子。

#include<bits/stdc++.h>
using namespace std;
int read() {
    int q=0,w=1;char ch=' ';
    while((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
    if(ch=='-') w=-1,ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
int s,n,m,q;
struct node{int bj,id,qid,x,y,v;}Q[650005],tmp[650005];
int ans[10005],tr[2000005];
int lowbit(int x) {return x&(-x);}
void add(int x,int num) {while(x<=n) tr[x]+=num,x+=lowbit(x);}
int ask(int x) {
    int re=0;
    while(x) re+=tr[x],x-=lowbit(x);
    return re;
}
void cdq(int l,int r) {//步骤2
    if(l==r) return;
    int mid=(l+r)>>1;
    for(int i=l;i<=r;++i)//步骤3,4
        if(Q[i].bj==1&&Q[i].id<=mid) add(Q[i].y,Q[i].v);
        else if(Q[i].bj==2&&Q[i].id>mid) ans[Q[i].qid]+=Q[i].v*ask(Q[i].y);
    for(int i=l;i<=r;++i)//步骤5
        if(Q[i].bj==1&&Q[i].id<=mid) add(Q[i].y,-Q[i].v);
    int t1=l-1,t2=mid;
    for(int i=l;i<=r;++i)//步骤6
        if(Q[i].id<=mid) tmp[++t1]=Q[i];
        else tmp[++t2]=Q[i];
    for(int i=l;i<=r;++i) Q[i]=tmp[i];
    cdq(l,mid),cdq(mid+1,r);
}
int cmp(node a,node b) {//步骤1
    if(a.x!=b.x) return a.x<b.x;
    if(a.y!=b.y) return a.y<b.y;
    return a.bj<b.bj;
}
int main()
{
    int bj,x1,y1,x2,y2,w;
    s=read(),n=read();
    while("niconiconi") {
        bj=read();
        if(bj==3) break;
        if(bj==1) x1=read(),y1=read(),w=read(),Q[++m]=(node){1,m,0,x1,y1,w};
        else {
            x1=read(),y1=read(),x2=read(),y2=read(),++q;
            Q[++m]=(node){2,m,q,x2,y2,1};
            Q[++m]=(node){2,m,q,x1-1,y2,-1};
            Q[++m]=(node){2,m,q,x2,y1-1,-1};
            Q[++m]=(node){2,m,q,x1-1,y1-1,1};
        }
    }
    sort(Q+1,Q+1+m,cmp),cdq(1,m);
    for(int i=1;i<=q;++i) printf("%d\n",ans[i]);
    return 0;
}

再讲讲CDQ分治的最重要应用:loj112 三维偏序
如果没有CDQ分治,那么这道题就要用树套树做了。众所周知,树套树写起来是很困难的。
这题没有“时间”概念,不过我们可以把a属性视作时间。先按照a为第一关键字,b为第二关键字,c为第三关键字的顺序排序。在CDQ分治的过程中,逐步把左边和右边两个区间变成以b为第一关键字的排序,然后用树状数组维护c,用归并排序维护b的顺序。
这么讲可能很不清楚,不过代码总能说明一切。

#include<bits/stdc++.h>
using namespace std;
int read() {
    int q=0;char ch=' ';
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
#define lowbit(x) (x&(-x))
const int N=100005,K=200005;
int n,lim,kn,tr[K],ans[N];
struct node{int a,b,c,js,cnt;}p[N],kl[N];
void add(int x,int num) {while(x<=lim) tr[x]+=num,x+=lowbit(x);}
int query(int x) {int re=0;while(x){re+=tr[x],x-=lowbit(x);}return re;}
int cmp2(int i,int j) {
    if(p[i].b!=p[j].b) return p[i].b<p[j].b;
    if(p[i].c!=p[j].c) return p[i].c<p[j].c;
    return 1;
}
void merge(int l,int r,int mid) {
    int t1=l,t2=mid+1;
    for(int i=l;i<=r;++i)
        if(t1<=mid&&(t2>r||cmp2(t1,t2))) kl[i]=p[t1],++t1;
        else kl[i]=p[t2],++t2;
    for(int i=l;i<=r;++i) p[i]=kl[i];
}
void cdq(int l,int r) {
    if(l==r) return;
    int mid=(l+r)>>1;
    cdq(l,mid),cdq(mid+1,r);
    for(int i=mid+1,j=l;i<=r;++i) {
        while(j<=mid&&p[j].b<=p[i].b) add(p[j].c,p[j].cnt),++j;
        p[i].js+=query(p[i].c);
    }
    for(int i=l;i<=mid&&p[i].b<=p[r].b;++i) add(p[i].c,-p[i].cnt);//清除影响
    merge(l,r,mid);//归并排序,比sort小3倍常数
}
int cmp1(node x,node y) {
    if(x.a!=y.a) return x.a<y.a;
    if(x.b!=y.b) return x.b<y.b;
    return x.c<y.c;
}
int main()
{
    n=read(),lim=read();
    for(int i=1;i<=n;++i)
        p[i].a=read(),p[i].b=read(),p[i].c=read(),p[i].cnt=1;
    sort(p+1,p+1+n,cmp1);kn=1;
    for(int i=2;i<=n;++i)//去重
        if(p[i].a==p[kn].a&&p[i].b==p[kn].b&&p[i].c==p[kn].c) ++p[kn].cnt;
        else p[++kn]=p[i];
    cdq(1,kn);
    for(int i=1;i<=kn;++i) ans[p[i].js+p[i].cnt-1]+=p[i].cnt;
    for(int i=0;i<n;++i) printf("%d\n",ans[i]);
    return 0;
}

还有一道cdq分治的经典例题:戳我瞧瞧QWQ
最后我们再来提一下CDQ套CDQ的经典四维偏序问题,例题就用:COGS 2479啦,这道题不用去重而且只要考虑偏序对个数,做起来很舒服啊~
三维偏序第一维排序,第二维CDQ,第三维树状数组。那么四维偏序呢?
orzCai
……说的很有道理,不过不只线段树可以套起来,CDQ也是可以的。
(伪)伪代码如下:

CDQ2(l,r) {
    CDQ2(l,mid),CDQ2(mid+1,r);
    以b为关键字归并排序 {
        如果按a排序在左半边且初始下标在左半边,上传到树状数组中。
        如果都在右半边,在树状数组中获得答案。
    }
}
CDQ1(l,r) {
    CDQ1(l,mid),CDQ1(mid+1,r);
    以a为关键字归并排序 {把初始下标在左半边的打上标记1,在右半边的打上标记2}
    CDQ2(l,r);
}

真代码如下:

#include<bits/stdc++.h>
using namespace std;
int read() {
    int q=0;char ch=' ';
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
#define RI register int
const int N=50005;
struct node{int a,b,c,tag;}t[N],t1[N],t2[N];
int n,ans,st[N],s[N];
#define lowbit(x) (x&(-x))
void add(int x,int num) {while(x<=n) s[x]+=num,x+=lowbit(x);}
int query(int x) {
    int re=0;
    while(x) re+=s[x],x-=lowbit(x);
    return re;
}
void cdq2(int l,int r) {
    if(l==r) return;
    int mid=(l+r)>>1,i=l,j=mid+1,top=0;
    cdq2(l,mid),cdq2(mid+1,r);
    for(RI k=l;k<=r;++k)
        if(j>r||(i<=mid&&t1[i].b<t1[j].b)) {
            t2[k]=t1[i++];
            if(t2[k].tag) add(t2[k].c,1),st[++top]=t2[k].c;
        }
        else {
            t2[k]=t1[j++];
            if(!t2[k].tag) ans+=query(t2[k].c);
        }
    for(RI k=l;k<=r;++k) t1[k]=t2[k];
    for(RI k=1;k<=top;++k) add(st[k],-1);
}
void cdq1(int l,int r) {
    if(l==r) return;
    int mid=(l+r)>>1,i=l,j=mid+1;
    cdq1(l,mid),cdq1(mid+1,r);
    for(RI k=l;k<=r;++k)
        if(j>r||(i<=mid&&t[i].a<t[j].a)) t1[k]=t[i++],t1[k].tag=1;
        else t1[k]=t[j++],t1[k].tag=0;
    for(RI k=l;k<=r;++k) t[k]=t1[k];
    cdq2(l,r);
}
int main()
{
    freopen("partial_order.in","r",stdin);
    freopen("partial_order.out","w",stdout);
    n=read();
    for(RI i=1;i<=n;++i) t[i].a=read();
    for(RI i=1;i<=n;++i) t[i].b=read();
    for(RI i=1;i<=n;++i) t[i].c=read();
    cdq1(1,n),printf("%d\n",ans);
    return 0;
}

总结

整体二分的思想是同时对处理区间和答案进行二分。
CDQ分治的思想是用处理方式进行排序,然后对时间进行二分。
整体二分可以用于求询问操作一样,而且可以二分答案解决的问题
CDQ分治可以用于求多维偏序问题
两种分治算法都比较暴力,它们的优点是代码短而清晰,缺点是复杂度玄学,必须离线。
所以,这一轮还是没有决出胜负啊。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值