因為近期進行了有關 主席樹 的專題訓練,為了鞏固對 “主席樹” 的了解,同時方便后期復習,在這里整理了一下有關 “主席樹” 的一些要點。
————————————————————
首先,我們來講講
什么是 “主席樹” ?
主席樹 ,又名 “可持久化線段樹” ,顧名思義,其本質就是一棵 “進化版” 的線段樹,且進化了 “可持久化” 這個功能。
那么,什么是 “可持久化” ?
百度百科
上的解釋是 “持久化是將程序數據在持久狀態和瞬時狀態間轉換的機制” 。
換句話說,傳統意義上的線段樹在修改后就無法復原,只能記錄當前狀態,不夠“持久”,而可持久化線段樹在修改后仍然可以訪問以前的狀態,很“持久”。
可它為什么又叫 “主席樹” ?
這是一個比較有意思的問題,有一種說法是:據說發明主席樹的人的姓名簡寫為hjt,與當時的國家主席的姓名簡寫一樣,故叫做“主席樹”。
————————————————————
了解了主席樹,我們再了解一下
主席樹如何 “持久” ?
傳統的線段樹只能維護一維,並在這一維上進行修改、詢問。
就像這樣:
而主席樹在線段樹的基礎上多了一維,相當於同時存了多個線段樹,就像這樣:
或者有點像這樣:
但這仍然是暴力開出來的多個線段樹,在實際應用中很容易MLE,可真正的主席樹是一個實用且好用的算法,所以顯然不是這樣。
我們注意到,每次更新線段樹時,只會更新一個點或者一個區間,也就是說,大部分節點是沒有改變的。
只要我們把這些不變的點壓成一個點,無論是時間還是內存上都會大大優化。
換句話說,在更新時,只用將修改的節點重開即可,其他節點保留。
所以真正的主席樹長得有點像這樣:
看不習慣?換一個:
也就是說,主席樹通過建點不建樹這種折中的辦法,不僅節約了時間和空間,還實現了“持久化”。
————————————————————
到這里為止,已經對主席樹有了大致的了解。那么
如何實現主席樹?
以
模板題
為例。
事先說明,在本題中,主席樹起到了前綴和的作用,主席樹節點以數值(區間)為下標進行二分,主席樹節點權值記錄該數值(區間)出現的數量的前綴和。
先解釋一下主席樹最關鍵的幾個變量:
- rt,即root,每棵主席樹的根節點。
- ls,即left son,記錄每個節點的左兒子
- rs,即right son,記錄每個節點的右兒子
- t,即tree,記錄每個節點的權值
- tot,即total,統計節點的總個數,用於建新點
主席樹的關鍵在於建點,所以,建點的部分是必不可少的。建新點時要注意將上一個點的狀態繼承過來,這樣的主席樹才是完整的:
void nw(int &p){
++tot;
t[tot]=t[p];
ls[tot]=ls[p];
rs[tot]=rs[p];
p=tot;
}
更新時,大體上與傳統的線段樹相似,都是從\([l,r]\)區間轉移到\([l,mid]\)或\([mid+,r]\)區間。
不同的在於,傳統線段樹轉移時,節點由\(p\)變為\(p<<1\)或\(p<<1|1\),而主席樹因為建了許多新點,編號不是吧連續的,所以轉移時要變為\(ls[p]\)或\(rs[p]\)。
不要忘記建新點,這才是主席樹的精髓!
void change(int l,int r,int x,int &p){
nw(p);++t[p];
if(l==r)return;
int mid=l+r>>1;
if(x<=mid)change(l,mid,x,ls[p]);
else change(mid+1,r,x,rs[p]);
}
詢問時,大體上也和傳統線段樹相似,在本題中,因為詢問的區間第k小,所以需要記錄\(L,R\)兩個節點,通過做差求得區間某數個數,再與k比較判斷向左還是向右轉移:
注意:詢問時不能建新點!
int query(int l,int r,int k,int L,int R){
if(l==r)return l;
int mid=l+r>>1;
int s=t[ls[R]]-t[ls[L]];
if(k<=s)return query(l,mid,k,ls[L],ls[R]);
return query(mid+1,r,k-s,rs[L],rs[R]);
}
綜上,完整的主席樹成型了:
struct HJT{
int tot,ls[MM],rs[MM],t[MM];
void nw(int &p){
++tot;
t[tot]=t[p];
ls[tot]=ls[p];
rs[tot]=rs[p];
p=tot;
}
void change(int l,int r,int x,int &p){
nw(p);++t[p];
if(l==r)return;
int mid=l+r>>1;
if(x<=mid)change(l,mid,x,ls[p]);
else change(mid+1,r,x,rs[p]);
}
int query(int l,int r,int k,int L,int R){
if(l==r)return l;
int mid=l+r>>1;
int s=t[ls[R]]-t[ls[L]];
if(k<=s)return query(l,mid,k,ls[L],ls[R]);
return query(mid+1,r,k-s,rs[L],rs[R]);
}
}T;
再放入題目中,寫得以下代碼:
#include<bits/stdc++.h>
#define lb lower_bound
using namespace std;
const int M=2e5+5,MM=M*20;
int n,m,a[M],b[M],cnt,rt[M];
struct HJT{
int tot,ls[MM],rs[MM],t[MM];
void nw(int &p){
++tot;
t[tot]=t[p];
ls[tot]=ls[p];
rs[tot]=rs[p];
p=tot;
}
void change(int l,int r,int x,int &p){
nw(p);++t[p];
if(l==r)return;
int mid=l+r>>1;
if(x<=mid)change(l,mid,x,ls[p]);
else change(mid+1,r,x,rs[p]);
}
int query(int l,int r,int k,int L,int R){
if(l==r)return l;
int mid=l+r>>1;
int s=t[ls[R]]-t[ls[L]];
if(k<=s)return query(l,mid,k,ls[L],ls[R]);
return query(mid+1,r,k-s,rs[L],rs[R]);
}
}T;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){scanf("%d",&a[i]);b[i]=a[i];}
sort(b+1,b+n+1);cnt=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;++i){
a[i]=lb(b+1,b+cnt+1,a[i])-b;
rt[i]=rt[i-1];
T.change(1,cnt,a[i],rt[i]);
}
for(int l,r,k;m;--m){
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",b[T.query(1,cnt,k,rt[l-1],rt[r])]);
}
return 0;
}
————————————————————
到這里為止,已經掌握了主席樹最基本也是最關鍵的部分。
當然,主席樹的應用還有很多,它不僅可以隨着數組變化,記錄前綴和,還可以隨着時間變化,並隨時回溯(據說Word、Dev等應用中的返回功能與主席樹的原理類似)。
\(\mathcal{By}\quad\mathcal{Most}\ \mathcal{Handsome}\)