整體二分概述
一、適用問題
整體二分,即對所有的查詢進行一個整體的二分答案,需要數據結構題滿足以下性質。
- 詢問的答案具有可二分性
- 修改對判定答案的貢獻相對獨立,修改之間互不影響效果
- 修改如果對判定答案有貢獻,則貢獻爲一確定的與判定標準無關的值
- 貢獻滿足交換律、結合律,具有可加性
- 題目允許離線操作
(來自《淺談數據結構題的幾個非經典解法》)
上面的性質看上去複雜,其實只要滿足詢問答案具有可二分性,且題目允許離線操作,就可以考慮一下是否可以利用整體二分算法進行求解。
二、算法介紹
上面的文字可能有些過於理論,我們現在用淺顯一點的方式來理解這個算法。
假設你現在有 次查詢,查詢區間第 大的值。首先考慮如果只有 個查詢,是否可以直接二分解決。
顯然是可以的,我們只需要定位到具體區間,數一下小於等於當前二分值的數個數是否大於等於 即可。於是問題就變成了如何從單次二分演變到整體二分。
我們首先維護一個操作序列,即每個點的賦值和查詢,共 個操作。然後實現一個 函數,表示當前的操作序列在 範圍內,而該操作序列中所有的查詢操作的答案都在 中。
於是我們二分一個值 ,然後將 中所有的賦值操作中數值小於等於 的數加入到對應位置,比如 ,則 ,對於所有的查詢操作,判斷其查詢區間 的值是否大於等於 ,如果是則將其遞歸到 中,否則遞歸到 中,具體內容看一下下面的例題就可以理解。
最後分析一下時間複雜度,最多分了 層,每一層的時間複雜度爲 ,因此總時間複雜度爲 。
最後附上《淺談數據結構題的幾個非經典解法》中對該算法的理論概述。
詢問的答案可二分且修改對判定標準的貢獻相對獨立,且貢獻的值與判定標準無關。因此如果我們已經計算過某一些修改對詢問的貢獻,那麼這個貢獻永遠不會改變,我們沒有必要當判定標準改變時再次計算這部分修改的貢獻,只要記錄下當前的總貢獻,再進一步二分時,直接加上新的貢獻即可。
整體二分系列習題
1. K-th Number
題意:
無修改的區間第 大數問題。
思路:
主席樹模板題,但此處我們要用整體二分的方法來解決此題。
首先我們將所有賦值操作和查詢操作都放到一個數組中,形成了此題的操作序列。然後就是代碼中的核心關鍵點 函數,該函數表示區間 中的操作序列中的查詢操作的答案一定在 範圍內。
因此問題就變成了如何將 中的序列進行分組,再遞歸到 和 中。我們只需遍歷 中的所有操作,如果是賦值操作,則判斷數值 是否大於 ,如果小於等於 ,則將該操作丟到 數組中,並在樹狀數組的 位置加 ;否則將操作丟到 數組中。
如果是查詢操作,則在樹狀數組中查詢區間 的值 ,如果 ,則將該操作丟到 中;否則將 ,然後丟到 中。
可能說起來比較複雜,但是代碼比較清晰,推薦直接對代碼進行理解。
代碼:
#include <iostream>
#include <algorithm>
#include <cstdio>
#define mem(a,b) memset(a,b,sizeof a);
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
typedef long long ll;
typedef double db;
const int N = 1e5+100;
const int inf = 1e9+10;
const db EPS = 1e-9;
using namespace std;
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
int n,m,ans[N],c[N];
struct Node{int x,y,k,id;}q[2*N],q1[2*N],q2[2*N];
inline int lowbit(int x) {return x&(~x+1);}
inline void update(int x,int v) {for(; x<=n; x+=lowbit(x)) c[x] += v;}
inline int ask(int x){
int res = 0;
while(x) res += c[x], x -= lowbit(x);
return res;
}
void solve(int l,int r,int L,int R){
if(l > r || L > R) return;
if(l == r){
rep(i,L,R) if(q[i].id) ans[q[i].id] = l;
return;
}
int cnt1 = 0, cnt2 = 0, mid = (l+r)>>1;
rep(i,L,R){
if(q[i].id){ //查詢
int tmp = ask(q[i].y)-ask(q[i].x-1);
if(q[i].k <= tmp) q1[++cnt1] = q[i];
else q[i].k -= tmp, q2[++cnt2] = q[i];
}
else{ //賦值
if(q[i].x <= mid) update(q[i].y,1), q1[++cnt1] = q[i];
else q2[++cnt2] = q[i];
}
}
rep(i,1,cnt1) if(!q1[i].id) update(q1[i].y,-1);
rep(i,1,cnt1) q[L+i-1] = q1[i];
rep(i,1,cnt2) q[L+cnt1+i-1] = q2[i];
solve(l,mid,L,L+cnt1-1); solve(mid+1,r,L+cnt1,R);
}
int main()
{
scanf("%d%d",&n,&m);
rep(i,1,n) {scanf("%d",&q[i].x); q[i].id = 0; q[i].y = i;}
rep(i,1,m) {scanf("%d%d%d",&q[i+n].x,&q[i+n].y,&q[i+n].k); q[i+n].id = i;}
solve(-inf,inf,1,n+m);
rep(i,1,m) printf("%d\n",ans[i]);
return 0;
}
2. Dynamic Rankings
題意:
帶修改的區間第 大數問題。
思路:
帶修改第 大問題,如果要用主席樹來解決的話,則需要再加上一層樹狀數組來維護修改信息,即用樹套樹解決該問題。
但是如果用整體二分來處理這個問題的話,難度則會瞬間驟降。其實此題與上題唯一的區別就是這題多了一個修改操作,而修改操作無非就是刪除原來的數,加上新的數。
因此對於 的修改操作,我們將其拆成兩部分,第一部分爲刪除 ,然後令 ,第二部分是加上 ,具體細節可以參考下面的代碼實現。
代碼:
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#define mem(a,b) memset(a,b,sizeof a);
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
typedef long long ll;
typedef double db;
const int N = 1e5+100;
const int inf = 1e9+10;
const db EPS = 1e-9;
using namespace std;
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
int n,m,ans[N],c[N],a[N];
struct Node{int x,y,k,id;}q[2*N],q1[2*N],q2[2*N];
inline int lowbit(int x) {return x&(~x+1);}
inline void update(int x,int v) {for(; x<=n; x+=lowbit(x)) c[x] += v;}
inline int ask(int x){
int res = 0;
while(x) res += c[x], x -= lowbit(x);
return res;
}
void solve(int l,int r,int L,int R){
if(l > r || L > R) return;
if(l == r){
rep(i,L,R) if(q[i].k) ans[q[i].id] = l;
return;
}
int cnt1 = 0, cnt2 = 0, mid = (l+r)>>1;
rep(i,L,R){
if(q[i].k){ //查詢
int tmp = ask(q[i].y)-ask(q[i].x-1);
if(q[i].k <= tmp) q1[++cnt1] = q[i];
else q[i].k -= tmp, q2[++cnt2] = q[i];
}
else{ //賦值
if(q[i].x <= mid) update(q[i].id,q[i].y), q1[++cnt1] = q[i];
else q2[++cnt2] = q[i];
}
}
rep(i,1,cnt1) if(!q1[i].k) update(q1[i].id,-q1[i].y);
rep(i,1,cnt1) q[L+i-1] = q1[i];
rep(i,1,cnt2) q[L+cnt1+i-1] = q2[i];
solve(l,mid,L,L+cnt1-1); solve(mid+1,r,L+cnt1,R);
}
int main()
{
int _; scanf("%d",&_);
while(_--){
scanf("%d%d",&n,&m);
int cnt = 0, tot = 0;
memset(c,0,sizeof c);
rep(i,1,n) {
scanf("%d",&a[i]);
q[++cnt] = {a[i],1,0,i};
}
rep(i,1,m){
char op[5]; scanf("%s",op);
int x,y,k;
if(op[0] == 'Q'){
scanf("%d%d%d",&x,&y,&k);
q[++cnt] = {x,y,k,++tot};
}
else{
scanf("%d%d",&x,&y);
q[++cnt] = {a[x],-1,0,x};
a[x] = y;
q[++cnt] = {a[x],1,0,x};
}
}
solve(-inf,inf,1,cnt);
rep(i,1,tot) printf("%d\n",ans[i]);
}
return 0;
}
3. K大數查詢
題意:
個位置, 個操作。操作有兩種, 表示在第 個位置到第 個位置,每個位置加入一個數 ; 表示詢問從第 個位置到第 個位置,第 大的數是多少。
思路:
其實和上面第二個問題沒有太大的差別,只不過上一個問題是單點修改,而這題變成了區間修改。因此我們用線段樹維護一下整體二分即可解決。
代碼:
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#define mem(a,b) memset(a,b,sizeof a);
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
typedef long long ll;
typedef double db;
const int N = 1e5+100;
const int inf = 1e9+10;
const db EPS = 1e-9;
using namespace std;
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
int n,m;
ll ans[N],sum[2*N],lazy[2*N];
struct Node{ll x,y,k,id;}q[2*N],q1[2*N],q2[2*N];
inline int get_id(int l,int r) {return (l+r)|(l!=r);}
inline void pushDown(int l,int r){
int mid = (l+r)>>1, now = get_id(l,r), ls = get_id(l,mid), rs = get_id(mid+1,r);
sum[ls] += lazy[now]*(ll)(mid-l+1); sum[rs] += lazy[now]*(ll)(r-mid);
lazy[ls] += lazy[now]; lazy[rs] += lazy[now];
lazy[now] = 0;
}
inline void update(int l,int r,int L,int R,int v){
int now = get_id(l,r);
if(L <= l && r <= R){
sum[now] += (ll)v*(ll)(r-l+1);
lazy[now] += v;
return;
}
if(lazy[now]) pushDown(l,r);
int mid = (l+r)>>1;
if(L <= mid) update(l,mid,L,R,v);
if(R > mid) update(mid+1,r,L,R,v);
sum[now] = sum[get_id(l,mid)]+sum[get_id(mid+1,r)];
}
inline ll query(int l,int r,int L,int R){
int now = get_id(l,r);
if(L <= l && r <= R) return sum[now];
if(lazy[now]) pushDown(l,r);
int mid = (l+r)>>1;
ll thp = 0;
if(L <= mid) thp += query(l,mid,L,R);
if(R > mid) thp += query(mid+1,r,L,R);
return thp;
}
void solve(int l,int r,int L,int R){
if(l > r || L > R) return;
if(l == r){
rep(i,L,R) if(q[i].id) ans[q[i].id] = l;
return;
}
int cnt1 = 0, cnt2 = 0, mid = (l+r)>>1;
rep(i,L,R){
if(q[i].id){ //查詢
ll tmp = query(1,n,q[i].x,q[i].y);
if(q[i].k <= tmp) q2[++cnt2] = q[i];
else q[i].k -= tmp, q1[++cnt1] = q[i];
}
else{ //賦值
//由於右區間的起始點爲mid+1, 因此此處爲 >= mid+1
if(q[i].k >= mid+1) update(1,n,q[i].x,q[i].y,1), q2[++cnt2] = q[i];
else q1[++cnt1] = q[i];
}
}
rep(i,1,cnt2) if(!q2[i].id) update(1,n,q2[i].x,q2[i].y,-1);
rep(i,1,cnt1) q[L+i-1] = q1[i];
rep(i,1,cnt2) q[L+cnt1+i-1] = q2[i];
solve(l,mid,L,L+cnt1-1); solve(mid+1,r,L+cnt1,R);
}
int main()
{
scanf("%d%d",&n,&m);
int tot = 0;
rep(i,1,m){
ll op,x,y,k;
scanf("%lld%lld%lld%lld",&op,&x,&y,&k);
if(op == 1) q[i] = {x,y,k,0};
else q[i] = {x,y,k,++tot};
}
solve(-inf,inf,1,m);
rep(i,1,tot) printf("%lld\n",ans[i]);
return 0;
}
4. Stamp Rally
題意:
一個 個點, 條邊的圖,第 條邊連接 和 ,保證圖是連通的。
現在有 次詢問,每次詢問給出一個三元組 ,表示詢問從 兩個點出發,一共擴展 個不同的點(包括起始點),求所經過的邊中最大編號的最小值。
思路:
此題較之上面三題,沒有那麼套路,因此我們先從只有一個詢問開始找思路。
首先考慮能不能把圖變成樹,因爲圖上問題往往都很複雜,而變成樹上問題後我們的可操作空間會大很多。繼續思考不難發現,如果我們按邊的編號爲權值構建一棵最小生成樹,每次詢問的答案也一定會落在最小生成樹上的邊上。
轉到樹上問題之後,我們考慮能不能二分答案然後 ,如果只有一個詢問的話,顯然是可以的。只需要維護一個可加邊可刪邊的按秩合併的並查集即可。
既然單個查詢可以二分,那一定可以用整體二分的方法對所有查詢進行二分。我們在二分值爲 時,將所有編號小於等於 的邊連接起來,然後再遞歸到 區間。右區間遞歸結束後,再撤銷二分值爲 時連接的邊,然後遞歸 。
如果連接操作是 ,那麼撤銷操作的時候,如果只有 是不夠的,需要從 開始不斷向上訪問,對於訪問到的每一個節點都減去 ,如此才能保證撤銷操作的正確性。
代碼:
#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a);
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
typedef long long ll;
typedef double db;
const db EPS = 1e-9;
const int N = 1e6+100;
using namespace std;
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
int n,m,Q,fa[N],sz[N],ans[N];
struct Node {int x,y,k,id,h1,h2;} q[N],q1[N],q2[N];
int find(int x) {return x == fa[x] ? x : find(fa[x]);}
int calc(int x,int y){
int fx = find(x), fy = find(y);
return fx == fy ? sz[fx] : (sz[fx] + sz[fy]);
}
pair<int,int> merge(int x,int y){
int fx = find(x), fy = find(y);
if(fx == fy) return make_pair(-1,-1);
if(sz[fx] < sz[fy]){
fa[fx] = fy, sz[fy] += sz[fx];
return make_pair(fx,fy);
}
else{
fa[fy] = fx, sz[fx] += sz[fy];
return make_pair(fy,fx);
}
}
void Delete(int x,int y) {
fa[x] = x;
while(y){
sz[y] -= sz[x];
if(y == fa[y]) break;
y = fa[y];
}
}
void solve(int l,int r,int L,int R){
if(l > r || L > R) return;
if(l == r){
rep(i,L,R) if(q[i].k) ans[q[i].id] = l;
return;
}
int mid = (l+r)>>1, cnt1 = 0, cnt2 = 0;
// logs(mid,L,R);
rep(i,L,R){
if(q[i].k){ //查詢
int tmp = calc(q[i].x,q[i].y);
if(q[i].k <= tmp) q1[++cnt1] = q[i];
else q2[++cnt2] = q[i];
}
else{
if(q[i].id <= mid){
pair<int,int> tmp = merge(q[i].x,q[i].y);
q1[++cnt1] = q[i];
q1[cnt1].h1 = tmp.first; q1[cnt1].h2 = tmp.second;
}
else q2[++cnt2] = q[i];
}
}
rep(i,1,cnt1) q[L+i-1] = q1[i];
rep(i,1,cnt2) q[L+cnt1+i-1] = q2[i];
solve(mid+1,r,L+cnt1,R);
rep(i,1,cnt1) if(!q[L+i-1].k) Delete(q[L+i-1].h1,q[L+i-1].h2);
solve(l,mid,L,L+cnt1-1);
}
int main()
{
int cnt = 0;
scanf("%d%d",&n,&m);
rep(i,1,n) fa[i] = i, sz[i] = 1;
rep(i,1,m){
int x,y; scanf("%d%d",&x,&y);
if(merge(x,y).first == -1) continue;
q[++cnt] = {x,y,0,i,0,0};
// logs(i);
}
scanf("%d",&Q);
rep(i,1,Q){
int x,y,z; scanf("%d%d%d",&x,&y,&z);
q[++cnt] = {x,y,z,i,0,0};
}
rep(i,1,n) fa[i] = i, sz[i] = 1;
solve(0,m+1,1,cnt);
rep(i,1,Q) printf("%d\n",ans[i]);
return 0;
}
後記
整體二分的內容到這裏就結束了,總的來說,該算法應該屬於一種解題套路,所需要的學習時間不長,可以當作解決數據結構問題的一種套路。最後祝大家 愉快,一起愛上二分把!(๑•̀ㅂ•́)و✧
ACM 的旅行雖然充滿荊棘但一擡頭便能看見無數束光,請務必堅持下去,負重前行終有云開霧散之日!💪💪💪