數據結構中的一塊內容:$CDQ$分治算法。
$CDQ$顯然是一個人的名字,陳丹琪(NOI2008金牌女選手)
這種離線分治算法被算法界稱為"cdq分治"
我們知道,一個動態的問題一定是由"更改""查詢"操作構成的,顯然,有些“更改”會改變"查詢的結果",而有些不能
如果我們合理安排一個次序,把每一個查詢分成幾個部分,分別計算值,最后合起來就是原來詢問的值。
離線算法和在線算法的概念不用過多解釋.
接下來通過幾個例題將基本的$CDQ$分治算法解釋明白.
A. 從逆序對開始的二維偏序問題
下面將給出逆序對的題目:
例題·A1: 逆序對的定義是對於序列a[],取第$l$個元素和第$r$個元素,滿足$l <r$且$a[l]>a[r]$,對於這樣數對$(l,r)$被稱為一對逆序對
求解給定序列a中有多少對逆序對,你需要輸出對數.
對於100%的數據 $a \leq 5 \times 10^5 $
我們都知道可以用樹狀數組和歸並排序兩種方法做,這里我們只講歸並排序(默認樹狀數組大家都會)
不斷把$[l,r]$細分,每次取$mid=\frac{l+r}{2}$ 然后指針$i$和$j$分別指向區間$[l,mid]$和$[mid+1,r]$並且單調
先確定$i$不動,把$j$右移歸並,如果滿足$a[i]>a[j]$記錄逆序對個數加$mid-i+1$意味着a[i],$i \in [i,mid]$和 a[j]互成逆序對,跳出把$i$移動
依次,直到完成所有區間,復雜度$O(n log_2 n)$
核心代碼Code:
void sort(int l,int r,int mid) { for (int i=l;i<=mid;i++) a[i]=t[i]; for (int i=mid+1;i<=r;i++) b[i]=t[i]; int i=l,j=mid+1,k=l; while (i<=mid&&j<=r) { if (a[i]<=b[j]) c[k++]=a[i++]; else c[k++]=b[j++],ans+=mid-i+1; //當前的b中當前的j和剩下的ai都是一對逆序對 } while (i<=mid) c[k++]=a[i++]; while (j<=r) c[k++]=b[j++]; for (int i=l;i<=r;i++) t[i]=c[i]; } void merge_sort(int l,int r) { int mid=(l+r)/2; if (l<mid) merge_sort(l,mid); if (r>mid) merge_sort(mid+1,r); sort(l,r,mid); }
然后這個問題反映什么實質呢,顯然我們可以造出2個維度記偏序(t,i)表示時間為t,位置為i
由於我們按照順序讀入,那么默認第一維偏序是有序的,便忽略了這一維度的記錄和排序。
換句話說,在歸並的時候雖然左區間和右區間里面值不一定相等,但是左區間的所有元素都排在右區間前面
這是基於第一維偏序是有序的情況下進行的.
然而,對於有些情況,第一維偏序的記錄和第一維偏序的排序是不可省略的!
下面給出樹狀數組的模板題,請用CDQ分治(二維偏序)方法AC本題。
例題A2:維護含n個元素的原始序列a[],有m個操作,2種不同格式:
1 x k 含義:將第x個數加上k
2 x y 含義:輸出區間[x,y]內每個數的和
請正確輸出每一個2操作的答案,不強制在線。
對於100%的數據 $n,m \leq 5 \times 10^5$
Solution:把原始序列讀入操作變化成插入第i個位置上加上對應的值,參與cdq分治。
首先把查詢拆成兩個,減去x-1的前綴和和加上y的前綴和(前綴和優化的思路)
每一個詢問記錄4個量type[類型],x,y(兩個參數),idx(若是詢問操作表示是第幾個詢問)
時間作為默認有序的第一維度,用cdq分治維護第二維度位置(歸並按照位置歸並)。
僅考慮左邊的更改對右邊查詢的影響,更改只有在左側有影響,當$ tl\geq mid $且$a[tl].x<a[tr].x $同時滿足的情況下,將前綴和sum+=y[加上的值]
這個時候第一維已經默認有序了,也就是說明亂序下左邊操作的時間一定在右邊操作之前,所以更改對右邊有影響,影響是sum
同時對於位置歸並cdq,雖然打亂了這一部分時間,但是對於大塊時間的划分是沒有影響的.
我們更新的是處理過[mid,r]這一段的答案,如果tr>r不在這個區間內,需要舍去
一種通俗的寫法是,將tr>r的情況歸屬到更改的影響中,這樣不會對答案造成干擾.
這是核心代碼Code:
void cdq(int l,int r) { if (l==r) return; int mid=(l+r)>>1; cdq(l,mid); cdq(mid+1,r); int tl=l,tr=mid+1,sum=0; fp(i,l,r) { if ((tl<=mid&&a[tl]<a[tr])||tr>r) { if (a[tl].type==1) sum+=a[tl].y; b[i]=a[tl++]; } else { if (a[tr].type==2) ans[a[tr].idx]-=sum; //2代表-sum{l-1} if (a[tr].type==3) ans[a[tr].idx]+=sum;//3代表+sum{r} b[i]=a[tr++]; } } fp(i,l,r) a[i]=b[i]; }

# include <bits/stdc++.h> # define fp(i,s,t) for (int i=s;i<=t;i++) # define int long long using namespace std; const int N=(5e5+10)*3; struct rec{ int type,x,y,idx; bool operator < (const rec &t) const{ if (x!=t.x) return x<t.x; else return type<t.type; } }a[N],b[N]; int n,m,tot,qes,ans[N]; inline int read(int &x) { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); x=w?-X:X; } void write(int x) { if (x<0) putchar('-'),x=-x; if (x>9) write(x/10); putchar('0'+x%10); } void cdq(int l,int r) { if (l==r) return; int mid=(l+r)>>1; cdq(l,mid); cdq(mid+1,r); int tl=l,tr=mid+1,sum=0; fp(i,l,r) { if ((tl<=mid&&a[tl]<a[tr])||tr>r) { if (a[tl].type==1) sum+=a[tl].y; b[i]=a[tl++]; } else { if (a[tr].type==2) ans[a[tr].idx]-=sum; if (a[tr].type==3) ans[a[tr].idx]+=sum; b[i]=a[tr++]; } } fp(i,l,r) a[i]=b[i]; } signed main() { read(n); read(m); fp(i,1,n) { tot++; a[tot].type=1; a[tot].x=i; read(a[tot].y); } fp(i,1,m) { int t,x,y; tot++; read(t); a[tot].type=t; if (t==1) read(a[tot].x),read(a[tot].y); else { read(x),read(y); qes++; a[tot].idx=qes; a[tot].x=x-1; a[tot].type=2; tot++; a[tot].idx=qes; a[tot].x=y; a[tot].type=3; } } cdq(1,tot); fp(i,1,qes) write(ans[i]),putchar('\n'); return 0; }
B. 從二維偏序到三維偏序
一維偏序直接sort
二維偏序第1維sort,第2維cdq分治
三維偏序第1維sort,第2維cdq分治,第3維 數據結構
下面給出三維偏序問題:
給定$n$個有序三元組$(a,b,c)$,求對於每個3元組$(a,b,c)$,
有多少個3元組$(a_i,b_i,c_i)$滿足$a_i<a$且$b_i<b$且$c_i<c$.
仿造上面的寫法,對第1維a排序,對第2維b歸並cdq,
對於第3維c,我們借助權值樹狀數組, 每次從左邊取出三元組(a,b,c),根據c值在樹狀數組中進行修改
從右邊的序列中取出三元組(a,b,c)時,在樹狀數組中查詢c值小於(a,b,c)的三元組的個數
注意,每次使用完樹狀數組要把樹狀數組清零
下面我們給出一個例題,即陌上花開是luogu三維偏序的模板題
例題B1:陌上花開
有$n$個元素,每個元素有$a_i,b_i,c_i$三種屬性,定義兩個元素大小比較為:
若$a_i>a_j$且$b_i>b_j$且$c_i>c_j$簡記為$i>j$,對於不同元素i,需要統計他比多少元素大,
就是元素$i$的權$f(i)$,問對於 $d\in [0,n-1] $有多少$x\in [1,n] $的取值$f(x)=d$ 輸出一個數組表示 $ d=0,1,2,3...n-1$時的答案
對於100%的數據 $n \leq 10^5 $
Solution:
首先是可能存在一些相同的元素的,我們定義元素相同為三種屬性均相同。這樣較為難處理,第一步我們把他們合並起來了。
於是對於每種元素,記錄了四個參數a,b,c,w,id表示三種屬性、數量、他的標號。
然后對其第1維a進行排序[其實在去重這一步我們事實上已經完成了這一操作]
然后對於第2維b進行cdq分治,在每一處分治的時候樹狀數組維護插入和查詢,每次需要插入的時候在值域樹狀數組維護數量的前綴和,
插入的時候,對於z處加上數量w,update(z,w),查詢的時候,在ans[id]+=query(z)。
然后就可以統計出對於每一朵花有多少元素比他大了,記錄在ans[id]中,id $\in$ [1,n]
輸出的時候還需要統計,別忘了加上自己的那份w和減去自己的1!
核心代碼Code:
//去重操作 sort(t+1,t+1+n,cmp1); int i=1,j; tot=0; while (i<=n){ j=i+1; while (j<=n&&same(t[i],t[j])) j++; j--; a[++tot]=t[i]; a[tot].w=(j-i+1); a[tot].id=tot; i=j+1; }
void cdq(int l,int r) //cdq分治 { if (l==r) return; int mid=(l+r)>>1; cdq(l,mid);cdq(mid+1,r); dfn++; //便於去重 int t1=l,t2=mid+1; for (int i=l;i<=r;i++) { if ((t1<=mid&&a[t1].y<=a[t2].y)||t2>r) { update(a[t1].z,a[t1].w); b[i]=a[t1++]; } else { ans[a[t2].id]+=query(a[t2].z); b[i]=a[t2++]; } } for (int i=l;i<=r;i++) a[i]=b[i]; }

# include <bits/stdc++.h> # define int long long # define lowbit(x) (x&(-x)) using namespace std; const int N=4e5+10; struct rec{ int x,y,z,w,id;}a[N],t[N],b[N]; int n,m,dfn,k,tot; int tim[N],ans[N],c[N],out[N]; inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } bool cmp1(rec a,rec b) { if (a.x!=b.x) return a.x<b.x; if (a.y!=b.y) return a.y<b.y; if (a.z!=b.z) return a.z<b.z; } bool same(rec a,rec b) { if (a.x==b.x&&a.y==b.y&&a.z==b.z) return true; else return false; } void update(int x,int y) { for (int i=x;i<=k;i+=lowbit(i)) { if (tim[i]!=dfn) tim[i]=dfn,c[i]=0; c[i]+=y; } } int query(int x) { int ret=0; for (int i=x;i;i-=lowbit(i)) if (tim[i]==dfn) ret+=c[i]; return ret; } void write(int x) { if (x>9) write(x/10); putchar('0'+x%10); } void cdq(int l,int r) { if (l==r) return; int mid=(l+r)>>1; cdq(l,mid);cdq(mid+1,r); dfn++; int t1=l,t2=mid+1; for (int i=l;i<=r;i++) { if ((t1<=mid&&a[t1].y<=a[t2].y)||t2>r) { update(a[t1].z,a[t1].w); b[i]=a[t1++]; } else { ans[a[t2].id]+=query(a[t2].z); b[i]=a[t2++]; } } for (int i=l;i<=r;i++) a[i]=b[i]; } signed main() { n=read();k=read(); for (int i=1;i<=n;i++) t[i].x=read(),t[i].y=read(),t[i].z=read(); sort(t+1,t+1+n,cmp1); int i=1,j; tot=0; while (i<=n){ j=i+1; while (j<=n&&same(t[i],t[j])) j++; j--; a[++tot]=t[i]; a[tot].w=(j-i+1); a[tot].id=tot; i=j+1; } cdq(1,tot); for (int i=1;i<=tot;i++) out[ans[a[i].id]+a[i].w-1]+=a[i].w; for (int i=0;i<n;i++) write(out[i]),putchar('\n'); return 0; }
C. 從三維偏序到三維偏序的應用
這一專題我們安排了兩道例題,請按照三維偏序的思想和方法,嘗試解決。
- 練習C1:[CQOI2011]動態逆序對
- 練習C2:[Violet]天使玩偶/SJY擺棋子
下面對於每一個例題講解:
例題C1:逆序對的定義:對於序列a[],取第$i$個元素和第$j$個元素,滿足$i <j$且$a[l]>a[r]$,對於這樣數對$(l,r)$被稱為一對逆序對 ,
給出一個[1,n]的排列,按照順序依次刪除m個數,詢問當前逆序對數量.不強制在線。
對於100%的數據 :$N\leq 10^5,M\leq 5\times 10^4$
Solution:可以把每次的答案分成兩個部分:原先存在的逆序對+加入這個數新產生的逆序對,
那么每次只要算出當前新產生的逆序對,最后算一遍前綴和即可。
考慮逆序對產生方式: 1.位置靠前且值比它大的,2.位置靠后且比它小的。
這個問題在於有插入操作,和刪除操作,比較難過
對於逆序對的產生是插入比他前的滿足1 or 2兩個條件的元素
對於逆序對的刪除是刪除時間比他后面的,滿足1 or 2兩個條件的元素。
顯然對於把時間作為第1維進行排序是不合適的(時間在前和后有不同的影響),所以我們考慮把操作的位置作為第1維,進行排序
對於時間這1維度作為第2維度CDQ分治,
設當前刪除了第i個元素,那么在[1,i]中比它大的都要減去,在[i+1,N]中比他小的都要減去。
正序循環的時候默認先加入樹狀數組的元素的位置都是在詢問元素的前面的,所以要減去的是比詢問元素大的那一個部分 [i+1,n]
倒序循環的時候默認先加入樹狀數組的元素的位置都是在詢問元素后面的,所以要減去的是比詢問元素小的那個部分[1,n-1]
同樣的考慮影響還是處於左邊時間修改對處於右邊時間的查詢造成的影響,注意同步清空樹狀數組.
Code :

# include <bits/stdc++.h> //# define int long long # define LL long long using namespace std; const int N=4e5+10; int n,m,tot; int o[N]; LL ans[N]; struct node{ int t,pos,x,op,idx; }a[N],b[N]; inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } void write(LL x) { if (x>9) write(x/10); putchar(x%10+'0'); } struct TreeArray{ # define lowbit(x) (x&(-x)) int c[N]; void update(int x,int y) { for (;x<=n;x+=lowbit(x)) c[x]+=y;} int query(int x) { int ret=0; for (;x;x-=lowbit(x)) ret+=c[x]; return ret;} #undef lowbit }tr; int Query(int l,int r){return tr.query(r)-tr.query(l-1);} bool cmp(node a,node b) { if (a.pos!=b.pos) return a.pos<b.pos; else return a.x<b.x; } void cdq(int l,int r) { if (l==r) return; int mid=(l+r)>>1; for (int i=l;i<=r;i++) if (a[i].t<=mid) tr.update(a[i].x,a[i].op); else ans[a[i].idx]+=(LL)a[i].op*Query(a[i].x+1,n); for (int i=l;i<=r;i++) if (a[i].t<=mid) tr.update(a[i].x,-a[i].op); for (int i=r;i>=l;i--) if (a[i].t<=mid) tr.update(a[i].x,a[i].op); else ans[a[i].idx]+=(LL)a[i].op*Query(1,a[i].x-1); for (int i=r;i>=l;i--) if (a[i].t<=mid) tr.update(a[i].x,-a[i].op); int t1=l,t2=mid+1; for (int i=l;i<=r;i++) if (a[i].t<=mid) b[t1++]=a[i]; else b[t2++]=a[i]; for (int i=l;i<=r;i++) a[i]=b[i]; cdq(l,mid); cdq(mid+1,r); } signed main() { n=read();m=read(); for (int i=1;i<=n;i++) { int t=read(); o[t]=i; a[++tot]=(node){tot,i,t,1,0}; } for (int i=1;i<=m;i++) { int t=read(); int pos=o[t]; a[++tot]=(node){tot,pos,t,-1,i}; } sort(a+1,a+1+tot,cmp); cdq(1,tot); for (int i=1;i<=m;i++) { ans[i]+=ans[i-1]; write(ans[i-1]); putchar('\n'); } return 0; }
例題C2:定義兩點距離為麥哈頓距離$ Dist(A,B) = |A_x - B_x|+|A_y-B_y| $
給出初始n個點的坐標 $(x_i,y_i)$ 有m個操作,
1 x y : 表示在(x,y)處新增1個點
2 x y : 表示詢問所以存在的點到點(x,y)最小距離
對於100%的數據 $n,m \leq 3 \times 10^5 , x_i,y_i \leq 10^6$
Solution:
如果點都在詢問點的左下方就好了...(這樣絕對值就直接消掉了)
However,人家有4個方向,怎么辦,考慮到對稱我們可以把所有點都做一次變換,稱為對稱
怎么個對稱法呢,關於x軸對稱,關於y軸對稱,關於原點對稱,對稱的意思是所有點的坐標發生變換,
以原點對稱為例,原來A在B的左下方,對稱后A就在B的右上方了,那么類似於 按原點B對稱的意思。
按照x軸對稱,按照y軸對稱同理。
接下來我們只討論點在點左下方的情況(變換以后跑4遍不就好了!)
計算lx,ly表示x,y坐標最大取值+1
把Dist絕對值打開得$ Dist(A,B) = |A_x - B_x|+|A_y-B_y| =A_x+A_y-(B_x+B_y)$
對於給定的A點,$A_x+A_y$一定,當$ Dist(A,B)$取到最小值的時候$B_x+B_y$最大
所以用樹狀數組維護前綴最大值就行。
這里時間t是默認有序的作為第1維,然后cdq分治第2維x,然后用樹狀數組維護一個y,前綴最大值
當分治求答案的時候左邊時間早於右邊,左邊影響右邊,第2維x經過歸並左邊小於右邊,所以為了保證在左下方還需y左邊小於右邊
所以求的時候求y的這個點前綴最大值即可.(注意如果沒有請不要更新為0)
寫了一個STD碼風的程序Code:

# include <cstdio> # include <cstring> # define fp(i,s,t) for (int i=s;i<=t;i++) using namespace std; const int N=1e6 + 10; struct rec{ int t,x,y,op,idx; bool operator < (const rec &t) const { return x < t.x; } }a[N],tt[N],t[N]; int tot,n,m,lx,ly,qes; int ans[N]; int max(int x , int y) {return x > y ? x : y;} int min(int x , int y) {return x > y ? y : x;} inline int read() { int X = 0,w = 0; char c = 0; while(c < '0' || c > '9') {w |= c=='-'; c = getchar();} while(c >= '0' && c <= '9') X=(X<<3) + (X<<1) + (c^48),c=getchar(); return w ? -X :X; } inline void write(int x) { if (x > 9) write(x / 10); putchar('0' + x % 10); } struct TreeArray{ # define lowbit(x) (x & (-x)) int c[N]; void Empty() {memset(c,0,sizeof(c));} void update(int x,int y) { for (;x <= ly;x += lowbit(x)) c[x] = max(c[x],y); } int query(int x) { int ret=0; for (;x;x -= lowbit(x)) ret = max(ret,c[x]); return ret; } void clear(int x) { for (;x <= ly;x += lowbit(x)) if (c[x]) c[x] = 0; } # undef lowbit }tr;//前綴最大值 void Merge(int l,int r) { int mid = l + r >> 1; int i = l,j = mid + 1,k = l; while (i <= mid && j <= r) { if (a[i] < a[j]) tt[k++] = a[i++]; else tt[k++] = a[j++]; } while (i <= mid) tt[k++] = a[i++]; while (j <= r) tt[k++] = a[j++]; }//歸並a[l,mid]和a[mid+1,r] void cdq(int l,int r) { if (l==r) return; int mid = l + r>>1; cdq(l,mid); cdq(mid+1,r); int j=l; for (int i = mid + 1;i <= r; i++) if (a[i].op==2) { for (;j <= mid&&a[j].x <= a[i].x; j++) { if (a[j].op==1) tr.update(a[j].y,a[j].x + a[j].y); } int tmp = tr.query(a[i].y); if (tmp) ans[a[i].idx] = min(ans[a[i].idx],a[i].x + a[i].y - tmp); //注意考慮0情況不要更新 } for (int i = l;i < j;i++) if (a[i].op==1) tr.clear(a[i].y); Merge(l,r); //等價於STL merge(a+l,a+mid+1,a+mid+1,a+r+1,tt+l); for (int i=l;i<=r;i++) a[i] = tt[i]; } int main() { n = read(); m = read(); fp(i,1,n) { int x = read() + 1, y=read() + 1; //有 0 的問題加 1 解決 t[++tot] = (rec) {tot , x , y , 1 , 0}; lx = max(lx,x); ly = max(ly,y); } fp(i,1,m) { int op = read(),x = read() + 1,y = read() + 1; //有 0 的問題加 1 解決 t[++tot] = (rec) {tot , x , y , op , (op==1) ? 0 : (++qes)}; lx = max(lx,x); ly = max(ly,y); } lx++; ly++; memset(ans, 0x3f ,sizeof(ans)); tr.Empty(); fp(i,1,tot) a[i] = t[i]; cdq(1,tot); tr.Empty(); fp(i,1,tot) a[i] = t[i] , a[i].x = lx - a[i].x; //x對稱 cdq(1,tot); tr.Empty(); fp(i,1,tot) a[i] = t[i] , a[i].y = ly - a[i].y; //y對稱 cdq(1,tot); tr.Empty(); fp(i,1,tot) a[i] = t[i] , a[i].x = lx - a[i].x , a[i].y = ly - a[i].y; //O對稱 cdq(1,tot); for (int i = 1;i <= qes; i++) write(ans[i]) , putchar('\n'); return 0; }
D.四維偏序(CDQ套CDQ)
下面給出四維偏序的模板題:
例題D1:現在有$n$個三元組$(a_i,b_i,c_i)$ 求有多少組三元組對$(i,j)$滿足 $ i<j, a_i<a_j , b_i<b_j , c_i<c_j$
你的程序必須輸出三元組對總對數。 數據保證a,b,c三個數組都是$[1,n]$排列。
對於100%的數據 $n \leq 5 \times 10^4$
(數據來自ljc20020730)
solution:我們類比二維偏序、三維偏序的方法考慮四維偏序的求解方法
首先把第1維排序,顯然是位置(這里沒記,原數組就是按照順序位置排布的)
然后對第2維進行CDQ分治,但是剩下還有2維怎么辦。
我們不妨再對第3維進行CDQ分治,然后第4維使用數據結構統計。
顯然當我們歸並第2維以后數組變成了按照第2維度排序的有序數組但是第1維時間是雜亂無章的。
然而我們只關心第1維是在左邊還是右邊,並不關心具體值,於是我們可以用Left和Right來標記a值(part)
既然b已經有序在CDQ套的cdq下面(不妨把正式求解的分治算法稱之為CDQ,CDQ中套如的子分治算法叫做cdq)
把原來CDQ中第2、3、4維換成之前做過三維偏序的1、2、3維就行了(同時注意更新時判斷,對於CDQ是不是完成了左邊更改對右邊查詢的影響)
cdq完成的目的是:在CDQ后前面部分對后面部分的影響。
step 1: 對第一維進行排序。
step 2:對第1維重新標號(left或right),然后對第2維分治,遞歸解決子問題,按照第2維的順序合並。此時只是單純的合並,並不進行統計。
step 3: 把合並后的序列復制一份,在復制的那一份中進行cdq分治。(這時第2維默認有序)即對第3維分治,遞歸解決子問題,按照第3維的順序合並。合並過程中用樹狀數組維護第4維的信息。
Code:
void cdq(int l,int r) //對於2,3,4維度重新cdq分治 { if (l==r) return; int mid=(l+r)>>1; cdq(l,mid); cdq(mid+1,r); int i=l,j=mid+1,k=l; while (i<=mid&&j<=r) { if (t1[i].b<t1[j].b) { //按照第3維歸並 if (t1[i].part==Left) update(t1[i].c); //更新的前提條件第1維在左邊,影響第1維的右邊 t2[k++]=t1[i++]; } else { if (t1[j].part==Right) ans+=query(t1[j].c); //答案累加的前提條件是第1維在右邊受到第1維左邊影響 t2[k++]=t1[j++]; } } while (i<=mid) t2[k++]=t1[i++]; while (j<=r) { if (t1[j].part==Right) ans+=query(t1[j].c); t2[k++]=t1[j++]; } fp(i,l,r) { if (t2[i].part==Left) clear(t2[i].c);//清空樹狀數組 t1[i]=t2[i]; } } void CDQ(int l,int r) //注意此時已經默認第1維有序我們不必記錄 { if (l==r) return; int mid=(l+r)>>1; CDQ(l,mid); CDQ(mid+1,r); int i=l,j=mid+1,k=l; while (i<=mid&&j<=r) { if (a[i].a<a[j].a) { a[i].part=Left; //對於第2維度a歸並,並記錄第1維是左邊(left)還是右邊(right) t1[k++]=a[i++]; } else { a[j].part=Right; t1[k++]=a[j++]; } } while (i<=mid) a[i].part=Left,t1[k++]=a[i++]; while (j<=r) a[j].part=Right,t1[k++]=a[j++]; fp(i,l,r) a[i]=t1[i]; //t1就是復制過去的數組 cdq(l,r); }

# include<bits/stdc++.h> # define int long long # define fp(i,s,t) for (int i=s;i<=t;i++) # define Left 0 # define Right 1 using namespace std; const int N=5e4+10; struct rec{ int a,b,c,part; }a[N],t1[N],t2[N]; int c[N]; int n,ans; # define lowbit(x) (x&(-x)) void update(int x){for (;x<=n;x+=lowbit(x))c[x]++;} int query(int x) { int ret=0; for (;x;x-=lowbit(x)) ret+=c[x]; return ret;} void clear(int x){for (;x<=n;x+=lowbit(x)) c[x]=0;} inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } void cdq(int l,int r) //對於2,3,4維度重新cdq分治 { if (l==r) return; int mid=(l+r)>>1; cdq(l,mid); cdq(mid+1,r); int i=l,j=mid+1,k=l; while (i<=mid&&j<=r) { if (t1[i].b<t1[j].b) { //按照第3維歸並 if (t1[i].part==Left) update(t1[i].c); //更新的前提條件第1維在左邊,影響第1維的右邊 t2[k++]=t1[i++]; } else { if (t1[j].part==Right) ans+=query(t1[j].c); //答案累加的前提條件是第1維在右邊受到第1維左邊影響 t2[k++]=t1[j++]; } } while (i<=mid) t2[k++]=t1[i++]; while (j<=r) { if (t1[j].part==Right) ans+=query(t1[j].c); t2[k++]=t1[j++]; } fp(i,l,r) { if (t2[i].part==Left) clear(t2[i].c);//清空樹狀數組 t1[i]=t2[i]; } } void CDQ(int l,int r) //注意此時已經默認第1維有序我們不必記錄 { if (l==r) return; int mid=(l+r)>>1; CDQ(l,mid); CDQ(mid+1,r); int i=l,j=mid+1,k=l; while (i<=mid&&j<=r) { if (a[i].a<a[j].a) { a[i].part=Left; //對於第2維度a歸並,並記錄第1維是左邊(left)還是右邊(right) t1[k++]=a[i++]; } else { a[j].part=Right; t1[k++]=a[j++]; } } while (i<=mid) a[i].part=Left,t1[k++]=a[i++]; while (j<=r) a[j].part=Right,t1[k++]=a[j++]; fp(i,l,r) a[i]=t1[i]; //t1就是復制過去的數組 cdq(l,r); } signed main() { n=read(); fp(i,1,n) a[i].a=read(); fp(i,1,n) a[i].b=read(); fp(i,1,n) a[i].c=read(); CDQ(1,n); printf("%lld\n",ans); return 0; }
E. CDQ系列問題時間復雜度分析:
給出 Master 定理:
定義T(n)為算法時間復雜度,對於規模為n的問題可以分為a個規模為$\frac{n}{b}$的子問題,
在在每一個子問題的解決中,其他運算的時間復雜度是$f(n)$,$c$是一個由$a,b$決定的數
定義$T(n)=aT(\frac{n}{b})+f(n)$ , 令$crit=log_b \ a$
$f(n)=O(n^c)$且$c<crit$時 , $T(n)=O(n^{crit})$
$\exists k \geq 0$使得$f(n)=O(n^{crit} {log_2}^k n)$那么$T(n)=O(n^{crit}{log_2}^{k+1} n)$
對於2維偏序問題時間復雜度是$O(n log_2 n)$ 如歸並排序求逆序對
對於3維偏序問題由於加上樹狀數組的維護時間復雜度是$O(n {log_2}^2 n)$ 如陌上花開
對於4維偏序的問題由於CDQ套CDQ還加上了樹狀數組維護復雜度是$O(n {log_2}^3 n)$ 如偏序[HZOI2016]
對於一般k維偏序問題,利用CDQ套...求解是時間復雜度是$O(n {log_2}^{k-1} n)$
上述結論可由Master定理求證。
尾聲:
到此處為止,$CDQ$分治的算法介紹已經基本完畢。
已經有了離線處理動態問題的思路:把動態問題按照時間、位置分治進行cdq優化
使時間復雜度降低,並且不需要高級數據結構如$K-D Tree$在線維護,簡便快速解決問題。
每降低一維的時間復雜度只需要用$log_n$的復雜度。
當然,只掌握這些內容是遠遠不夠的,需要強調cdq算法的限制:
1、必須離線操作
2、對區間更新問題解決有困難
3、代碼調試細節較多,請積極對拍驗證
(The End)