1. 程式人生 > >主席樹入門詳解+題目推薦

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

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

為什麼會有主席樹這個資料結構呢?它被髮明是用來解決什麼問題的呢?

給定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大)

可持久化並查集

粟粟的書架

混合果汁

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

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