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

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

爲何會有主席樹這個數據結構呢?它被髮明是用來解決什麼問題的呢?git

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

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

若是沒有這個歷史狀態顯然處理很簡單,一個線段樹就解決了。那麼加上歷史狀態呢?若是咱們優化一下暴力,咱們會發現咱們能夠建若干棵樹,一棵樹存儲一個狀態下的全部信息。ui

顯然這種處理方式還不如剛纔呢,狀態的轉移依然很慢,MLE也更加嚴重了,因此咱們仍是TM雙LE。怎麼辦呢?咱們要想辦法加快轉移,同時優化空間,二者要同時作到彷佛有點難,這個時候就要用到主席樹了。spa

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

這是題面get

那麼直接上代碼吧string

#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大it

區間第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大)

可持久化並查集

粟粟的書架

混合果汁

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

若是有什麼不滿意的地方,歡迎在評論區反饋

相關文章
相關標籤/搜索