cdq分治学习笔记
0xFF 学习CDQ分治的前置知识——分治&归并排序
分治:分治,字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
——度娘
归并排序:归并排序是普通分治的一种基本应用。将排序序列分为2个子序列,再将2个子序列分别再分为4个子序列,这样一直分下去,直到子序列长度为1,这个长度为1的子序列即有序子序列。此时可以通过回溯将每个序列的两个有序子序列通过不断选取两个子序列对头中,小的那一个入队的方法,达到排序的目的。由于这样做相当于遍历了log(n)遍原序列,所以归并排序的时间复杂度为O(nlog(n)) .
0x00 CDQ分治是什么?
cdq分治,是由一位国家队的神仙——陈丹琦在09年的集训队论文中提出的一种特殊的分治。相对于普通分治而言,cdq分治解决的问题的前后两部分具有关联,这种关联可以影响到最终答案,所以不能像普通分治那样直接合并两部分的解。
具体看下图:
0x10 CDQ分治怎么用&例题
cdq所能解决的问题中,偏序问题是一个大类。偏序可简单地理解为有多个相同优先级的关键字的排序。逆序对便是一个经典的二维偏序问题。对于二维偏序问题,我们常常先用排序解决掉一维,在另一维中使用数据结构(树状数组,线段树等)来维护。当然,归并排序也不失为一种好办法。就以最简单的逆序对为例:
因为给定序列的前后位置自然有序,所以并不需要用排序解决掉一维。
我们按照顺序遍历给定序列,将每一个数字加入树状数组中。由于当前数字与它前面的数字若能组成逆序对,它前面的数字应比当前数字大,所以统计当前树状数组中权值比当前数字大的数字的个数,累加进最终答案即可。
Ps:由于此题的数字最大为1e9,所以需要离散化。
Code:
#include#include #include using namespace std; int n,a[500001],rk[500001]; struct number{ int posi,val; }b[500001]; int tot; long long ans; struct tree_array{ int tree[500001]; int lowbit(int sum){return sum&(-sum);} int query(int pos){int num=0;for(int i=pos;i;i-=lowbit(i))num+=tree[i];return num;} void add(int pos,int sum){for(int i=pos;i<=tot;i+=lowbit(i))tree[i]+=sum;} }t; bool cmp(number p1,number p2){ return p1.val b[i-1].val)++tot; rk[b[i].posi]=tot; } for(int i=1;i<=n;i++){ t.add(rk[i],1); ans+=t.query(tot)-t.query(rk[i]); } printf("%lld\n",ans); return 0; }
解决了二维偏序问题,那么三维偏序问题要如何解决呢?
当然是用cdq分治!
对于第一维,依然用排序解决。
对于第二维,我们用cdq分治:
先分治左区间和右区间,累加左右区间的答案。 然后分别对于左区间和右区间按照第二维关键字排序。
有人可能会问,那么第一维的排序不就乱了吗?的确如此。但是通过第一维的排序,我们可以保证右区间所有第一维的权值都大于左区间的第一维权值。这样可以保证只有左区间能影响右区间的答案,而右区间不能影响左区间的答案。
对于第三维,我们借鉴解决二维偏序的方法:树状数组
我们再借鉴归并排序的方法,遍历右区间,将左区间中所有第二维小于当前数字的第二维的数字的第三维都加入树状数组。然后再以处理二维偏序的方法来统计左区间对右区间的影响,累加进[l,r]的答案中,这个问题就解决了。
Code:
#include#include #include using namespace std; const int N=100001; const int K=200001; int n,cnt,tot,k,ansum[N]; struct flower{ int x,y,z; int wei,ans; }s[N],a[N]; bool cmp_x(flower p1,flower p2){ if(p1.x!=p2.x)return p1.x =1;i-=lowbit(i))num+=array[i];return num;} void add(int w,int sum){for(int i=w;i<=k;i+=lowbit(i))array[i]+=sum;} }t;//树状数组 void cdq(int l,int r){ if(l==r)return; int mid=(l+r)>>1; cdq(l,mid);cdq(mid+1,r); sort(a+l,a+mid+1,cmp_y); sort(a+mid+1,a+r+1,cmp_y); int j=l; for(int i=mid+1;i<=r;i++){ while(j<=mid&&a[j].y<=a[i].y){ t.add(a[j].z,a[j].wei);j++; } a[i].ans+=t.ask(a[i].z); } for(int i=l;i 0x20 例2 & cdq分治题单
【例2】[CQOI2011]动态逆序对
我们可以根据删除的顺序给每个数字一个删除时间$tim$,再根据它在原序列中的位置给定一个位置$wz$
那么当前数字i在删除前能造成的逆序对数即为满足
$a[i].tima[j].val$
$a[i].tima[j].wz$&&$a[i].val
Code:
#include#include #include using namespace std; int n,m,ti,pos[100005]; long long ansum; struct number{ int tim,val,wz; int ans; }a[100005]; bool cmp_tim(number p1,number p2){ if(p1.tim!=p2.tim)return p1.tim p2.val; return p1.wz >1; cdq(l,mid);cdq(mid+1,r); sort(a+l,a+mid+1,cmp_val_big); sort(a+mid+1,a+r+1,cmp_val_big); int j=mid+1; for(int i=l;i<=mid;i++){ while(j<=r&&a[j].val>a[i].val){ t.add(a[j].wz,1); j++; } a[i].ans+=t.ask(a[i].wz-1); } for(int i=mid+1;i =l;i--){ while(j>=mid+1&&a[j].valj;i--) t.add(a[i].wz,-1); } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d",&a[i].val); pos[a[i].val]=i; a[i].wz=i; a[i].tim=m+1; } for(int i=1;i<=m;i++){ scanf("%d",&ti); a[pos[ti]].tim=i; } sort(a+1,a+n+1,cmp_tim); cdq(1,n); sort(a+1,a+n+1,cmp_tim); for(int i=1;i<=n;i++)ansum+=a[i].ans; printf("%lld\n",ansum); for(int i=1;i 【题单】
[Violet]天使玩偶/SJY摆棋子