分治挑战数据结构——小记整体二分和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分治可以用于求多维偏序问题
两种分治算法都比较暴力,它们的优点是代码短而清晰,缺点是复杂度玄学,必须离线。
所以,这一轮还是没有决出胜负啊。
相关文章推荐
- POI2011 Meteors(CDQ分治 + 整体二分)
- 整体二分\cdq分治——洛谷P3332 [ZJOI2013]K大数查询
- [CDQ分治与整体二分]个人对CDQ分治与整体二分的理解
- 整体二分&cdq分治 ZOJ 2112 Dynamic Rankings
- cdq分治&整体二分学习缓存
- Uva7716 The Cure(CDQ分治 + 整体二分)
- cdq分治和整体二分
- 【cdq分治】cdq分治与整体二分学习笔记Part1.整体二分
- HDU - 5618 Jam's problem again(cdq分治和整体二分)
- 整体二分&CDQ分治:[BZOJ2527][POI2011] meteors [BZOJ3295][CQOI2011] 动态逆序对
- cdq分治和整体二分
- [CDQ分治与整体二分]个人对CDQ分治与整体二分的理解
- [BZOJ3110][ZJOI2013]K大数查询-CDQ分治-整体二分
- 整体二分<QAQ> && CDQ分治
- 整体二分 && CDQ分治
- CDQ分治与整体二分小结
- CDQ分治与整体二分小结
- 主席树+CDQ分治+整体二分
- HDU 5412 CRB and Queries(整体二分 | CDQ分治)
- 2017 暑假艾教集训 day9(整体二分 + cdq分治 cdq真是我女神!!!)