前言ios
辣雞蒟蒻__stdcall終於會CDQ分治啦!git
CDQ分治是咱們處理各種問題的重要武器。它的優點在於能夠頂替複雜的高級數據結構,並且常數比較小;缺點在於必須離線操做。數組
CDQ分治的基本思想和實現都很簡單,可是由於沒有人給本蒟蒻詳講,因此我對着幾篇論文頭疼了一個下午,最終在menci和sxysxy大佬的幫助下學會了CDQ分治。本文介紹一些很是simple的CDQ分治問題,目的在於幫助新手更快地入門CDQ分治,但願對你們有幫助。數據結構
轉載請註明做者:__stdcall。工具
基本思想spa
CDQ分治的基本思想十分簡單。以下:code
這就是CDQ分治的基本思想。和普通分治不一樣的地方在於,普通分治在合併兩個子問題的過程當中,[L,M]內的問題不會對[M+1,R]內的問題產生影響。blog
具體實現和用途排序
二維偏序問題教程
給定N個有序對(a,b),求對於每一個(a,b),知足a2<a且b2<b的有序對(a2,b2)有多少個。
咱們從歸併排序求逆序對來引入二維偏序問題。
回憶一下歸併排序求逆序對的過程,咱們在合併兩個子區間的時候,要考慮到左邊區間的對右邊區間的影響。即,咱們每次從右邊區間的有序序列中取出一個元素的時候,要把「以這個元素結尾的逆序對的個數」加上「左邊區間有多少個元素比他大」。這是一個典型的CDQ分治的過程。
如今咱們把這個問題拓展到二維偏序問題。在歸併排序求逆序對的過程當中,每一個元素能夠用一個有序對(a,b)表示,其中a表示數組中的位置,b表示該位置對應的值。咱們求的就是「對於每一個有序對(a,b),有多少個有序對(a2,b2)知足a2<a且b2>b」,這就是一個二維偏序問題。
注意到在求逆序對的問題中,a元素是默認有序的,即咱們拿到元素的時候,數組中的元素是默認從第一個到最後一個按順序排列的,因此咱們才能在合併子問題的時候忽略a元素帶來的影響。由於咱們在合併兩個子問題的過程當中,左邊區間的元素必定出如今右邊區間的元素以前,即左邊區間的元素的a都小於右邊區間元素的a。
那麼對於二維偏序問題,咱們在拿到全部有序對(a,b)的時候,先把a元素從小到大排序。這時候問題就變成了「求順序對」,由於a元素已經有序,能夠忽略a元素帶來的影響,和「求逆序對」的問題是同樣的。
考慮二維偏序問題的另外一種解法,用樹狀數組代替CDQ分治,即經常使用的用樹狀數組求順序對。在按照a元素排序以後,咱們對於整個序列從左到右掃描,每次掃描到一個有序對,求出「掃描過的有序對中,有多少個有序對的b值小於當前b值」,能夠用 權值樹狀數組/權值線段樹 實現。然而當b的值很是大的時候,空間和時間上就會吃不消,即可以用CDQ分治代替,就是咱們所說的「頂替複雜的高級數據結構」。別急,一下子咱們會看到CDQ分治在這方面更大的用途。
二維偏序問題的拓展
給定一個N個元素的序列a,初始值所有爲0,對這個序列進行如下兩種操做:
操做1:格式爲1 x k,把位置x的元素加上k(位置從1標號到N)。
操做2:格式爲2 x y,求出區間[x,y]內全部元素的和。
這是一個經典的樹狀數組問題,能夠毫無壓力地秒掉,如今,咱們用CDQ分治解決它——帶修改和查詢的問題。
咱們把他轉化成一個二維偏序問題,每一個操做用一個有序對(a,b)表示,其中a表示操做到來的時間,b表示操做的位置,時間是默認有序的,因此咱們在合併子問題的過程當中,就按照b從小到大的順序合併。
問題來了:如何表示修改與查詢?
具體細節請參見代碼,這裏對代碼作一些解釋,請配合代碼來看。咱們定義結構體Query包含3個元素:type,idx,val,其中idx表示操做的位置,type爲1表示修改,val表示「加上的值」。而對於查詢,咱們用前綴和的思想把他分解成兩個操做:sum[1,y]-sum[1,x-1],即分解成兩次前綴和的查詢。在合併的過程當中,type爲2表示遇到了一個查詢的左端點x-1,須要把該查詢的結果減去當前「加上的值的前綴和」,type爲3表示遇到了一個查詢的右端點y,須要把查詢的結果加上當前「加上的值的前綴和」,val表示「是第幾個查詢」。這樣,咱們就把每一個操做轉換成了帶有附加信息的有序對(時間,位置),而後對整個序列進行CDQ分治。
有幾點須要注意:
代碼以下:
1 #include <iostream> 2 #include <cstring> 3 #include <algorithm> 4 #include <cstdio> 5 #include <cstdlib> 6 #include <cmath> 7 8 using namespace std; 9 typedef long long ll; 10 const int MAXN = 500001; // 原數組大小 11 const int MAXM = 500001; // 操做數量 12 const int MAXQ = (MAXM<<1)+MAXN; 13 14 int n,m; 15 16 struct Query { 17 int type, idx; ll val; 18 bool operator<( const Query &rhs ) const { // 按照位置從小到大排序,修改優先於查詢 19 return idx == rhs.idx ? type < rhs.type : idx < rhs.idx; 20 } 21 }query[MAXQ]; 22 int qidx = 0; 23 24 ll ans[MAXQ]; int aidx = 0; // 答案數組 25 26 Query tmp[MAXQ]; // 歸併用臨時數組 27 void cdq( int L, int R ) { 28 if( R-L <= 1 ) return; 29 int M = (L+R)>>1; cdq(L,M); cdq(M,R); 30 ll sum = 0; 31 int p = L, q = M, o = 0; 32 while( p < M && q < R ) { 33 if( query[p] < query[q] ) { // 只統計左邊區間內的修改值 34 if( query[p].type == 1 ) sum += query[p].val; 35 tmp[o++] = query[p++]; 36 } 37 else { // 只修改右邊區間內的查詢結果 38 if( query[q].type == 2 ) ans[query[q].val] -= sum; 39 else if( query[q].type == 3 ) ans[query[q].val] += sum; 40 tmp[o++] = query[q++]; 41 } 42 } 43 while( p < M ) tmp[o++] = query[p++]; 44 while( q < R ) { 45 if( query[q].type == 2 ) ans[query[q].val] -= sum; 46 else if( query[q].type == 3 ) ans[query[q].val] += sum; 47 tmp[o++] = query[q++]; 48 } 49 for( int i = 0; i < o; ++i ) query[i+L] = tmp[i]; 50 } 51 52 int main() { 53 scanf( "%d%d", &n, &m ); 54 for( int i = 1; i <= n; ++i ) { // 把初始元素變爲修改操做 55 query[qidx].idx = i; query[qidx].type = 1; 56 scanf( "%lld", &query[qidx].val ); ++qidx; 57 } 58 for( int i = 0; i < m; ++i ) { 59 int type; scanf( "%d", &type ); 60 query[qidx].type = type; 61 if( type == 1 ) scanf( "%d%lld", &query[qidx].idx, &query[qidx].val ); 62 else { // 把查詢操做分爲兩部分 63 int l,r; scanf( "%d%d", &l, &r ); 64 query[qidx].idx = l-1; query[qidx].val = aidx; ++qidx; 65 query[qidx].type = 3; query[qidx].idx = r; query[qidx].val = aidx; ++aidx; 66 } 67 ++qidx; 68 } 69 cdq(0,qidx); 70 for( int i = 0; i < aidx; ++i ) printf( "%lld\n", ans[i] ); 71 return 0; 72 }
三維偏序問題
給定N個有序三元組(a,b,c),求對於每一個三元組(a,b,c),有多少個三元組(a2,b2,c2)知足a2<a且b2<b且c2<c。
不用CDQ分治的方法:先按照a元素排序,從左到右掃描。按照b元素構造權值樹狀數組,樹狀數組每一個節點按照c元素構造平衡樹。樹套樹的解法不只常數大,並且代碼量巨大,還容易寫錯。
相似二維偏序問題,先按照a元素從小到大排序,忽略a元素的影響。而後CDQ分治,按照b元素從小到大的順序進行歸併操做。可是這時候沒辦法像 求逆序對 同樣簡單地統計 個數 了,c元素如何處理呢?
這時候比較好的方案就是藉助權值樹狀數組。每次從右邊的序列中取出三元組(a,b,c)時,對樹狀數組查詢c值小於(a,b,c)的三元組有多少個;每次從左邊序列取出三元組(a,b,c)的時候,根據c值在樹狀數組中進行修改。注意,每次使用完樹狀數組記得把樹狀數組歸零!詳細代碼我會放在下面一道例題中。
三維偏序問題的拓展
平面上有N個點,每一個點的橫縱座標在[0,1e7]之間,有M個詢問,每一個詢問爲查詢在指定矩形以內有多少個點,矩形用(x1,y1,x2,y2)的方式給出,其中(x1,y1)爲左下角座標,(x2,y2)爲右上角座標。
不用CDQ分治的話能夠用二維線段樹或者二維樹狀數組來作,然而空間是明顯吃不消的。用CDQ分治如何作呢?
到這裏你們應該比較清楚了吧,把每一個點的位置變成一個修改操做,用三元組(時間,橫座標,縱座標)來表示,把每一個查詢分解成4個前綴和查詢,一樣用三元組來表示。對於修改操做,每一個三元組沒有附加信息;對於查詢操做,每一個三元組的附加信息爲「第幾個查詢」和「對結果的影響是+仍是-,用+1表示+,用-1表示-」。操做到來的時間是默認有序的,分治過程當中按照橫座標從小到大排序,用樹狀數組維護縱座標的信息。代碼以下:
1 #include <iostream> 2 #include <cstring> 3 #include <algorithm> 4 #include <cstdio> 5 #include <cmath> 6 #include <cstdlib> 7 #include <cctype> 8 9 using namespace std; 10 const int MAXN = 500001; // 點的數量 11 const int MAXM = 500001; // 詢問數量 12 const int MAXQ = MAXN+(MAXM<<2); 13 const int MAXL = 10000002; // 樹狀數組大小 14 15 int n, m, maxy = -1; 16 17 namespace IO { // 快讀相關 18 const int BUFSZ = 1e7; 19 char buf[BUFSZ]; int idx, end; 20 void init() { idx = BUFSZ; } 21 char getch() { 22 if( idx == BUFSZ ) { 23 end = fread( buf, 1, BUFSZ, stdin ); idx = 0; 24 } 25 if( idx == end ) return EOF; 26 return buf[idx++]; 27 } 28 int getint() { 29 int num = 0; char ch; 30 while( isspace(ch=getch()) ); 31 do { num = num*10 + ch-'0'; } while( isdigit(ch=getch()) ); 32 return num; 33 } 34 } 35 using IO::getint; 36 37 struct Query { 38 int type, x, y, w, aid; // w表示對查詢結果貢獻(+仍是-),aid是「第幾個查詢」 39 bool operator<( const Query &rhs ) const { 40 return x == rhs.x ? type < rhs.type : x < rhs.x; 41 } 42 }query[MAXQ]; 43 int qidx = 0; 44 void addq( int type, int x, int y, int w, int aid ) { 45 query[qidx++] = (Query){type,x,y,w,aid}; 46 } 47 48 int ans[MAXM], aidx = 0; 49 50 namespace BIT { // 樹狀數組相關 51 int arr[MAXL]; 52 inline int lowbit( int num ) { return num&(-num); } 53 void add( int idx, int val ) { 54 while( idx <= maxy ) { 55 arr[idx] += val; 56 idx += lowbit(idx); 57 } 58 } 59 int query( int idx ) { 60 int ans = 0; 61 while( idx ) { 62 ans += arr[idx]; 63 idx -= lowbit(idx); 64 } 65 return ans; 66 } 67 void clear( int idx ){ 68 while( idx <= maxy ) { 69 if( arr[idx] ) arr[idx] = 0; else break; 70 idx += lowbit(idx); 71 } 72 } 73 } 74 75 Query tmp[MAXQ]; 76 void cdq( int L, int R ) { 77 if( R-L <= 1 ) return; 78 int M = (L+R)>>1; cdq(L,M); cdq(M,R); 79 int p = L, q = M, o = L; 80 while( p < M && q < R ) { 81 if( query[p] < query[q] ) { 82 if( query[p].type == 0 ) BIT::add( query[p].y, 1 ); 83 tmp[o++] = query[p++]; 84 } else { 85 if( query[q].type == 1 ) ans[query[q].aid] += query[q].w * BIT::query( query[q].y ); 86 tmp[o++] = query[q++]; 87 } 88 } 89 while( p < M ) tmp[o++] = query[p++]; 90 while( q < R ) { 91 if( query[q].type == 1 ) ans[query[q].aid] += query[q].w * BIT::query( query[q].y ); 92 tmp[o++] = query[q++]; 93 } 94 for( int i = L; i < R; ++i ) { 95 BIT::clear( tmp[i].y ); // 清空樹狀數組 96 query[i] = tmp[i]; 97 } 98 } 99 100 int main() { 101 IO::init(); n = getint(); m = getint(); 102 while( n-- ) { 103 int x,y; x = getint(); y = getint(); ++x; ++y; // 爲了方便,把座標轉化爲[1,1e7+1] 104 addq(0,x,y,0,0); maxy = max( maxy, y ); // 修改操做無附加信息 105 } 106 while( m-- ) { 107 int x1,y1,x2,y2; x1 = getint(); y1 = getint(); x2 = getint(); y2 = getint(); ++x1; ++y1; ++x2; ++y2; 108 addq(1,x1-1,y1-1,1,aidx); addq(1,x1-1,y2,-1,aidx); addq(1,x2,y1-1,-1,aidx); addq(1,x2,y2,1,aidx); ++aidx; 109 maxy = max( maxy, max(y1,y2) ); 110 } 111 cdq(0,qidx); 112 for( int i = 0; i < aidx; ++i ) printf( "%d\n", ans[i] ); 113 return 0; 114 }
總結
對於經典的多維偏序問題和多維數據結構的查詢和修改,咱們能夠用一步步「降維」的方式解決。排序,數據結構,CDQ分治都是咱們降維的工具。
CDQ分治還有其餘不少強大的功能,好比多重嵌套CDQ分治,用CDQ分治加速動態規劃等等。總的來講就是能夠頂一層數據結構,降維用。因爲本文是面向我這樣的新手的教程,並且我也沒有學這些用法(我好弱啊QAQ),因此對於這些更難一點的問題不做介紹。
習題(參考menci博客)
園丁的煩惱 SHOI2007 BZOJ 1935
【模板】樹狀數組 1 luogu P3374
Mokia BZOJ 1176
陌上花開 BZOJ 3262
簡單題BZOJ 2683
動態逆序對 CQOI2011 BZOJ 3295