1. 程式人生 > >【轉】cdq分治

【轉】cdq分治

def pre 第一個 scan bar 行修改 代碼量 sdi ron

https://wenku.baidu.com/view/860cfcd976a20029bd642d9c?mark_pay_doc=0&mark_rec_position=10&clear_uda_param=1#13&isbtn=2

https://www.cnblogs.com/mlystdcall/p/6219421.html

前言

  辣雞蒟蒻__stdcall終於會CDQ分治啦!

CDQ分治是我們處理各類問題的重要武器。它的優勢在於可以頂替復雜的高級數據結構,而且常數比較小;缺點在於必須離線操作。

  CDQ分治的基本思想和實現都很簡單,但是因為沒有人給本蒟蒻詳講,所以我對著幾篇論文頭疼了一個下午,最終在menci和sxysxy大佬的幫助下學會了CDQ分治。本文介紹一些非常simple的CDQ分治問題,目的在於幫助新手更快地入門CDQ分治,希望對大家有幫助。

  轉載請註明作者:__stdcall。

基本思想

  CDQ分治的基本思想十分簡單。如下:

  1. 我們要解決一系列問題,這些問題一般包含修改和查詢操作,可以把這些問題排成一個序列,用一個區間[L,R]表示。
  2. 分。遞歸處理左邊區間[L,M]和右邊區間[M+1,R]的問題。
  3. 治。合並兩個子問題,同時考慮到[L,M]內的修改對[M+1,R]內的查詢產生的影響。即,用左邊的子問題幫助解決右邊的子問題。

  這就是CDQ分治的基本思想。和普通分治不同的地方在於,普通分治在合並兩個子問題的過程中,[L,M]內的問題不會對[M+1,R]內的問題產生影響。

具體實現和用途

  二維偏序問題

  給定N個有序對(a,b),求對於每個(a,b),滿足a2<ab2<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. 對於位置相同的操作,要先修改後查詢。
  2. 代碼中為了方便,使用左閉右開區間。
  3. 合並問題的時候統計“加上的值的前綴和”,只能統計左邊區間內的修改操作,改動查詢結果的時候,只能修改右邊區間內的查詢結果。因為只有左邊區間內的修改值對右邊區間內的查詢結果的影響還沒有統計。
  4. 代碼中,給定的數組是有初始值的,可以把每個初始值變為一個修改操作。

  代碼如下:

技術分享圖片
 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<ab2<bc2<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

【轉】cdq分治