先挂上个大佬讲解,sunyutian1998学长给我推荐的mlystdcall大佬的【教程】简易CDQ分治教程&学习笔记
还有个B站小姐姐讲解的概念https://www.bilibili.com/video/av24295614?from=search&seid=4525696205028875575
以下就是我在看完mlystdcall大佬的博客后,自己的一些思考。
在学cdq分治的开始,我首先是把归并排序又看了一遍,归并排序也是一种分治的思想,把整个区间分成若干个区间处理,然后在合并。而cdq分治不同的地方就在于合并时,需要考虑左区间对右区间的影响。
然后cdq分治一个比较简单的例子,归并排序求逆序对。求逆序对的话,数据量小我们直接可以暴力,而当数据量大时我们可以用线段树或者树状数组来维护,而当数据规模(也就比如给的数组中的数很大,每个都1e9)很大时,我们可以对数据进行离散化,然后再用线段树或者树状数组维护。当然我们也可以不用离散化,改用归并排序来处理这个问题,具体怎么做呢?
首先,归并排序的话是先把每个区间分成[L,mid],[mid+1,R]两个区间,然后递归处理,这是分的过程。而当左右区间都处理好之后,这时我们就要把它们合并起来,归并排序就是简单的合并起来,而cdq分治就加上一个治的过程。因为我们是对逆序对,那么我们在合并的过程,对于L<=i<=mid,mid+1<=j<=R,有a[i]>a[j]的话,那么左区间在[i,mid]的数就都大于a[j],这时产生的逆序对就是mid-i+1个。也就是cdq分治思想中的
合并两个子问题,同时考虑到[L,M]内的修改对[M+1,R]内的查询产生的影响。即,用左边的子问题帮助解决右边的子问题。
Ultra-QuickSortOpenJ_Bailian - 2299
1 #include<cstdio> 2 const int N=500118; 3 int a[N],temp[N]; 4 long long ans=0; 5 void merge(int l,int r) 6 { 7 if(l==r) 8 return ; 9 int m=(l+r)>>1; 10 merge(l,m); 11 merge(m+1,r); 12 //递归处理好左右区间后,左右区间都已经有序 13 int i=l,j=m+1,k=l; 14 while(i<=m&&j<=r) 15 { 16 if(a[i]<=a[j]) 17 temp[k++]=a[i++]; 18 else 19 { 20 ans+=m-i+1; 21 //如果a[i]>a[j]那么包括后面的m-i+1个数都会与a[j]产生逆序对 22 temp[k++]=a[j++]; 23 } 24 } 25 while(i<=m) 26 temp[k++]=a[i++]; 27 while(j<=r) 28 temp[k++]=a[j++]; 29 for(i=l;i<=r;i++) 30 a[i]=temp[i]; 31 } 32 int main() 33 { 34 int n; 35 while(~scanf("%d",&n)&&n) 36 { 37 for(int i=0;i<n;i++) 38 scanf("%d",&a[i]); 39 ans=0; 40 merge(0,n-1); 41 printf("%lld ",ans); 42 } 43 return 0; 44 }
如果能明白归并排序求逆序对,那么对cdq分治的再深入,也没有太大问题。
CDQ最基础的应用二维偏序问题
给定N个有序对(a,b),求对于每个(a,b),满足a2<a且b2<b的有序对(a2,b2)有多少个。
在上面求逆序对中,其实我们就有涉及到二维偏序了,对于每个位置和相应的数视为有序对(位置i,数a[i])的话,那么逆序对,我们求的就是,满足位置i<位置j且a[i]>a[j]的有序对有多少个。由此扩展到二维偏序就是,我们可以先让a有序消除a元素的影响,然后合并问题时就是考虑b,也就相当于求顺序对。照着这个思想,我们可以去处理带修改和查询的问题。
二维偏序问题的拓展
给定一个N个元素的序列a,初始值全部为0,对这个序列进行以下两种操作:
操作1:格式为1 x k,把位置x的元素加上k(位置从1标号到N)。
操作2:格式为2 x y,求出区间[x,y]内所有元素的和。
线段树或者树桩数组裸题,但用CDQ分治怎么做呢,我们把每个操作的发生时间,和操作位置视为有序队(操作时间,操作位置)的话,首先操作时间是有序的,然后就是对操作位置由小到大合并,这样当左边区间的位置小于右边时,我们就可以记录一个更新的前缀和,当左边区间的位置大于右边时,我们可以统计一个更新的值对查询的影响。
那么说的话也就是,相应的值和操作类型的话我们需要用附加信息表示出来,op为1就代表着更新(加上k的操作),op为2就代表着查询操作中的减去[1,x-1]中的值,op为3就代表着查询操作中加上[1,y]中的值,op1和2合起来就是一整个查询操作。定义了这些之后我们就来考虑一下合并区间时,左右区间的影响。
首先因为第一维操作发生时间已经有序了,那么左边区间的发生时间就比右边区间早,这样的话右边区间的更新对左边区间的查询就没有影响(因为左边区间的查询发生时,右边区间的更新还没发生呢),由此也有左边区间的更新对右边的查询就有影响,而相同的区间内的更新对查询的影响呢,因为它们已经合成一个区间了,那么它们的影响在合并时已经计算过了,所以我们最终要统计的就只是左边区间的更新对右边查询的影响。
在具体的操作上就是,[L,M],[M+1,R],L<=i<=M,M+1<=j<=R这两个区间合并时,当a[i].pos<a[j].pos时,如果是a[i].op为1就记录左边区间更新的前缀和,而当a[i].pos>a[j].pos时,a[j].op为2就减去更新的前缀和,为3就加上更新的前缀和。而当a[i].pos=a[j].pos时,更新的优先级比查询高,因为我们前面所说的合并时就只需统计左边区间的更新对右边的查询就有影响,如果左区间的更新和右区间查询在同一个位置,应该先把左区间的更新算上。
最后一个注意的地方就是,如果有初始值的话直接使初始值视为一个更新操作。
1 #include<cstdio> 2 typedef long long ll; 3 const int N=501108,M=501108,Q=(M<<1)+N; 4 struct Nop{ 5 int pos,op,val;//更新的val代表加上的k,查询的val代表它的查询编号 6 friend bool operator <(const Nop &n1,const Nop &n2){ 7 return n1.pos==n2.pos ? n1.op<n2.op : n1.pos<n2.pos; 8 }//位置由小到大合并,相同位置更新优先 9 }P[Q],temp[Q];//因为初始值视为更新操作,而查询操作分为两个 10 ll ans[M<<1]; 11 int pn,qn;//pn总操作编号,qn总查询编号 12 void cdq(int l,int r) 13 { 14 if(l==r) 15 return ; 16 int m=(l+r)>>1; 17 cdq(l,m); 18 cdq(m+1,r); 19 ll sum=0; 20 int i=l,j=m+1,k=l; 21 while(i<=m&&j<=r) 22 { 23 if(P[i]<P[j]) 24 { 25 if(P[i].op==1)//左边统计更新前缀和 26 sum+=1ll*P[i].val; 27 temp[k++]=P[i++]; 28 } 29 else 30 { 31 if(P[j].op==2)//右边处理查询 32 ans[P[j].val]-=sum; 33 else if(P[j].op==3) 34 ans[P[j].val]+=sum; 35 temp[k++]=P[j++]; 36 } 37 } 38 while(i<=m)//右边区间没有查询了,没必要继续统计前缀和 39 temp[k++]=P[i++]; 40 while(j<=r) 41 { 42 if(P[j].op==2) 43 ans[P[j].val]-=sum; 44 else if(P[j].op==3) 45 ans[P[j].val]+=sum; 46 temp[k++]=P[j++]; 47 } 48 for(i=l;i<=r;i++) 49 P[i]=temp[i]; 50 } 51 int main() 52 { 53 int n,m,op,x,y; 54 while(~scanf("%d%d",&n,&m)) 55 { 56 pn=qn=0; 57 for(int i=1;i<=n;i++) 58 { 59 P[pn].pos=i,P[pn].op=1; 60 scanf("%d",&P[pn++].val); 61 } 62 for(int i=1;i<=m;i++) 63 { 64 scanf("%d%d%d",&op,&x,&y); 65 if(op==1) 66 P[pn].op=1,P[pn].pos=x,P[pn++].val=y; 67 else//把查询拆分成两个操作 68 { 69 P[pn].op=2,P[pn].pos=x-1,P[pn++].val=qn; 70 P[pn].op=3,P[pn].pos=y,P[pn++].val=qn++; 71 } 72 } 73 cdq(0,pn-1); 74 for(int i=0;i<qn;i++) 75 printf("%lld ",ans[i]); 76 } 77 return 0; 78 }
如果你能耐住性子坚持到现在,那么不用我说,也可以发现,CDQ分治的前提是,更新对查询的贡献独立,更新彼此间互不影响,而且的允许离线算法,所有的操作都得先记录下来再处理。看起来,CDQ的时间和空间复杂度都不怎么低,而且CDQ分治做的线段树和树状数组好像都能做,那为什么还需要CDQ分治呢?这也是我们前面说的,当数据规模很大时,无论是空间和时间,线段树和树状数组都吃不消,而CDQ分治取决于数据量不取决于数据规模。此外,当更复杂一点,三维偏序,四维偏序等问题的时候,线段树或者树状数组就得树套树之类的,处理起来非常麻烦,而CDQ分治就可以顶替复杂的高级数据结构,能对问题起到一个降维的效果。
至于更深的三维,四维问题,当做到相应的题时再更相应的内容。希望我的博客能对你学习CDQ分治有所帮助。
最后,路不只一条,解决问题的方法也不只一种,当一种不行时就去尝试其他的。