[學習筆記] 可持久化線段樹&主席樹

衆所周知,線段樹是一個很是好用也好寫的數據結構,ios

所以,咱們今天的前置技能:線段樹.數組

然而,可持久化究竟是什麼東西?數據結構

別急,咱們一步一步來...ui

step 1

首先,一道簡化的模型:spa

給定一個長度爲\(n\)的序列,\(m\)個操做,支持兩種操做:code

  • 修改某個點\(i\)的權值
  • 查詢歷史上某個版本\(u\)中點\(i\)的權值

同時,每一個操做都會生成一個新的版本(也就是說修改是改的一個新的版本,而查詢是直接\(copy\)上一個版本.blog

那麼,暴力的作法來了:圖片

直接維護\(m\)棵線段樹,先\(copy\)一遍,再直接修改/查詢.ci

然而時間空間都得炸啊啊啊get

別急,讓咱們仔細想一想...

首先,咱們考慮一下每次修改會發生什麼(看圖):

(其中修改的是6號節點,紅色是通過的路徑),

咱們能夠發現,每次修改都只改了一條鏈.

也就是說,對於上一個版本,就只有一條鏈不同(查詢就是如出一轍了).

所以,對於上一個版本中同樣的其餘的鏈,咱們就能夠直接沿用.

好比說,上一個版本長這樣:

而沿用後的圖就長這樣選點時的隨意致使了圖片的醜陋:

那麼,咱們就只須要在更新版本時,新建一個根節點\(rt[i]\),

而且只須要新建修改的那條鏈,其餘的沿用上一個版本的就好了.

代碼也很簡單:

void update(int &k/*動態加的點(當前節點)*/,int p/*沿用的點,即上個版本中這個位置的節點編號*/,int l,int r,int pla/*修改的元素位置*/,int x/*修改的權值*/){
    k=++tot;t[k]=t[p];//先copy一波
    if(l==r){t[k].val=x;return ;}
    int mid=(l+r)>>1;//這裏就和線段樹同樣了
    if(pla<=mid) update(t[k].l,t[p].l,l,mid,pla,x);
    else update(t[k].r,t[p].r,mid+1,r,pla,x);
}

順便把建樹和詢問也貼上來吧(其實和線段樹同樣):

void build(int &x,int l,int r){
    x=++tot;
    if(l==r){t[x].val=a[l];return ;}
    int mid=(l+r)>>1;
    build(t[x].l,l,mid);build(t[x].r,mid+1,r);
}

int query(int p,int l,int r,int x){
    if(l==r) return t[p].val;
    int mid=(l+r)>>1;
    if(x<=mid) return query(t[p].l,l,mid,x);
    else return query(t[p].r,mid+1,r,x);
}

到這裏,一個簡單的模板就結束啦.

例題:[模板]可持久化數組

這題就和模板如出一轍,

在葉子節點記錄權值,每次單點修改便可.

注意一下,詢問是複製的詢問的那個版本(不是上一個)由於這個調了很久qwq

上完整代碼吧其實都在上面了~:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

inline int read(){
    int sum=0,f=1;char c=getchar();
    while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
    while(c<='9'&&c>='0'){sum=sum*10+c-'0';c=getchar();}
    return sum*f;
}

const int MX=1000005;
struct tree{int l,r,val;}t[MX*30];
int n,m,a[MX];
int rt[MX],tot=0;

void build(int &x,int l,int r){
    x=++tot;
    if(l==r){t[x].val=a[l];return ;}
    int mid=(l+r)>>1;
    build(t[x].l,l,mid);build(t[x].r,mid+1,r);
}

void update(int &k,int p,int l,int r,int pla,int x){
    k=++tot;t[k]=t[p];
    if(l==r){t[k].val=x;return ;}
    int mid=(l+r)>>1;
    if(pla<=mid) update(t[k].l,t[p].l,l,mid,pla,x);
    else update(t[k].r,t[p].r,mid+1,r,pla,x);
}

int query(int p,int l,int r,int x){
    if(l==r) return t[p].val;
    int mid=(l+r)>>1;
    if(x<=mid) return query(t[p].l,l,mid,x);
    else return query(t[p].r,mid+1,r,x);
}

int main(){
    n=read();m=read();
    for(int i=1;i<=n;i++) a[i]=read();
    build(rt[0],1,n);
    for(int i=1;i<=m;i++){
        int tt=read(),opt=read(),pla=read();
        if(opt==1){int x=read();update(rt[i],rt[tt],1,n,pla,x);}
        else if(opt==2){printf("%d\n",query(rt[tt],1,n,pla));rt[i]=rt[tt];}
    }
    return 0;
}

step 2

接下來,就是咱們喜聞樂見的主席樹了(話說有哪位\(dalao\)能告訴我爲何叫這名字?).

咱們以一道模板題開始吧:[模板]可持久化線段樹 1(主席樹)

給出一個長度爲\(n\)的序列,\(m\)個詢問,每次詢問區間\([l,r]\)中第\(k\)小的數.

這題暴力很好寫吧(然而咱們並不知足於暴力).

咱們仍是一步步來,

首先,先考慮下若是是區間\([1,n]\)的第\(k\)小該怎麼作:

這時候,咱們能夠考慮到權值線段樹.

將原序列離散化,再建一棵線段樹,

但這個線段樹維護的區間並非序列的區間,

而是權值的區間.

好比說區間\([l,r]\),其實指的是離散化後的權值\(l\)~\(r\),

也就是第\(l\)大到第\(r\)大.

仍是舉個栗子吧:

假設咱們如今有一個序列\(a\):1,3,3,5,5,5,8,8,8,8.

那麼離散化之後就是:1,2,2,3,3,3,4,4,4,4.

而後咱們再建一棵權值線段樹:

其中黑色的表明節點編號,紅色的表明權值區間,

而藍色的是接下來咱們要講的每一個節點維護的一個值:\(cnt\).

這個\(cnt\)表示的是當前節點的權值區間內有幾個節點.

聽不懂?不要緊咱們接着看:

當咱們講第一個元素(在離散化後就是\(1\))插入之後,樹就變成了這樣:

全部權值區間包括\(1\)\(cnt\)都加了\(1\).

而當咱們將全部數都插進去後,樹就成了這樣:

因而,咱們就能夠清楚地看到,在序列中,權值在區間\([l,r]\)的數有多少個.

插入的代碼以下:

int update(int p/*節點編號*/,int l,int r/*l,r爲權值區間*/,int k/*插入的權值*/){
    int x=++tot;t[x]=t[p];t[x].cnt++;
    if(l==r) return x;
    int mid=(l+r)>>1;
    if(k<=mid) t[x].l=build(t[p].l,l,mid,k);
    else t[x].r=build(t[p].r,mid+1,r,k);
    return x;
}

而後咱們在求第\(k\)小時,先與左子樹的\(cnt\)比較,

\(k<=cnt\),那答案就在左子樹的權值區間裏,

不然,將\(k\)減去\(cnt\),再在右子樹裏找,

一直到最底層就好了.

查詢代碼以下:

int query(int p/*當前節點*/,int l,int r,int k/*第k小*/){
    if(l==r) return l;
    int mid=(l+r)>>1;
    if(k<=t[t[p].l].cnt) return query(t[p].l,l,mid,k);
    else return query(t[p].r,mid+1,r,k-t[t[p].l].cnt);
}

那麼接下來,咱們來考慮區間\([l,r]\)的第\(k\)小:

仔細想一想,其實咱們能夠用前綴和的思想,

一個權值爲\(x\)的數在\(1\)~\(l-1\)中出現了\(a\)次,

\(1\)~\(r\)中出現了\(b\)次,

那麼它在區間\([l,r]\)中就出現了\(a-b\)次.

所以,咱們能夠對每一個區間\([1,i],i\in [1,n]\)建一顆權值線段樹,

在查詢時,用前綴和的思想計算\(cnt\),再查找就行啦.

然而到這裏就結束了嗎?

咱們注意到,對於區間\([1,i]\)的權值線段樹,

與區間\([1,i-1]\)比起來僅僅是多插入了一個\(i\)的權值而已.

想到了什麼? 可持久化線段樹!

沒錯,咱們能夠像可持久化線段樹同樣,

沿用相同的節點,只新建須要更新的一條鏈就好了.

貼上新的詢問代碼:

int query(int L/*區間[1,l-1]的節點*/,int R/*區間[1,r]的節點*/,int l,int r,int k/*第k小*/){
    if(l==r) return l;
    int mid=(l+r)>>1,sum=t[t[R].l].cnt-t[t[L].l].cnt;//前綴和
    if(sum>=k) return ask(t[L].l,t[R].l,l,mid,k);
    else return ask(t[L].r,t[R].r,mid+1,r,k-sum);   
}

到這裏,主席樹就講完啦.

上完整代碼吧:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

inline int read(){
    int sum=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9'){sum=sum*10+c-'0';c=getchar();}
    return sum*f;
}

struct tree{int cnt,l,r;}t[5000001];
int n,m,a[1000001],c[1000001];
int rt[500001],tot=0;

int build(int p,int l,int r,int k){
    int x=++tot;
    t[x]=t[p];t[x].cnt++;
    if(l==r) return x;
    int mid=(l+r)>>1;
    if(k<=mid) t[x].l=build(t[p].l,l,mid,k);
    else t[x].r=build(t[p].r,mid+1,r,k);
    return x;
}

int ask(int L,int R,int l,int r,int k){
    if(l==r) return l;
    int mid=(l+r)>>1,sum=t[t[R].l].cnt-t[t[L].l].cnt;
    if(sum>=k) return ask(t[L].l,t[R].l,l,mid,k);
    else return ask(t[L].r,t[R].r,mid+1,r,k-sum);   
}

int main(){
    n=read();m=read();
    for(int i=1;i<=n;i++) a[i]=read();
    memcpy(c,a,sizeof(c));sort(c+1,c+n+1);
    int T=unique(c+1,c+n+1)-c-1;
    for(int i=1;i<=n;i++) a[i]=lower_bound(c+1,c+T+1,a[i])-c;
    for(int i=1;i<=n;i++) rt[i]=build(rt[i-1],1,T,a[i]);
    for(int i=1;i<=m;i++){
        int l=read(),r=read(),k=read();
        printf("%d\n",c[ask(rt[l-1],rt[r],1,T,k)]);
    }
    return 0;
}

之後可能還會更新(埋個坑在這)...

step 3

還真的更新了...

以前,咱們講了可持久化線段樹的單點修改對吧.

然而,區間修改去哪了?

可是咱們仔細想一想,

對於每一個版本的線段樹,

它們是共用了一些節點,

因此在\(pushdown\) \(tag\)的時候,就會出鍋...(本身\(yy\)一下就清楚了)

所以,咱們有了一種新的操做——標記永久化.

將一個點的\(tag\)一直存下來,在詢問的時候直接加上去.

而在修改的時候,只要被區間覆蓋到,就要新建節點.

而且,還要一邊切割須要修改的區間(這個等下看代碼吧),

一直到徹底重合時再返回.

來上修改和查詢的代碼吧:

int update(int p,int l,int r,int d){
    int x=++tot;t[x]=t[p];
    t[x].sum+=(r-l+1)*d;
    if(t[x].l==l&&t[x].r==r){//徹底重合時返回
        t[x].tag+=d;return x;
    }
    int mid=(t[x].l+t[x].r)>>1;
    if(r<=mid) t[x].ls=update(t[p].ls,l,r,d);//把要修改的區間切一下
    else if(l>mid) t[x].rs=update(t[p].rs,l,r,d);
    else t[x].ls=update(t[p].ls,l,mid,d),t[x].rs=update(t[p].rs,mid+1,r,d);
    return x;
}

ll ask(int p,int ad/*一路加上的tag*/,int l,int r){
    if(t[p].l==l&&t[p].r==r){
        return (r-l+1)*ad/*別忘了tag*/+t[p].sum;
    }
    int mid=(t[p].l+t[p].r)>>1;
    if(r<=mid) return ask(t[p].ls,ad+t[p].tag,l,r);
    else if(l>mid) return ask(t[p].rs,ad+t[p].tag,l,r);
    else return ask(t[p].ls,ad+t[p].tag,l,mid)+ask(t[p].rs,ad+t[p].tag,mid+1,r);
}

接下來,讓咱們來看道例題吧:洛谷SP11470 TTM - To the moon

這題就是標記永久化的板子了.

看代碼吧:

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
using namespace std;

inline int read(){
    int sum=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9'){sum=sum*10+c-'0';c=getchar();}
    return sum*f;
}

struct tree{int l,r,ls,rs;ll sum,tag;}t[2000021];
int n,m,a[500001];
int tt=0,tot=0,rt[500001];

inline void pushup(int p){
    t[p].sum=t[t[p].ls].sum+t[t[p].rs].sum;
}

inline void build(int &p,int l,int r){
    p=++tot;t[p].l=l;t[p].r=r;
    if(l==r){t[p].sum=a[l];return ;}
    int mid=(l+r)>>1;
    build(t[p].ls,l,mid);build(t[p].rs,mid+1,r);
    pushup(p);
}

int update(int p,int l,int r,int d){
    int x=++tot;t[x]=t[p];
    t[x].sum+=(r-l+1)*d;
    if(t[x].l==l&&t[x].r==r){
        t[x].tag+=d;return x;
    }
    int mid=(t[x].l+t[x].r)>>1;
    if(r<=mid) t[x].ls=update(t[p].ls,l,r,d);
    else if(l>mid) t[x].rs=update(t[p].rs,l,r,d);
    else t[x].ls=update(t[p].ls,l,mid,d),t[x].rs=update(t[p].rs,mid+1,r,d);
    return x;
}

ll ask(int p,int ad,int l,int r){
    if(t[p].l==l&&t[p].r==r){
        return (r-l+1)*ad+t[p].sum;
    }
    int mid=(t[p].l+t[p].r)>>1;
    if(r<=mid) return ask(t[p].ls,ad+t[p].tag,l,r);
    else if(l>mid) return ask(t[p].rs,ad+t[p].tag,l,r);
    else return ask(t[p].ls,ad+t[p].tag,l,mid)+ask(t[p].rs,ad+t[p].tag,mid+1,r);
}

inline void change(){
    int l=read(),r=read(),d=read();
    rt[tt+1]=update(rt[tt],l,r,d);tt++;
}

inline void query(int x){
    int l=read(),r=read(),opt=(x? read():tt);
    printf("%lld\n",ask(rt[opt],0,l,r));
}

inline void back(int x){
    tt=x;tot=rt[x+1]-1;
}

int main(){
    n=read();m=read();
    for(int i=1;i<=n;i++) a[i]=read();
    build(rt[0],1,n);
    for(int i=1;i<=m;i++){
        char opt[5];cin>>opt;
        if(opt[0]=='C') change();
        else if(opt[0]=='Q') query(0);
        else if(opt[0]=='H'){query(1);}
        else if(opt[0]=='B'){int x=read();back(x);}
    }
    return 0;
}
相關文章
相關標籤/搜索