owned this note
owned this note
Published
Linked with GitHub
# 整體二分
顧名思義 : 全部的二分搜一起做就叫做整體二分,可以用來求區間第$k$小。
區間第$k$小也可以用另一個方法做 : [持久化線段樹](/K8qTIlbaTuWijADAnTDtGw)
### 用二分搜求整個數列的第$k$小
假設質域最大到$C$,那就可以用$[1,C]$來二分搜,如果$k$比序列中小於等於$mid$的數量小,代表第$k$小的數字會小於或等於$mid$,就往左搜,否則往右搜。
```cpp=
int l=1,r=MAXC;
while(l<r){
int mid=(l+r)/2;
if(k<=pref[mid]) r=mid;
else l=mid+1;
}
cout << l << '\n'; //answer
```
pref[i]為有幾個數比i小,可以先預處理 :
:::spoiler pref
```cpp=
for(int i=0;i<n;i++){
cin >> a[i];
cnt[a[i]]++;
}
for(int i=1;i<=C;i++) pref[i]=pref[i-1]+cnt[i];
```
:::
<br>
### 如果有很多個query呢 ?
那就全部一起做,初始$l,r$都是$1,C$,每做一次每個查詢的範圍都會除以$2$,並維護每個查詢的左右界,直到$l=r$。
```cpp=
bool done=true;
do{
done=true;
for(auto &[l,r,k]:q){
if(l<r){
done=false;
int mid=(l+r)/2;
if(k<=pre[mid]) r=mid;
else l=mid+1;
}
}
}while(!done);
for(auto [l,r,k]:q) cout << l << '\n';
```
遞迴版本 : 把要往左子樹遞迴的綁成一堆,往右子樹遞迴的綁成一堆,分別遞迴。遞迴到底的時候代表這組$Query$的答案就是$l$或$r$。
```cpp=
void cur(int l,int r,vector<Q> q){
if(q.empty()) return;
if(l==r){
for(auto i:q) ans[i.pos]=l;
return;
}
int mid=(l+r)/2;
vector<Q> qL,qR;
for(auto i:q){
if(i.k<=pref[mid]) qL.pb(i);
else qR.pb(i);
}
cur(l,mid,qL);
cur(mid+1,r,qR);
}
```
### 那如果每個$Query$都有範圍限制呢 ?
在查詢的時候就不是用$pref$了,因為預處理的時間複雜度及空間複雜度沒有很好,所以就直接在當下計算區間中有幾個數比$mid$還小,因為要計算區間合,我們就可以使用差分的概念,差分要用到前綴合,為了可以快速計算前綴合,還要使用到BIT。
### 實作
在每次遞迴的層中,先把<=$mid$的數字的位置update到BIT裡,也就是在$pos$的位置+1,這樣在求區間$[l,r]$<=$mid$的個數時就可以用$pref[r]-pre[l-1]$的方法來計算,因為被update進去的都是<=$mid$的數字,加上BIT的前綴合功能,可以很輕易地得知在前綴中有多少數字<=$mid$。
在決定要往左或往右遞迴後,另一邊的數字(假設往左遞迴的話,就是>mid的數字)就不會再被用到,可以只遞迴該邊的數字即可,節省時間。
```cpp=
void go(int l,int r,vector<Num> num,vector<Q> q){
if(l==r){
for(auto [lb,rb,k,i]:q) ans[i]=l;
return;
}
//先把<=mid的數字更新到BIT
int mid=(l+r)/2;
vector<Num> ltmid,gtmid;
for(auto i:num){
if(i.val<=mid) update(i.pos,1);
//往下的遞迴只會用到的其中一邊的元素,先記錄起來
if(i.val<=mid) ltmid.pb(i);
else gtmid.pb(i);
}
//判斷第k小在左邊還右邊
vector<Q> go_l,go_r;
for(auto [lb,rb,k,i]:q){
int L_num=query(rb)-query(lb-1); //計算區間<=mid的數字的數量和
if(k<=L_num) go_l.pb({lb,rb,k,i});
else go_r.pb({lb,rb,k-L_num,i});
}
//因為只有<=mid的數字才會被更新到BIT,所以跑ltmid即可
for(auto i:ltmid) update(i.pos,-1); //clear BIT
go(l,mid,ltmid,go_l);
go(mid+1,r,gtmid,go_r);
}
```
回圈版本 : 只能用在更新要照順序時
```cpp=
bool done=false;
while(true){
vector<int> todo(n);
for(int i=0;i<qt;i++){ //iterator all query
if(q[i].l<q[i].r){
done=false;
int mid=(q[i].l+q[i].r)/2;
todo[mid].push_back(i);
}
}
if(done) break;
int sum=0;
for(int i=0;i<n;i++){
update(a[i]); //update to BIT or something
sum=query(i); //get prefix
for(auto j:todo[i]){
if(sum<=q[j].need) q[j].r=mid;
else q[j].l=mid+1;
}
}
}
```