C# 在程式碼裡呼叫其他Webapi
好久沒寫過東西了...懶狗一條。
參考了:OI Wiki - 整體二分
為了理解與實現上的方便,本文中的實現均採用vector,空間複雜度為$O(nlogn)$;而IO選手更應該採用另一種實現方法(雙指標+重排序),空間複雜度為$O(n)$。不過本質上思想是一樣的,只是實現上的偷懶與否。
整體二分不同於資料結構:資料結構往往是需要線上修改、線上查詢(可能需要離線預處理,如離散化);而整體二分則是充分利用離線的優勢,將所有的詢問同時進行處理。
而其適用的範圍是,每個查詢的答案均具有單調性。這樣說起來有點抽象,不妨直接認為是“查詢區間內從小到大的第$k$個數”:如果我們二分出的中點$mid$大於答案$ans$,那麼小於等於$mid$的數的數量$num$肯定大於等於$k$;如果$mid\leq ans$,那麼$num\leq k$。
這說的恰恰是主席樹和動態主席樹的經典模板。
1. 不帶修改,查詢區間從小到大第$k$個數(POJ 2104,K-th Number)
先將陣列$a$離散化得到陣列$b$後,考慮這樣處理所有的查詢:
一開始,所有的詢問$\{l,r,k,id\}$都被扔到了一個vector中。此時答案的範圍是$1\thicksim m$($m$是離散化後不同$b_i$的數量)。
我們二分出一箇中點$mid=(1+m)/2$,然後考慮將這些詢問分到兩個vector $vl,vr$中,其中$vl$中詢問的答案小於等於$mid$、$vr$中詢問的答案大於$mid$。如何檢驗一個詢問的答案是否小於等於$mid$呢?我們可以利用樹狀陣列,先把所有$1\leq b_i\leq mid$的$i$全部$+1$。那麼對於某個具體的詢問$\{l,r,k,id\}$,其答案小於等於
這樣一來,對於所有的詢問,我們可以分別各進行一次BIT query將其放入$vl$或$vr$中。需要注意的是,對於所有放入$vr$的詢問,我們需要將它們的$k$各減去分別的$cnt$。這是因為在之後的處理中,我們僅需要計算$mid+1\thicksim m$的數有多少個出現在其包含的區間中。
以上是第一次對半劃分的情況。對於一般的情況,答案區間為$[l,r]$,$mid=(l+r)/2$,我們將$l\leq b_i\leq mid$的$i$全部$+1$。其餘部分均是一樣的。
需要記得在每次對半劃分後需要將樹狀陣列清空。這裡的清空不能用memset,而是需要進行新增的逆操作、在原來$+1$的位置$-1$。
分析一下複雜度。由於我們是對答案區間進行劃分,所以劃分的深度為$logn$;而由於相同深度的vector的並就是查詢的全集,所以每個查詢都會被劃分$logn$次,而每次劃分的複雜度是$(n+q)logn$(BIT query),所以整體的複雜度為$O((n+q)(logn)^2)$。
關鍵部分的程式碼:
struct Query { int l,r,k,id; }; //詢問的定義 vector<int> vpos[N]; //vpos[i]包含了,所有離散化後為i的數 在a中的下標 void solve(int l,int r,vector<Query> vq) { if(l==r) //l=r則vq中的答案確定 { for(int i=0;i<vq.size();i++) ans[vq[i].id]=l; return; } int mid=(l+r)>>1; //二分中點 for(int i=l;i<=mid;i++) //將所有l<=b_i<=r的i全部+1 for(int j=0;j<vpos[i].size();j++) bit.add(vpos[i][j],1); vector<Query> vl,vr; for(int i=0;i<vq.size();i++) { Query q=vq[i]; int cnt=bit.query(q.r)-bit.query(q.l-1); //cnt為[l,r]區間中l~mid出現的次數 if(cnt>=q.k) vl.push_back(q); else q.k-=cnt,vr.push_back(Query(q)); } for(int i=l;i<=mid;i++) //清空樹狀陣列 for(int j=0;j<vpos[i].size();j++) bit.add(vpos[i][j],-1); solve(l,mid,vl); solve(mid+1,r,vr); }
完整程式碼:
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; struct Query { int l,r,k,id; Query(int a=0,int b=0,int c=0,int d=0) { l=a,r=b,k=c,id=d; } }; const int N=100005; int n,m; int a[N]; int rev[N]; vector<int> v; int ans[N]; vector<int> vpos[N]; struct BIT { int t[N]; int lowbit(int x) { return x&(-x); } inline void add(int k,int x) { for(int i=k;i<=n;i+=lowbit(i)) t[i]+=x; } inline int query(int k) { int ans=0; for(int i=k;i;i-=lowbit(i)) ans+=t[i]; return ans; } }bit; void solve(int l,int r,vector<Query> vq) { if(l==r) { for(int i=0;i<vq.size();i++) ans[vq[i].id]=l; return; } int mid=(l+r)>>1; for(int i=l;i<=mid;i++) for(int j=0;j<vpos[i].size();j++) bit.add(vpos[i][j],1); vector<Query> vl,vr; for(int i=0;i<vq.size();i++) { Query q=vq[i]; int cnt=bit.query(q.r)-bit.query(q.l-1); if(cnt>=q.k) vl.push_back(q); else q.k-=cnt,vr.push_back(Query(q)); } for(int i=l;i<=mid;i++) for(int j=0;j<vpos[i].size();j++) bit.add(vpos[i][j],-1); solve(l,mid,vl); solve(mid+1,r,vr); } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]),v.push_back(a[i]); sort(v.begin(),v.end()); v.resize(unique(v.begin(),v.end())-v.begin()); for(int i=1;i<=n;i++) { int pos=lower_bound(v.begin(),v.end(),a[i])-v.begin()+1; vpos[pos].push_back(i); rev[pos]=a[i]; } vector<Query> vq; for(int i=1;i<=m;i++) { int l,r,k; scanf("%d%d%d",&l,&r,&k); vq.push_back(Query(l,r,k,i)); } solve(1,v.size(),vq); for(int i=1;i<=m;i++) printf("%d\n",rev[ans[i]]); return 0; }View Code
2. 帶修改,查詢區間從小到大第$k$個數(ZOJ 2212,Dynamic Rankings)
這道題看起來就比上一題複雜了許多——畢竟用資料結構實現從主席樹進化到了樹套樹。
不過,用整體二分仍然可以比較輕鬆地解決;雖然離實現還需要進行不少的分析。
首先考慮能否像上一題一樣,將查詢與操作分離,從而使得vector中只存放查詢?答案是否定的。這是因為帶修改引入了“時間”這個因素,而我們在進行$vq$到$vl,vr$的劃分時會將時間打亂,因此在每次BIT query時 無法得知是否存在操作對於當前詢問已經“過期”,故無法計算。
那麼可以嘗試將查詢與操作一同放入vector中同時處理。其中,給陣列$a$賦初值視為一次插入操作,進行一次單點修改視為一次刪除操作後接一次插入操作。
看起來仍然無法處理“時間”因素?其實還有一些潛在的性質可以利用。由於在一開始,所有的指令均是按照時間順序插入vector的,而且在每一層的處理都是按照vector的順序進行,所以劃分出的$vl,vr$中的指令分別按時間單調增(只不過時間不再連續)。
我們按照vector的順序依次處理$vq$中的指令。當答案區間為$[l,r]$時,二分中點為$mid=(l+r)/2$。對於所有的詢問仍然和之前一樣,在樹狀陣列上查詢$[l,r]$的區間和$cnt$,並將$cnt$與$k$比較以放入$vl$或$vr$;而對於所有操作,僅在被修改值(不論是插入還是刪除)小於等於$mid$時才在樹狀陣列上修改、並放入$vl$,否則放入$vr$。
考慮一下上述做法的正確性。對於所有$l\thicksim mid$的數的貢獻,假如其未被刪除那麼就和不帶修改的情況一樣,會在賦初值的那一段指令中在樹狀陣列上$+1$;假如其會被修改,那麼在被修改之前能夠正確產生貢獻,而在被修改時則在樹狀陣列上$-1$,從而對之後的詢問不產生貢獻。這樣一起處理 與 將查詢和操作分離的區別,就在於能夠保證按照時間處理。(如果將兩者分離的話,雖然理論上也可行,但需要根據當前詢問的時間 維護不同值的操作的指標,實現起來相當繁瑣)
關鍵部分的程式碼:
//在表示操作時,x=下標,y=離散化值,k=插入1/刪除-1,op=0,id=0 //在表示查詢時,x=l,y=r,k=k,op=1,id=id struct Opt { int x,y,k,op,id; Opt(int a=0,int b=0,int c=0,int d=0,int e=0) { x=a,y=b,k=c,op=d,id=e; } }; void solve(int l,int r,vector<Opt> opt) { if(l==r) //當l=r時,opt中的所有查詢操作結果均為l { for(Opt cur: opt) if(cur.op==1) ans[cur.id]=l; return; } int mid=(l+r)>>1; vector<Opt> vl,vr; for(Opt cur: opt) if(cur.op==1) //op=1,是查詢指令 { int cnt=bit.query(cur.y)-bit.query(cur.x-1); if(cnt>=cur.k) vl.emplace_back(cur); else cur.k-=cnt,vr.emplace_back(cur); } else //op=0,是操作指令 if(cur.y<=mid) //當操作值小於等於mid,在樹狀陣列上進行修改、並放入vl bit.add(cur.x,cur.k),vl.emplace_back(cur); else //否則直接放入vr vr.emplace_back(cur); for(Opt cur: opt) //樹狀陣列清空 if(cur.op==0 && cur.y<=mid) bit.add(cur.x,-cur.k); solve(l,mid,vl); solve(mid+1,r,vr); }
完整程式碼:
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; struct Opt { int x,y,k,op,id; Opt(int a=0,int b=0,int c=0,int d=0,int e=0) { x=a,y=b,k=c,op=d,id=e; } }; const int N=200005; int n,m; int qcnt,ans[N]; struct BIT { int t[N]; int lowbit(int x) { return x&(-x); } inline void add(int k,int x) { for(int i=k;i<=n;i+=lowbit(i)) t[i]+=x; } inline int query(int k) { int ans=0; for(int i=k;i;i-=lowbit(i)) ans+=t[i]; return ans; } }bit; void solve(int l,int r,vector<Opt> opt) { if(l==r) { for(Opt cur: opt) if(cur.op==1) ans[cur.id]=l; return; } int mid=(l+r)>>1; vector<Opt> vl,vr; for(Opt cur: opt) if(cur.op==1) { int cnt=bit.query(cur.y)-bit.query(cur.x-1); if(cnt>=cur.k) vl.emplace_back(cur); else cur.k-=cnt,vr.emplace_back(cur); } else if(cur.y<=mid) bit.add(cur.x,cur.k),vl.emplace_back(cur); else vr.emplace_back(cur); for(Opt cur: opt) if(cur.op==0 && cur.y<=mid) bit.add(cur.x,-cur.k); solve(l,mid,vl); solve(mid+1,r,vr); } int a[N]; int op[N],x[N],y[N],k[N]; vector<int> v; inline int getpos(int x) { return lower_bound(v.begin(),v.end(),x)-v.begin()+1; } int main() { int T; scanf("%d",&T); while(T--) { qcnt=0; v.clear(); scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]),v.emplace_back(a[i]); for(int i=1;i<=m;i++) { char buf[5]; scanf("%s",buf); scanf("%d%d",&x[i],&y[i]); if(buf[0]=='Q') op[i]=1,scanf("%d",&k[i]); else op[i]=0,v.emplace_back(y[i]); } sort(v.begin(),v.end()); v.resize(unique(v.begin(),v.end())-v.begin()); vector<Opt> opt; for(int i=1;i<=n;i++) opt.emplace_back(Opt(i,getpos(a[i]),1,0,0)); for(int i=1;i<=m;i++) if(op[i]==1) opt.emplace_back(Opt(x[i],y[i],k[i],1,++qcnt)); else { opt.emplace_back(Opt(x[i],getpos(a[x[i]]),-1,0,0)); a[x[i]]=y[i]; opt.emplace_back(Opt(x[i],getpos(y[i]),1,0,0)); } solve(1,v.size(),opt); for(int i=1;i<=qcnt;i++) printf("%d\n",v[ans[i]-1]); } return 0; }View Code
暫時感覺帶修改查詢的整體二分已經比較牛了,如果遇到標誌性的玩法再繼續補充。
整點例題。
洛谷P3242 (接水果,HNOI2015)
直接看題目還是有點麻煩的,需要進行轉化。轉化有兩種思路:
一種是將水果的路徑拎起來,在上面找子路徑。也就是說,盤子的端點要分別在$lca(x,y)$到$x,y$的路徑上、或者均在同一條路徑上。由於任意點到根的一段路徑無法簡單提出,所以需要樹剖後獲得一段連續的dfs序。然後只能依次列舉盤子兩端所在的dfs序段,從而提取出所有的盤子路徑,然後線段樹上二分,抽象上來說是計算矩形內的點數。時間複雜度爆炸(感覺是$O(n(logn)^3)$),細節也很多。
另一種是將盤子的路徑拎起來,看能夠成為多少水果路徑的子路徑。考慮固定盤子端點$u,v$時,水果路徑的端點可能出現在哪裡。記dfs整棵樹時,進入$x$點的時間戳為$in[x]$、離開的時間戳為$out[x]$。那麼一共只有三種情況:
1. 盤子的路徑不是一條鏈,那麼水果的端點必然分別存在於兩個盤子端點的子樹中
2. 盤子的路徑是一條鏈,那麼其中一個水果路徑端點必然在較深盤子端點的子樹中,另一個水果路徑端點有兩種情況
可以看出,該盤子能夠接住的水果的兩端點dfs序應在所確定的矩形區域內。我們不妨規定端點為$x,y$的水果的座標為$(in[x],in[y])$(其中規定順序為$in[x]$小於$in[y]$),那麼一個盤子能夠覆蓋的區域為x座標為較小dfs序範圍、y座標為較大dfs序範圍 的矩形。
於是,對於每個水果(詢問),就是要求能夠覆蓋該點的矩形中,值從小到大的第$k$個。與序列的區間相比,“覆蓋”是稍微複雜一點的,不過可以考慮使用掃描線來解決。由於x/y座標都是dfs序,所以範圍都在$[1,n]$之內,那麼我們考慮將每個矩形拆成上邊界和下邊界兩個線段,如下圖所示:
然後考慮按照y座標從小到大的順序開始掃描。一開始掃描到$y=y_1$,那麼區間$[x_1,x_3]$均被覆蓋了一遍,即是樹狀陣列在$x_1$處$+1$、在$x_3$處$-1$;然後掃描到$y=y_2$,將區間$[x_2,x_4]$覆蓋一遍;然後掃描到$y=y_3+1$(在$y=y_3$上的點仍然被覆蓋),將區間$[x_1,x_3]$的覆蓋刪去,即是樹狀陣列在$x_1$處$-1$、在$x_3$處$+1$;然後掃描到$y=y_4+1$,將區間$[x_1,x_3]$的覆蓋刪去。
我們在排序矩形的線段時,也將所有的水果的點一起按照y座標排序。當遇到一個水果時,BIT query水果的x座標就能得到其被覆蓋的次數。其餘部分就是帶修改整體二分的板子了。
整體時間複雜度$O(n(logn)^2)$。
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; struct penta { int l,r,y,op,k; penta(int a=0,int b=0,int c=0,int d=0,int e=0) { l=a,r=b,y=c,op=d,k=e; } }; const int N=100005; int n,p,q; vector<int> v[N]; struct BIT { int t[N]; int lowbit(int x) { return x&(-x); } void add(int k,int x) { for(int i=k;i<=n;i+=lowbit(i)) t[i]+=x; } int query(int k) { int ans=0; for(int i=k;i;i-=lowbit(i)) ans+=t[i]; return ans; } }bit; int ans[N]; vector<penta> opt[N]; inline bool cmp(const penta &X,const penta &Y) { if(X.y!=Y.y) return X.y<Y.y; return X.op<Y.op; } //for querys: x=l, y=y, id=r, op=2, k=k //for operations: l=l, r=r, y=y, op=1 void solve(int l,int r,vector<penta> vq) { if(l==r) { for(penta cur: vq) ans[cur.r]=l; return; } int mid=(l+r)>>1; vector<penta> vl,vr; vector<penta> ord; for(int i=l;i<=mid;i++) for(penta cur: opt[i]) ord.emplace_back(cur); for(penta cur: vq) ord.emplace_back(cur); sort(ord.begin(),ord.end(),cmp); for(penta cur: ord) if(cur.op==2) { int cnt=bit.query(cur.l); if(cnt>=cur.k) vl.emplace_back(cur); else cur.k-=cnt,vr.emplace_back(cur); } else { bit.add(cur.l,cur.op); bit.add(cur.r+1,-cur.op); } for(penta cur: ord) if(cur.op<2) { bit.add(cur.l,-cur.op); bit.add(cur.r+1,cur.op); } solve(l,mid,vl); solve(mid+1,r,vr); } int to[N][16]; int tot,dep[N],in[N],out[N]; inline void dfs(int x,int fa) { to[x][0]=fa; in[x]=++tot; dep[x]=dep[fa]+1; for(int y: v[x]) if(y!=fa) dfs(y,x); out[x]=tot; } inline int lca(int x,int y) { if(dep[x]<dep[y]) swap(x,y); for(int i=15;i>=0;i--) if(dep[to[x][i]]>=dep[y]) x=to[x][i]; if(x==y) return x; for(int i=15;i>=0;i--) if(dep[to[x][i]]!=dep[to[y][i]]) x=to[x][i],y=to[y][i]; return to[x][0]; } int a[N],b[N],c[N]; int main() { scanf("%d%d%d",&n,&p,&q); for(int i=1;i<n;i++) { int x,y; scanf("%d%d",&x,&y); v[x].emplace_back(y); v[y].emplace_back(x); } dfs(1,0); for(int i=1;i<16;i++) for(int j=1;j<=n;j++) to[j][i]=to[to[j][i-1]][i-1]; vector<int> vec; for(int i=1;i<=p;i++) { scanf("%d%d%d",&a[i],&b[i],&c[i]); vec.emplace_back(c[i]); } sort(vec.begin(),vec.end()); vec.resize(unique(vec.begin(),vec.end())-vec.begin()); for(int i=1;i<=p;i++) { int x=a[i],y=b[i],w=c[i]; int pos=lower_bound(vec.begin(),vec.end(),w)-vec.begin()+1; if(in[x]>in[y]) swap(x,y); if(lca(x,y)==x) { int z=y; for(int j=15;j>=0;j--) if(dep[to[z][j]]>dep[x]) z=to[z][j]; opt[pos].emplace_back(penta(1,in[z]-1,in[y],1)); opt[pos].emplace_back(penta(1,in[z]-1,out[y]+1,-1)); if(out[z]<n) { opt[pos].emplace_back(penta(in[y],out[y],out[z]+1,1)); opt[pos].emplace_back(penta(in[y],out[y],n+1,-1)); } } else { opt[pos].emplace_back(penta(in[x],out[x],in[y],1)); opt[pos].emplace_back(penta(in[x],out[x],out[y]+1,-1)); } } vector<penta> vq; for(int i=1;i<=q;i++) { int x,y,k; scanf("%d%d%d",&x,&y,&k); if(in[x]>in[y]) swap(x,y); vq.emplace_back(penta(in[x],i,in[y],2,k)); } solve(1,vec.size(),vq); for(int i=1;i<=q;i++) printf("%d\n",vec[ans[i]-1]); return 0; }View Code
(完)