【基礎操作】整體二分概述


整體二分是一個常數小的離線做法。

這篇講 $CDQ$ 的文章里提到了其一個分支——整體二分。

 

整體二分的適用性

有一些問題,在有多組操作(一開始賦初值也算操作)但只有一組詢問的情況下(當然這組詢問正常情況下就放在最后的,不然它后面的操作是擺着玩的),可以二分這個詢問的答案。

二分的時間復雜度是 $O(log(n))$,驗證一個答案是大了還是小了的時間復雜度是 $O(n)$(或者是這個級別的,差不多不到 $O(n^2)$ 就行),總時間復雜度是 $O(n\times log(n))$(或者是這個級別的)。

對於很多組詢問的情況,設詢問組數為 $Q$,如果對每組都二分,時間復雜度是 $O(Q\times n\times log(n))$ 級別的,對於祖傳數據(全 $100000$)直接升天。

然后我們發現,有一些問題的修改對詢問的影響非常有特性,支持高效維護和查詢。這時候就可以把所有操作(包括修改和查詢)放到一塊二分。

總之,整體二分只能在滿足以下條件的情況下使用:

  - 單組詢問可以二分

  - 存在高效的數據結構維護修改對詢問的影響(了解整體二分的應該知道,像區間修改這種的就不存在)

  - 題目可以離線做(廢話)

  ……

 

主席樹(模板)

詢問靜態區間第 $k$ 小。$n,Q\le 200000$。

題解

其實也是個整體二分模板題。

如果只有一組詢問,我們可以二分答案,判斷的話就掃一遍詢問區間,看有幾個數比這個答案小,就可以確定這個答案是區間第幾小了。

對於多組詢問的話,我們可以把所有操作扔到一起二分。

具體是什么意思呢?

舉個例子,給出一個數 $x$,在二分答案 $mid$ 時,它只會對所有大於 $mid$ 的答案造成影響。也就是說它跟詢問一樣,可以被二分。

 

下面說說具體操作:

我們按輸入的順序,依次模擬當前操作區間的操作。(在模擬當前區間的所有操作之前,把之前二分時記錄的信息都清空,即假裝之前啥操作也沒做過)

如果這個操作是賦值操作(在這題就是一開始給數組賦值),

  如果賦的數小於等於 $mid$,設這個數為 $x$,把它在樹狀數組/線段樹的第 $x$ 位 $+1$,並把這個操作扔到左遞歸區間(即 $[l,mid]$ 答案區間);

  如果賦的數大於 $mid$,直接把這個操作扔到右遞歸區間(即 $(mid,r]$ 區間)。

如果這個操作是詢問操作,

  先用樹狀數組/線段樹對其詢問區間求和(這就是之前為什么說在問題比較簡單時 才適合整體二分,你得確保能再用個數據結構維護),也就是查其詢問區間當前有多少個小於等於 $mid$ 的數,這里設有 $x$ 個。

  如果 $x$ 小於該查詢的 $k$(即這個查詢問的是區間第 $k$ 小),就把該詢問的 $k$ 減去 $x$(左遞歸區間有足夠的數小於 $mid$,這個詢問的答案顯然在右遞歸區間,於是減去左區間的數的影響),並把這個操作扔到右遞歸區間;

  如果 $x$ 大於等於該查詢的 $k$,直接把這個操作扔到左遞歸區間。

如果二分答案的區間 $[l,r]$ 遞歸到了 $l=r$,就把扔到這個遞歸區間的所有詢問操作的答案都賦為 $l$ 或 $r$,回溯。

 

$Q1:$ 為什么對於賦值操作,賦的數小於等於 $mid$ 時,要把操作扔到左遞歸區間?

$A1:$ 因為我們在詢問操作中,把要遞歸到右子區間的詢問 減掉了小於等於 $mid$ 的數對詢問的影響,也就是說這個賦值操作以后不用管遞歸到右子區間的詢問操作了,於是扔到左子區間即可。

$Q2:$ 那賦的數大於 $mid$ 時,要把操作扔到右遞歸區間?

$A2:$ 因為這個數不會對詢問答案在左子區間的詢問造成影響,它不會參與這些詢問的排名。

其它證明都比較顯然了吧(其實是我沒時間寫)

 

對於這題,如果不離散化的話,只能用權值線段樹;如果離散化的話,樹狀數組和權值線段樹就都可以用了。

