主席樹學名可持久化線段樹,就是這個可持久化,衍生了多少數據結構
為什么會有主席樹這個數據結構呢?它被發明是用來解決什么問題的呢?
給定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;
}
這就是主席樹,是不是很簡單。
有人也許會問,知道單點修改的主席樹怎么寫了,區間修改的怎么寫呢?
它的本質是一樣的,只需要把修改的值做一個永久標記在它的祖先們身上,然后求交就可以了
題單
這篇文章對你有沒有幫助呢?有的話,點個贊吧。
如果有什么不滿意的地方,歡迎在評論區反饋