CQH分治與整體二分
CDH分治,核心思想就是對操作進行二分。感覺和我以前對操作分塊的思想很像啊,fhb分塊 ……(⊙o⊙)…
日常懶得寫模板的題解,轉載一篇(本家)
-----------------------------------------------------------分割線----------------------------------------------------------------------
在線/離線:首要考慮
在線算法: 可以以序列化的方式一個一個的處理輸入,不必事先知道所有輸入數據
離線算法: 必須事先知道所有的輸入數據
(例如選擇排序就是一個離線算法,而插入排序則不是)
眾所周知,現在遍地毒瘤高級數據結構題(以及在一些算法之中需要用高級數據結構來加速的題),各種樹(套樹)*,代碼量->INF,調試難度->INF,煩躁程度->INF,所幸在一些問題中我們可以利用分治的思想來解決之,最具有代表性的就是CDQ分治以及整體二分
如果題目強制要求在線的話(比如操作參數依賴於之前答案),只能乖乖地碼數據結構了(不過似乎有一種二進制分組的做法能化一些在線問題為離線),而如果題目沒有要求(或者你設計的算法不需要)在線的話,離線算法常常成為我們首要考慮的對象,CDQ分治和整體二分就是離線算法條件下可以運用的有力武器
CDQ分治
查詢的限制——序
對於一個數據結構題而言(或者需要運用數據結構的地方),我們無非就是做兩件操作,一是修改,二是查詢
對於修改而言,有插入,刪除,變更(其實等價於刪除再插入)這幾種方式
那麽查詢的本質是什麽呢
我們思考所遇到過的數據結構題,可以發現查詢實際上就在做一件事情:
把符合本次查詢的限制的修改對答案產生的效果合並起來
滿足這種限制通常表現為一種序的要求,並且這種序是廣義的,符合限制的操作往往是按某種序(或多種序)排序後的操作的前綴
通常來說,查詢一定有時間上的限制,也就是要求考慮發生在某個時刻之前的所有查詢,對於一個問題而言,假如所有查詢要求的發生時刻相同,那這就是一個靜態查詢問題,如果要求發生的時刻隨著查詢而變,那這就是一個動態修改問題,動態修改問題較靜態查詢而言復雜很多,往往需要高級數據結構,可持久化等手段,而靜態查詢簡單很多,例如時間倒流,twopointers
動態修改->靜態查詢
CDQ分治算法的核心就在於:去掉時間的限制,將所有查詢要求發生的時刻同化,化動態修改為靜態查詢
(其實對於有些問題來說可以把某一維的限制通過排序看作時間限制然後運用CDQ分治)
我們記過程DivideConquer(l,r)表示處理完[l,r]內的修改對查詢的影響
此時我們引入分治思想,將操作序列劃分為[l,mid],[mid+1,r]兩個區間
這兩個區間內部的修改對區間內部的查詢的影響是完全相同的子問題,我們遞歸處理
處理完之後剩下來只要考慮[l,mid]中的修改對[mid+1,r]中的查詢的影響
這時我們發現這其實已經變成了一個靜態查詢問題,因為所有的查詢都發生在修改之後,我們只需要考慮靜態查詢的問題如何處理即可
時間復雜度分析
假設我們處理前面部分的修改對後面部分的復雜度為O(f(n))
CDQ分治的復雜度就為O(f(n)logn)
也就是說CDQ分治用一個log的代價完成了動態到靜態
在處理靜態查詢的時候,我們往往需要對操作進行重新排序,如果直接做最後會多一個log,這時候我們有兩種手段,一是在CDQ分治開始之前就先將這一維有序化,通過從左往右掃分兩邊來保證時刻操作序列都這一維有序,另一種方法時每次分治都得到一個有序表,通過合並兩邊的有序表來得到新的有序表
整體二分
二分答案——整體二分的前身
首先對於一類查詢而言,我們要找的答案滿足二分性,例如區間第k大(統計權值然後二分答案),這時候我們就可以采用二分答案的方法來解決,二分答案是把計算問題轉化為判定問題的有效手段
二分答案的做法是不斷維護一個可能的答案區間[l,r],每次二分,我們先求出當前的判定答案mid=(l+r)/2,然後我們統計在當前標準下會對查詢產生貢獻的修改(例如參數≤mid)的貢獻和,我們再比較現在的貢獻和與我們想要的貢獻和的大小,如果貢獻和已經超過我們想要的貢獻和了,說明符合標準的修改太多了,我們需要緊縮標準(將答案區間變為l,mid),否則我們需要放寬標準(將答案區間變為mid+1,r),
所有操作的二分——從單個到整體
對於單個查詢而言,我們可以采用預處理+二分答案的方法解決,但往往我們要回答的是一系列的查詢,對於每個查詢而言我們都要重新預處理然後二分,時間復雜度無法承受,但是我們仍然希望通過二分答案的思想來解決,整體二分就是基於這樣一種想法——我們將所有操作(包括修改和查詢)一起二分,進行分治
整體二分具體的做法比較難理解,我先把偽代碼給出來
Divide_Conquer(Q, AL, AR) //Q是當前處理的操作序列 //WANT是要求的貢獻,CURRENT為已經累計的貢獻(記錄的是1~AL-1內所有修改的貢獻) //[AL, AR]是詢問的答案範圍區間 if AL = AR then 將Q中所有是詢問操作的答案設為AL end if //我們二分答案,AM為當前的判定答案 AM = (AL+AR) / 2 //Solve是主處理函數,只考慮參數滿足判定標準[AL, AM]的修改的貢獻,因為CURRENT域中已經記錄了[1,AL-1]的修改的貢獻了,這一步是保證時間復雜度的關鍵,因為SOLVE只於當前Q的長度有關,而不與整個操作序列的長度有線性關系,這保證了主定理解出來只多一個log Solve(Q, AL, AM) //Solve之後Q中各個參數滿足判定標準的修改對詢問的貢獻被存儲在ANS數組 //Q1,Q2為了兩個臨時數組,用於劃分操作序列 for i = 1 to Length(Q) do if (Q[i].WANT <= Q[i].CURRENT + ANS[i]) then //當前已有貢獻不小於要求貢獻,說明最終答案應當不大於判定答案 向數組Q1末尾添加Q[i] else //當前已有貢獻小於要求貢獻,說明最終答案應當大於判定答案 //這裏是整體二分的關鍵,把當前貢獻累計入總貢獻,以後不再重復統計! Q[i].CURRENT = Q[i].CURRENT + ANS[i] 向數組Q2末尾添加Q[i] end if end for //分治,遞歸處理 Divide_Conquer(Q1, AL, AM) Divide_Conquer(Q2, AM+1, AR)
我們時刻維護一個操作序列和對應的可能答案區間[AL,AR]
我們先求得一個判定答案AM=(AL+AR)/2
然後我們考慮操作序列的修改操作,將其中符合標準(例如參數<=AM)的修改對各個詢問的貢獻統計出來
然後我們對操作序列進行劃分
第一類操作是查詢
如果當前查詢累計貢獻比要求貢獻大,說明AM過大,滿足標準的修改過多,我們需要給這中查詢設置更小的答案區間來緊縮標準,於是將它劃分到答案區間[AL,AM]中(這種情況我們不改變查詢的CURRENT域,保證了繼續下一次分治時這些查詢的CURRENT域還是累計的[1,AL−1]的修改的貢獻)
否則我們將當前已經統計到的貢獻更新,將它劃分到答案區間[AM+1,AR](這種情況下我們將[AL,AM]內的修改的貢獻更新了CURRENT域,保證了下次繼續分治時這些查詢的CURRENT域已經保留的是[1,AM]的貢獻了)
第二類操作是修改
假如它符合當前的標準,已經被統計入了貢獻,那麽它對於答案區間是[AM+1,AR]的查詢來說已經沒有意義了(因為我們知道它一定會對這些查詢產生貢獻,並且我們已經累計了這種貢獻到CURRENT域中),我們就把它劃分到[AL,AM]的區間裏,
對於不符合當前的標準,未被統計入貢獻的修改來說,如果我們放寬標準,它仍然可能起貢獻,然而我們並未統計這種貢獻,因此對於[AM+1,AR]的區間來說它仍具有考慮的意義,我們把它劃分到[AM+1,AR]中
劃分好了操作序列之後就繼續分治遞歸下去就可以了
至此整體二分結束
時間復雜度分析
和CDQ分治一樣,整體二分的代價也是O(f(n)logn)
--------------------------------------------------------分割線-----------------------------------------------------------------------------
1.三維偏序
我們通過排序得到第一維,CDQ分治第二維,樹狀數組統計第三維就好了。
#include<bits/stdc++.h> #define sight(c) (‘0‘<=c&&c<=‘9‘) #define N 200007 inline void read(int &x){ static char c; for (c=getchar();!sight(c);c=getchar()); for (x=0;sight(c);c=getchar())x=x*10+c-48; } void write(int x){if (x<10) {putchar(‘0‘+x); return;} write(x/10); putchar(‘0‘+x%10);} inline void writeln(int x){if (x<0) x*=-1,putchar(‘-‘); write(x); putchar(‘\n‘); } using namespace std; struct Node{ int a,b,c,id; inline bool operator <(const Node& A)const{ if (a==A.a) { if (b==A.b) return c<A.c; return b<A.b; } return a<A.a; } inline bool operator ==(const Node& A)const{ return A.a==a&&A.b==b&&A.c==c; } }p[N>>1],a[N>>1]; struct Tre{ #define L(x) x&-x int s[N]; void in(int x,int dla) {for (;x<N;x+=L(x)) s[x]+=dla;} int ask(int x) {static int L;for (L=0;x;x-=L(x)) L+=s[x]; return L;} void clear() {memset(s,0,sizeof s);} #undef L }Tree; int ans[N>>1],n,k,tot,id[N>>1]; inline bool cmp(const Node &x,const Node &y){ if (x.b==y.b) return x.id<y.id; return x.b<y.b; } #define Mid ((l+r)>>1) void cqh(int l,int r){ if (l==r) return; for (int i=l;i<=r;i++) p[i]=a[i],p[i].id=i; sort(p+l,p+r+1,cmp); for (int i=l;i<=r;i++) if (p[i].id<=Mid) Tree.in(p[i].c,1); else ans[a[p[i].id].id]+=Tree.ask(p[i].c); for (int i=l;i<=r;i++) if (p[i].id<=Mid) Tree.in(p[i].c,-1); cqh(l,Mid); cqh(Mid+1,r); } int main () { read(n); read(k); for (int i=1;i<=n;i++) read(a[i].a),read(a[i].b),read(a[i].c),a[i].id=i; sort(a+1,a+n+1); for (int i=n-1;i;i--) { if (a[i]==a[i+1]) tot++; else tot=0; ans[a[i].id]=tot; } cqh(1,n); for (int i=1;i<=n;i++) id[ans[i]]++; for (int i=0;i<n;i++) writeln(id[i]); return 0; }
2.三維偏序最長鏈
我們把時間當做第一維,我們考慮如何維護最長鏈。我們使用樹狀數組,用max取代+操作。重置操作在拓展過的節點遍歷置0。(不要用memset)。
我們還要註意先分治(l,mid)再合並再分治(mid+1,r)。
#include<bits/stdc++.h> #define sight(c) (‘0‘<=c&&c<=‘9‘) #define N 307007 inline void read(int &x){ static char c;static int b; for (b=1,c=getchar();!sight(c);c=getchar())if (c==‘-‘) b=-1; for (x=0;sight(c);c=getchar())x=x*10+c-48; x*=b; } void write(int x){if (x<10) {putchar(‘0‘+x); return;} write(x/10); putchar(‘0‘+x%10);} inline void writeln(int x){if (x<0) x*=-1,putchar(‘-‘); write(x); putchar(‘\n‘); } using namespace std; struct Node{ int a,b,c; }p[N>>1],a[N>>1]; struct Tre{ #define max(a,b) (a>b?a:b) #define L(x) x&-x int s[N]; void in(int x,int dla) {for (;x<N;x+=L(x)) s[x]=max(dla,s[x]);} int ask(int x) {static int L;for (L=0;x;x-=L(x)) L=max(s[x],L); return L;} void clear(int x) {for (;x<N;x+=L(x)) s[x]=0;} #undef L #undef max }Tree; int ans[N>>1],n,k,tot,T; vector<int> Q; void Li() { for(int i=1;i<=n;i++) Q.push_back(a[i].c); sort(Q.begin(),Q.end()); for(int i=1;i<=n;i++) a[i].c=lower_bound(Q.begin(),Q.end(),a[i].c)-Q.begin()+1; } inline bool cmp(const Node &x,const Node &y){ if (x.b^y.b) return x.b<y.b; return x.a>y.a; } #define Mid (l+r>>1) void cqh(int l,int r){ if (l==r) return; cqh(l,Mid); for (int i=l;i<=r;i++) p[i]=a[i]; sort(p+l,p+r+1,cmp); for (int i=l;i<=r;i++) if (p[i].a<=Mid) Tree.in(p[i].c,ans[p[i].a]); else ans[p[i].a]=max(Tree.ask(p[i].c-1)+1,ans[p[i].a]); for (int i=l;i<=r;i++) if (p[i].a<=Mid) Tree.clear(p[i].c); // for (int i=l;i<=Mid;i++) Tree.clear(a[i].c); cqh(Mid+1,r); } //void cqh(int l,int r){ // if (l==r) return; // cqh(l,Mid); // for (int i=l;i<=r;i++) p[i]=a[i]; // sort(p+l,p+Mid+1,cmp); sort(p+Mid+1,p+r+1,cmp); // for (int i=Mid+1,j=l;i<=r;i++) { // for (;j<=Mid&&p[j].b<p[i].b;j++) Tree.in(p[j].c,ans[p[j].a]); // ans[p[i].a]=max(ans[p[i].a],Tree.ask(p[i].c-1)+1); // } // for (int i=l;i<=Mid;i++) Tree.clear(p[i].c); // cqh(Mid+1,r); //} int main () { read(n); for (int i=1;i<=n;i++) read(a[i].b),read(a[i].c),a[i].a=i,ans[i]=1; Li(); cqh(1,n); int Ans=0; for (int i=1;i<=n;i++) Ans=max(Ans,ans[i]); writeln(Ans); return 0; } //兩個cqh函數都是對的,只是不同的實現而已。
其實CDQ是可以拓展的。即使某些操作之間的貢獻會互相影響,只要其滿足可加性,我們也可以用CDQ加以解決。
CQH分治與整體二分