主席樹


因為近期進行了有關 主席樹 的專題訓練,為了鞏固對 “主席樹” 的了解,同時方便后期復習,在這里整理了一下有關 “主席樹” 的一些要點。

模板題傳送門

————————————————————

首先,我們來講講

什么是 “主席樹” ?

主席樹 ,又名 “可持久化線段樹” ,顧名思義,其本質就是一棵 “進化版” 的線段樹,且進化了 “可持久化” 這個功能。

那么,什么是 “可持久化”

百度百科
上的解釋是 “持久化是將程序數據在持久狀態和瞬時狀態間轉換的機制” 。

換句話說,傳統意義上的線段樹在修改后就無法復原,只能記錄當前狀態,不夠“持久”,而可持久化線段樹在修改后仍然可以訪問以前的狀態,很“持久”。

可它為什么又叫 “主席樹” ?

這是一個比較有意思的問題,有一種說法是:據說發明主席樹的人的姓名簡寫為hjt,與當時的國家主席的姓名簡寫一樣,故叫做“主席樹”。

————————————————————

了解了主席樹,我們再了解一下

主席樹如何 “持久” ?

傳統的線段樹只能維護一維,並在這一維上進行修改、詢問。

就像這樣:

線段樹

而主席樹在線段樹的基礎上多了一維,相當於同時存了多個線段樹,就像這樣:

主席樹

或者有點像這樣:

主席樹2

但這仍然是暴力開出來的多個線段樹,在實際應用中很容易MLE,可真正的主席樹是一個實用且好用的算法,所以顯然不是這樣。

我們注意到,每次更新線段樹時,只會更新一個點或者一個區間,也就是說,大部分節點是沒有改變的。

只要我們把這些不變的點壓成一個點,無論是時間還是內存上都會大大優化。

換句話說,在更新時,只用將修改的節點重開即可,其他節點保留。

所以真正的主席樹長得有點像這樣:

主席樹3

看不習慣?換一個:

主席樹4

也就是說,主席樹通過建點不建樹這種折中的辦法,不僅節約了時間和空間,還實現了“持久化”。

————————————————————

到這里為止,已經對主席樹有了大致的了解。那么

如何實現主席樹?


模板題
為例。

事先說明,在本題中,主席樹起到了前綴和的作用,主席樹節點以數值(區間)為下標進行二分,主席樹節點權值記錄該數值(區間)出現的數量的前綴和。

先解釋一下主席樹最關鍵的幾個變量:

  • 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}\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM