主席樹入門詳解+題目推薦


主席樹學名可持久化線段樹,就是這個可持久化,衍生了多少數據結構

為什么會有主席樹這個數據結構呢?它被發明是用來解決什么問題的呢?

給定n個數,m個操作,操作類型有在某個歷史版本下單點修改,輸出某個歷史版本下某個位置的值的值,n和m小於等於1e6

乍一看是不是一點頭緒也沒有。我們先來想想暴力怎么做,暴力存儲第i個狀態下每個數的值,顯然這樣做不是TLE就是MLE,我們不妨管這種狀態叫做TM雙LE。

如果沒有這個歷史狀態顯然處理很簡單,一個線段樹就解決了。那么加上歷史狀態呢?如果我們優化一下暴力,我們會發現我們可以建若干棵樹,一棵樹存儲一個狀態下的所有信息。

顯然這種處理方式還不如剛才呢,狀態的轉移依然很慢,MLE也更加嚴重了,所以我們還是TM雙LE。怎么辦呢?我們要想辦法加快轉移,同時優化空間,兩者要同時做到似乎有點難,這個時候就要用到主席樹了。

主席樹是怎么維持可持久化的呢?跟上面說的一樣建若干棵樹,第i棵樹表示第i次操作后的狀態。我們會發現,在每次修改時,兩個子節點中只有一個會被修改,也就是說一次修改只會有logn個節點被修改,那么顯然所有節點都新建備份是又慢又浪費的。我們可以讓修改后的樹跟修改前的樹共享節點,大大節省了時間和空間,這道題就做完了。

這是題面

那么直接上代碼吧

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cctype>
#define ll long long
#define gc getchar
#define maxn 1000005
using namespace std;

inline ll read(){
	ll a=0;int f=0;char p=gc();
	while(!isdigit(p)){f|=p=='-';p=gc();}
	while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
	return f?-a:a;
}int n,m,a[maxn];

struct ahaha{
	int v,ch[2];
}t[maxn*20];int cnt,num,rt[maxn];
#define lc t[i].ch[0]
#define rc t[i].ch[1]
#define Lc t[j].ch[0]
#define Rc t[j].ch[1]
void build(int &i,int l,int r){
	i=++num;
	if(l==r){t[i].v=a[l];return;}
	int m=l+r>>1;
	build(lc,l,m);build(rc,m+1,r);
}
void update(int &i,int j,int l,int r,int k,int z){
	i=++num;lc=Lc;rc=Rc;  //共用一個子節點節省空間,加快速度
	if(l==r){t[i].v=z;return;}
	int m=l+r>>1;
	if(k<=m)update(lc,Lc,l,m,k,z);
	else update(rc,Rc,m+1,r,k,z);
}
int query(int i,int l,int r,int k){
	if(l==r)return t[i].v;
	int m=l+r>>1;
	if(k<=m)return query(lc,l,m,k);
	return query(rc,m+1,r,k);
}

inline void solve_1(int k){
	int x=read(),z=read();
	update(rt[++cnt],rt[k],1,n,x,z);
}
inline void solve_2(int k){
	int x=read();rt[++cnt]=rt[k];
	printf("%d\n",query(rt[cnt],1,n,x));
}

int main(){
	n=read();m=read();
	for(int i=1;i<=n;++i)
		a[i]=read();
	build(rt[0],1,n);  //先把第0版本的樹建出來
	while(m--){
		int k=read(),zz=read();
		switch(zz){
			case 1:solve_1(k);break;
			case 2:solve_2(k);break;
		}
	}
	return 0;
}

提到主席樹,想必各位最先想到的還是區間第k大

區間第k大是怎么利用可持久化的呢?

首先說一下什么是權值線段樹。平常的線段樹下標是表示第幾個數,權值線段樹的下標是代表數字的值,那么節點的權值就是代表數字出現的次數。

那么維護區間第k大就需要建n棵權值線段樹,第i棵樹維護的是區間\([1,i]\)中每個數出現的次數

很顯然用剛才的方法維護就ok了

上代碼

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cctype>
#define ll long long
#define gc getchar
#define maxn 200005
using namespace std;

inline ll read(){
	ll a=0;int f=0;char p=gc();
	while(!isdigit(p)){f|=p=='-';p=gc();}
	while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
	return f?-a:a;
}int n,m,cnt,a[maxn],b[maxn];

struct ahaha{
	int v,ch[2];
}t[maxn*20];int num,rt[maxn];
#define lc t[i].ch[0]
#define rc t[i].ch[1]
#define Lc t[j].ch[0]
#define Rc t[j].ch[1]
void update(int &i,int j,int l,int r,int k){
	i=++num;t[i]=t[j];++t[i].v;
	if(l==r)return;
	int m=l+r>>1;
	if(k<=m)update(lc,Lc,l,m,k);
	else update(rc,Rc,m+1,r,k);
}
int query(int i,int j,int l,int r,int k){
	if(l==r)return l;
	int m=l+r>>1,v=t[Lc].v-t[lc].v;
	if(k<=v)return query(lc,Lc,l,m,k);
	return query(rc,Rc,m+1,r,k-v);
}

inline void solve(){
	int x=read(),y=read(),k=read();
	printf("%d\n",b[query(rt[x-1],rt[y],1,cnt,k)]);   //別忘了要求輸出的是原數,別把離散化后的值輸出了
}

int main(){
	n=read();m=read();
	for(int i=1;i<=n;++i)  //先要離散化,否則沒法存
		a[i]=b[i]=read();
	sort(b+1,b+n+1);cnt=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;++i)   //建n棵權值線段樹
		update(rt[i],rt[i-1],1,cnt,lower_bound(b+1,b+cnt+1,a[i])-b);
	while(m--)
		solve();
	return 0;
}

這就是主席樹,是不是很簡單。

有人也許會問,知道單點修改的主席樹怎么寫了,區間修改的怎么寫呢?

它的本質是一樣的,只需要把修改的值做一個永久標記在它的祖先們身上,然后求交就可以了

題單

KUR-Couriers

Count on a tree(樹上第k大)

可持久化並查集

粟粟的書架

混合果汁

這篇文章對你有沒有幫助呢?有的話,點個贊吧。

如果有什么不滿意的地方,歡迎在評論區反饋


免責聲明!

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



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