注意一個事情,一開始就要把所有操作的數改為離散化后的數,不要在整體二分里再對每個數套 $map$ 查詢,因為整體二分的時間復雜度頂多做到 $O(n\times log^2(n))$,再套個 $map$ 的 $log$ 可能會爆炸。

 

 1 #include<bits/stdc++.h>
 2 #define rep(i,x,y) for(int i=x;i<=y;++i)
 3 #define dwn(i,x,y) for(int i=x;i>=y;--i)
 4 #define ll long long
 5 #define N 400001
 6 using namespace std;
 7 inline int read(){
 8     int x=0; bool f=1; char c=getchar();
 9     for(;!isdigit(c);c=getchar()) if(c=='-') f=0;
10     for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^'0');
11     if(f) return x;
12     return 0-x;
13 }
14 int n,m,a[N],b[N],ans[N],cnt;
15 map<int,int>mp; int lsh[N];
16 struct query{int x,l,r,k;}q[N],L[N],R[N];
17 namespace Fwk{
18     int fwk[N];
19     inline int lowbit(int x){return x&(-x);}
20     inline void add(int x,int v){
21         for(;x<=n;x+=lowbit(x)) fwk[x]+=v;
22     }
23     inline int query(int x){
24         int ret=0;
25         for(;x>0;x-=lowbit(x)) ret+=fwk[x];
26         return ret;
27     }
28 };
29 void solve(int ql,int qr,int l,int r){
30     //printf("solve:%d %d %d %d\n",ql,qr,l,r);
31     if(l==r){
32         rep(i,ql,qr) if(q[i].x) ans[q[i].x]=l;
33         return;
34     }
35     int mid=(l+r)>>1,cntL=0,cntR=0,tmp;
36     rep(i,ql,qr){
37         //cout<<"faq:"<<Fwk::query(2)<<' '<<Fwk::query(0)<<endl;
38         if(!q[i].x){
39             if(q[i].k<=mid) L[++cntL]=q[i], Fwk::add(q[i].l,1);
40             else R[++cntR]=q[i];
41             //printf("1:%d %d %d\n",q[i].l,q[i].k,mid);
42         }
43         else{
44             tmp=Fwk::query(q[i].r)-Fwk::query(q[i].l-1);
45             if(tmp<q[i].k) q[i].k-=tmp, R[++cntR]=q[i];
46             else L[++cntL]=q[i];
47             //printf("2:%d %d %d %d %d\n",q[i].x,q[i].l,q[i].r,q[i].k,tmp);
48         }
49     }
50     rep(i,1,cntL){
51         q[ql-1+i]=L[i];
52         if(!L[i].x) Fwk::add(L[i].l,-1);
53     }
54     rep(i,1,cntR) q[ql-1+cntL+i]=R[i];
55     solve(ql,ql-1+cntL,l,mid), solve(ql-1+cntL+1,qr,mid+1,r);
56 }
57 int main(){
58     //freopen("luogu3834.in","r",stdin);
59     //freopen("luogu3834.out","w",stdout); 
60     n=read(),m=read();
61     rep(i,1,n) b[i]=read(), q[i]=(query){0,i,i,b[i]};
62     sort(b+1,b+n+1);
63     mp[b[1]]=++cnt, lsh[cnt]=b[1];
64     rep(i,2,n) if(b[i]!=b[i-1]) mp[b[i]]=++cnt, lsh[cnt]=b[i];
65     int l,r,k;
66     rep(i,1,m) l=read(),r=read(),k=read(), q[n+i]=(query){i,l,r,k};
67     rep(i,1,n) q[i].k=mp[q[i].k];
68     solve(1,n+m,1,cnt);
69     rep(i,1,m) printf("%d\n",lsh[ans[i]]);
70     return 0;
71 }
View Code

 

Dynamic Rankings(zoj2112)

詢問動態區間第 $k$ 小。

題解

我們發現,既然可以把所有操作都混到一塊,為什么不能在詢問中間插入一些修改操作呢?

修改操作可以看成減一個數,再加一個數。因此把每個修改操作拆成減去原來的數、加上新的數即可。

注意,對於減去的數,要按它的絕對值進行整體二分的遞歸判斷。因為之前肯定加過這個數,之前那個數對 $mid$ 有影響(給線段樹對應數位加 $1$ 什么的),這個數就對 $mid$ 有影響(比如給線段樹對應數位 $-1$,去掉之前那個數的影響),也就是說這個數實際上就是消去之前加上的那個數的影響,之前那個數在哪,這個數就得在哪。(可以用反證法證明這個性質,想想如果這對加減操作不在同一個操作區間,減法后面的操作會發生什么)

后記

其實大部分整體二分的題目都完全可以用純數據結構(比如主席樹)代替,寫整體二分只是為了減小常數。

了解一下,會寫就好了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM