您的位置:首页 > 理论基础 > 数据结构算法

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

2018-01-09 20:59 274 查看

整体二分

例题: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
;LL sum[N<<2];
struct node{int l,r,bj,id;LL v;}Q
,Q1
,Q2
;
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
;
struct node{int a
d4f1
,b,c,js,cnt;}p
,kl
;
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分治可以用于求多维偏序问题

两种分治算法都比较暴力,它们的优点是代码短而清晰,缺点是复杂度玄学,必须离线。

所以,这一轮还是没有决出胜负啊。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: