整體二分
整體二分是一種離線算法,主要用於解決題目中存在多次詢問,每次詢問都要二分,並且詢問可離線的問題,之前看了網上許多博客感覺大多都很難理解,我們先給出例題,通過題目能更好地理解
例題
題目傳送門:Luogu P3332 K大數查詢
題目大意
給定 \(n\) 個初始為空的可重集合與 \(m\) 個操作,對於每次插入操作你需要在編號為 \(l\) 到 \(r\) 的集合中插入元素 \(c\),對於每次詢問操作你需要給出在編號為 \(l\) 到 \(r\) 的集合中排名為 \(c\) 的元素
題解
首先考慮每一個單獨的詢問操作如何進行二分答案,對於每一個詢問我們都需要在值域上二分 \(mid\),每次check時統計詢問的集合中比 \(mid\) 大的數的個數,但是這樣顯然時間復雜度過高,怎么優化呢?
如果先不管插入操作的話其實我們可以把所有的詢問操作一起二分,每次找到一個 \(mid\),利用線段樹統計大於 \(mid\) 的數字的個數,如果個數大於詢問的排名,也就是說 \(mid\) 太小了,那么把這些詢問放在一起,另外把個數小於詢問的排名的放在一起,對於這兩堆詢問,依次分治下去,把值域根據 \(mid\) 應當更大或者更小改為 \([mid+1,r]\) 或者 \([l,mid]\) 即可,遞歸到 \(mid\) 的值唯一確定時這些詢問的答案就是 \(mid\),時間復雜度 \(\Theta(n\log^2n)\)
但是現在有了插入操作,怎么辦呢?其實我們發現,插入操作對於二分到的 \(mid\) 來說也是分為有貢獻和無貢獻的,如果插入的數字小於 \(mid\),那么它對於 \(mid\) 的排名就沒有任何影響,如果插入的數字大於 \(mid\),那么它對於 \(mid\) 的排名就已經產生了影響,根據這個我們可以把插入操作也分為兩部分
然后我們需要一起考慮插入和查詢操作,現在我們已經通過上面的方法根據二分到的 \(mid\) 把詢問和插入操作分別分為了兩個部分,對於沒有貢獻的插入操作,我們把它們和 “查詢的排名大於比 \(mid\) 大的數的個數” 的詢問操作放在一起,因為這些詢問操作需要更多的插入操作才能有結果,而已經有貢獻的插入操作我們把它們與 “查詢的排名小於比 \(mid\) 大的數的個數” 的詢問操作放在一起,因為這些詢問已經有了足夠的插入操作了,我們要減少插入操作的數量才能找到答案 遍歷操作后根據 \(mid\) 分堆,如下圖
現在我們就可以在 \(\Theta(n\log^2n)\) 的時間復雜度下解決這個問題了
再捋一遍思路,我們把所有操作一起二分,每次遞歸我們需要給定二分的值域和操作的序列(也就是上文中說到的每一堆操作),如果值域只剩下一個數,那么操作序列中所有詢問操作的答案就是這個數,記錄之后返回即可,否則我們就遍歷當前操作序列中的每一個操作,根據 \(mid\) 值來確定這個操作應該被放在哪一堆,對於修改操作如果已經有貢獻了就把它的貢獻加入線段樹中進行統計,最后別忘了把修改的線段樹恢復至初始狀態,然后分別把值域減半遞歸計算兩堆操作即可
Code
#include<bits/stdc++.h>
#define in read()
#define MAXN 100005
using namespace std;
typedef long long ll;
//IO優化
inline int read()
{
char c=getchar();
int x=0,f=1;
while(c<48){if(c=='-')f=-1;c=getchar();}
while(c>47)x=(x*10)+(c^48),c=getchar();
return x*f;
}
inline void mwrite(int a)
{
if(a>9)mwrite(a/10);
putchar((a%10)|48);
}
inline void write(int a,char c)
{
mwrite(a<0?(putchar('-'),a=-a):a);
putchar(c);
}
//Segment Tree-------------------------------------
struct Node
{
int l,r;
ll lazy,sum;
}node[MAXN<<2];
inline void pushup(int pos){node[pos].sum=node[pos<<1].sum+node[pos<<1|1].sum;}//維護節點信息
inline void down(int pos)//下傳懶標記
{
if(!node[pos].lazy)return;
node[pos<<1].lazy+=node[pos].lazy,node[pos<<1|1].lazy+=node[pos].lazy;
node[pos<<1].sum+=node[pos].lazy*(node[pos<<1].r-node[pos<<1].l+1);
node[pos<<1|1].sum+=node[pos].lazy*(node[pos<<1|1].r-node[pos<<1|1].l+1);
node[pos].lazy=0;
}
inline void build(int l,int r,int pos)//建樹
{
node[pos]=(Node){l,r,0,0};
if(l==r) return;
int mid=(l+r)>>1;
build(l,mid,pos<<1);
build(mid+1,r,pos<<1|1);
}
inline void modify(int l,int r,ll v,int pos)//區間修改
{
if(l<=node[pos].l&&node[pos].r<=r)
return node[pos].lazy+=v,node[pos].sum+=v*(node[pos].r-node[pos].l+1),void(0);
down(pos);
int mid=(node[pos].l+node[pos].r)>>1;
if(l<=mid) modify(l,r,v,pos<<1);
if(r>mid) modify(l,r,v,pos<<1|1);
pushup(pos);
}
inline ll query(int l,int r,int pos)//查詢區間和
{
if(l<=node[pos].l&&node[pos].r<=r) return node[pos].sum;
down(pos);
int mid=(node[pos].l+node[pos].r)>>1;
ll ans=0;
if(l<=mid) ans+=query(l,r,pos<<1);
if(r>mid) ans+=query(l,r,pos<<1|1);
return ans;
}
//Segment Tree-------------------------------------
struct Query
{
int opt,l,r,id;
ll v;
}q[MAXN],q1[MAXN],q2[MAXN];
int n,m,qnum,Ans[MAXN];
void solve(ll l,ll r,int ql,int qr)//值域,操作區間
{
if(ql>qr||l>r)return;
if(l==r)//答案唯一,直接賦值
{
for(int i=ql;i<=qr;++i)
if(q[i].opt==2) Ans[q[i].id]=l;
return;
}
ll mid=(l+r)>>1,tmpcnt;
int lcnt=0,rcnt=0;
bool queryr=0,queryl=0;
for(int i=ql;i<=qr;++i)//遍歷詢問
{
if(q[i].opt==1)//修改操作
{
if(q[i].v>mid)//比二分到的答案大,會有貢獻,需要在線段樹上修改
{
modify(q[i].l,q[i].r,1,1);//線段樹上詢問區間都加上1,表明有一個更大的數
q2[++rcnt]=q[i];//放進右側
}
else q1[++lcnt]=q[i];//小於二分到的答案,暫時沒有貢獻,放入左側
}
else//詢問操作
{
tmpcnt=query(q[i].l,q[i].r,1);//查詢詢問區間中比mid大的數的個數
if(tmpcnt<q[i].v)//當前排名比詢問排名小,要等加入更多修改后再處理
{
q[i].v-=tmpcnt;//下次詢問的時候就可以忽略已有的比它大的數,只需要找到不夠的比他大的數
q1[++lcnt]=q[i];//需要更多的當前沒有貢獻的修改操作,放入左側
queryl=1;
}
else q2[++rcnt]=q[i],queryr=1;//當前有的比它大的數字已經多於詢問的排名了,放入右側
}
}
for(int i=1;i<=rcnt;++i)
if(q2[i].opt==1) modify(q2[i].l,q2[i].r,-1,1);//還原線段樹的修改操作,方便繼續遞歸下次使用
for(int i=ql;i<ql+lcnt;++i) q[i]=q1[i-ql+1];//把詢問分成兩堆放回原數組中
for(int i=ql+lcnt;i<=qr;++i) q[i]=q2[i-ql-lcnt+1];
if(queryl)solve(l,mid,ql,ql+lcnt-1);//右邊的修改操作貢獻未統計,詢問操作還需要更多的修改
if(queryr)solve(mid+1,r,ql+lcnt,qr);//左邊的修改操作貢獻已統計,詢問操作不需要更多的修改
}
signed main()
{
n=in,m=in;
build(1,n,1);//建立維護區間和線段樹
for(int i=1,opt,l,r,c;i<=m;++i)
{
opt=in,l=in,r=in,c=in;
q[i]=(Query){opt,l,r,(opt==2)?++qnum:0,c};//讀入操作
}
solve(-n,n,1,m);//前兩個參數為值域下界上界,后兩個參數為操作序列下標起點和終點
for(int i=1;i<=qnum;++i) write(Ans[i],'\n');//打印答案
return 0;
}
小結
相信看到這里你應該理解了整體二分的大致思想了,整體二分就是通過同時進行多個操作的二分,在優秀的時間復雜度下解決問題,看到可離線操作,可二分找答案,貢獻可疊加,修改之間相互不影響就能嘗試使用二分了
可以嘗試一些整體二分練習題:
Luogu P2617 [ZJOI2013]Dynamic Rankings 帶插入刪除區間第 \(k\) 小
該文為本人原創,轉載請注明出處