1. 程式人生 > >可持久化線段樹入門小結

可持久化線段樹入門小結

使用 例如 %s 得到 upd ive hup 小結 區間查詢

以前學過可持久化線段樹,但是只會做區間第k小qwq(逃)。決定這段時間重新撿回來。

推薦博客 https://www.cnblogs.com/flashhu/p/8301774.html

https://yjzoier.gitee.io/hexo/p/af72.html

首先是可持久化線段樹

可持久化線段樹即可以訪問歷史版本的線段樹,暴力做法我們可以對每個歷史版本建一棵線段樹,容易發現這樣會MLE。因為連續版本只是插入一個數的關系,我們發現其實版本i和版本i-1只有一條鏈的差別(從根節點到插入數的葉子結點這條鏈),所有我們可以讓版本i和版本i-1公用相同結點而只建立新的不同鏈。

這樣的可持久化線段樹插入和查詢時間依然是O(logn),且這N棵線段樹的空間只要NlogN。

但是要註意的是因為結點公用的關系,其實我們不能修改歷史版本的信息,而只能發布新版本

洛谷P3919為模板

技術分享圖片
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m,a[N],root[N];
int cnt=0,lc[N<<6],rc[N<<6],val[N<<6];

//動態建樹函數 一般都會返回新建結點編號 
int build(int l,int r) {
    int q=++cnt;  //動態建樹 
    if (l==r) {
        lc[q]
=rc[q]=0; val[q]=a[l]; return q; } int mid=l+r>>1; lc[q]=build(l,mid); rc[q]=build(mid+1,r); return q; } //這個其實是插入函數,因為公用結點的緣故並不能修改版本信息導致其他版本受影響 int update(int p,int l,int r,int x,int v) { //新版本線段樹是基於版本p而來 int q=++cnt; //動態建樹 if (l==r) { lc[q]
=rc[q]=0; val[q]=v; return q; } int mid=l+r>>1; lc[q]=lc[p]; rc[q]=rc[p]; val[q]=val[p]; //為了公用信息,先復制一份 if (x<=mid) lc[q]=update(lc[p],l,mid,x,v); if (x>mid) rc[q]=update(rc[p],mid+1,r,x,v); return q; } int query(int p,int l,int r,int x) { //查詢版本p位置x的值 if (l==r) return val[p]; int mid=l+r>>1; if (x<=mid) return query(lc[p],l,mid,x); if (x>mid) return query(rc[p],mid+1,r,x); } int main() { cin>>n>>m; for (int i=1;i<=n;i++) scanf("%d",&a[i]); root[0]=build(1,n); for (int i=1;i<=m;i++) { int k,opt,x,v; scanf("%d%d",&k,&opt); if (opt==1) { scanf("%d%d",&x,&v); root[i]=update(root[k],1,n,x,v); } else { scanf("%d",&x); printf("%d\n",query(root[k],1,n,x)); root[i]=root[k]; } } return 0; }
View Code

然後我們接著講主席樹,主席樹和可持久化線段樹到底是什麽關系我也不太明白(知乎上有人說是包含關系,有人說是同一樣東西),沒所謂反正這又不是重點(逃)。不管怎樣主席樹和可持久化線段樹確實結構上很像,但是主席樹的思想要更為復雜一些。這裏說一下本蒟蒻的理解,如果錯了還請各位大佬指出,蒟蒻感激不盡 。

先是靜態的主席樹我們以洛谷P3834為例。

首先給出一句話:可持久化線段樹+權值線段樹+前綴和思想=主席樹

這怎麽理解呢?我們先考慮一種暴力的做法:對於前1個數建第一棵權值線段樹(當然你得先去了解什麽是權值線段樹不然沒法往下看),對於前2個數建第二棵權值線段樹,對於前3個數建第三棵權值線段樹......對於前n個數建第n棵權值線段樹。然後我們開始處理詢問,例如詢問為區間[ql,qr]中第K小的數是那個?那麽我們看第ql-1棵權值線段樹第qr棵權值線段樹,假設在第ql-1棵線段樹結點p代表[l,r]區間而在第qr棵線段樹結點q代表[l,r]區間,仔細觀察因為我們上訴建權值線段樹的時候是用了一種前綴和思想建立的,那麽此時的sum[lson[q]]-sum[lson[p]]是不是就代表權值位於[l,mid] (mid=l+r>>1) 的數的個數(並且這些數一定是詢問中[ql,qr]區間中的數:這是前綴和相減的緣故)。那麽我們不斷左右細分最終就能得到答案。

上訴算法在時間上詢問是logn的,但問題是建立n棵權值線段樹會MLE。回想起我們先講的可持久化線段樹,我們發現:哎,這n棵權值線段樹不就可以公用結點信息變成可持久化線段樹嗎?於是我們共用一下信息,呼呼呼,終於得到主席樹啦。

技術分享圖片
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,q,tot,cnt=0;
int a[N],b[N],root[N]; 
struct node{
    int lc,rc,sum;
}tree[N<<5]; 

void pushup(int x) {
    tree[x].sum=tree[tree[x].lc].sum+tree[tree[x].rc].sum;
}

int Build(int l,int r) {  //初始版本的樹,動態建樹 
    int q=++cnt;
    if (l==r) {
        tree[q].sum=0;
        tree[q].lc=tree[q].rc=0;
        return q;
    }
    int mid=l+r>>1;
    tree[q].lc=Build(l,mid);
    tree[q].rc=Build(mid+1,r);
    return q;
}

int Insert(int p,int l,int r,int val) {  //增加新點並返回編號 
    int q=++cnt;
    if (l==r) {
        tree[q].lc=tree[q].rc=0;
        tree[q].sum=tree[p].sum+1;
        return q;
    }
    int mid=l+r>>1;
    tree[q]=tree[p];
    if (val<=mid) tree[q].lc=Insert(tree[p].lc,l,mid,val);  //插入到左邊,所以只有左兒子改變,其他沿用p點 
    if (val>mid) tree[q].rc=Insert(tree[p].rc,mid+1,r,val); //插入到右邊,所以只有右兒子改變,其他沿用p點 
    pushup(q);  //更新新增點 
    return q;
}

int query(int p,int q,int l,int r,int k) {  //在p版本和q版本線段樹查找[l,r]第k小 
    if (l==r) return a[l];
    int mid=l+r>>1;
    int tmp=tree[tree[q].lc].sum-tree[tree[p].lc].sum;
    if (k<=tmp) return query(tree[p].lc,tree[q].lc,l,mid,k);  //pq版本通史左跳 
    if (k>tmp) return query(tree[p].rc,tree[q].rc,mid+1,r,k-tmp);  //pq版本同時右跳 
}

int main()
{
    scanf("%d%d",&n,&q);
    root[0]=Build(1,n);  //先建一棵空樹 
    for (int i=1;i<=n;i++) scanf("%d",&a[i]);
    memcpy(b,a,sizeof(a));
    sort(a+1,a+n+1);
    tot=unique(a+1,a+n+1)-(a+1);
    
    for (int i=1;i<=n;i++) {
        int x=lower_bound(a+1,a+tot+1,b[i])-a;
        root[i]=Insert(root[i-1],1,tot,x);  //逐個插入數並保留歷史版本 
    }
    
    for (int i=1;i<=q;i++) {
        int l,r,k; scanf("%d%d%d",&l,&r,&k);
        printf("%d\n",query(root[l-1],root[r],1,tot,k));
    }
    return 0;
}
View Code

動態主席樹

好了,我們發現上面的主席樹因為結點公用的緣故不能修改某個版本的信息,那麽要修改的主席樹怎麽辦呢?這就是動態的主席樹了。更準確來說這應該是樹套樹,也有很多人也叫它為動態主席樹或者帶修改的主席樹,但我感覺樹套樹和主席樹差別還是有點大的。

我們以洛谷P2617為例

我們回想靜態的主席樹,它其實就是一棵前綴和線段樹然後共用了結點,相當於root[i]=a[i]+root[i-1](當然這裏的運算不是簡單的加減運算,這是一種抽象的理解方式qwq)。於是我們一旦需要修改某個位置x的值,勢必會使得root[x]到root[n]的值都改變了,如果暴力修改就得把root[x]到root[n]的值都要改變一次,超時。

其實這個問題就是怎樣能滿足root單點修改,區間查詢?看到這個要求我們很熟悉,這不就是樹狀數組嗎?

所以我們線段樹建成一棵樹狀數組就得到了樹套樹,即從外面來看是一棵樹狀數組,但是樹狀數組的每一個結點都是一棵權值線段樹。這裏要和主席樹區分開來:例如主席樹的root[3]=a[1]+a[2]+a[3]這是主席樹的前綴和思想,但是樹套樹的root[3]=a[3]因為樹狀數組的構造裏sum[3]只有a[3]這是樹狀數組的思想。也就是說主席樹一個點就是前綴,樹套樹要一堆點和和才是前綴。

這也啟發我們學習樹套樹的,不妨把一棵樹當成一個結點去思考,這樣會更容易想象。

樹套樹的細節還是很多的,細節實現建議看代碼,蒟蒻學習的是上面博客大佬的寫法。因為使用的是動態建樹所以時間和空間復雜度都是O(n log^2n),對於這個問題貼一句上面博客大佬的話:其實還有一個問題,一開始本蒟蒻想不通,就是N棵線段樹已經無法共用內存了,那空間復雜度不會是O(N2logN)嗎?其實沒必要擔心的。。。。。。只考慮修改操作,每次有log棵線段樹被挑出來,每個線段樹只修改log個節點,因此程序一趟跑下來,僅有Nlog2N個節點被訪問過,我們只需要動態開點就好了。

技術分享圖片
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,num,tot,a[N],root[N],b[N<<1];
int n1,n2,t1[N],t2[N];
int opt[N],ql[N],qr[N],qv[N];
int lc[N<<9],rc[N<<9],val[N<<9];

void update(int &rt,int l,int r,int x,int v) {  //修改rt這棵線段樹的x點加上v 
    if (!rt) rt=++num;
    val[rt]+=v;
    if (l==r) return;
    int mid=l+r>>1;
    if (x<=mid) update(lc[rt],l,mid,x,v);
    if (x>mid) update(rc[rt],mid+1,r,x,v);
}

void update_pre(int id,int v) {  //樹狀數組修改部分 
    int x=lower_bound(b+1,b+tot+1,a[id])-b;
    for (int i=id;i<=n;i+=i&-i)
        update(root[i],1,tot,x,v);
}

//這裏的查詢部分還是有主席樹的前綴和相減思想 
int query(int l,int r,int k) {  //查詢部分 
    if (l==r) return l;
    int mid=l+r>>1,sum=0;
    for (int i=1;i<=n2;i++) sum+=val[lc[t2[i]]];  //這一堆組成1~r的前綴 
    for (int i=1;i<=n1;i++) sum-=val[lc[t1[i]]];  //這一堆組成1~l-1的前綴 
    if (sum>=k) {
        for (int i=1;i<=n1;i++) t1[i]=lc[t1[i]];  //一起向左跳 
        for (int i=1;i<=n2;i++) t2[i]=lc[t2[i]];
        return query(l,mid,k);  //答案在左子樹 
    } else {
        for (int i=1;i<=n1;i++) t1[i]=rc[t1[i]];  //一起向右跳 
        for (int i=1;i<=n2;i++) t2[i]=rc[t2[i]];
        return query(mid+1,r,k-sum);  //答案在右子樹 
    }
}

int query_pre(int l,int r,int k) {  //樹狀數組查詢部分 
    n1=n2=0;
    for (int i=l-1;i>=1;i-=i&-i) t1[++n1]=root[i];  //先把所有相關的線段樹找出來,
    for (int i=r;i>=1;i-=i&-i) t2[++n2]=root[i];  //因為這一堆線段樹加起來才是前綴 
    return query(1,tot,k);
}

int main()
{
    cin>>n>>m;
    for (int i=1;i<=n;i++) scanf("%d",&a[i]),b[++tot]=a[i];
    for (int i=1;i<=m;i++) {
        char s[3]; scanf("%s",s); 
        if (s[0]==Q) scanf("%d%d%d",&ql[i],&qr[i],&qv[i]);
        else {
            opt[i]=1;
            scanf("%d%d",&ql[i],&qv[i]);
            b[++tot]=qv[i];  //因為要離散化所以把修改的值也先讀入 
        }
    }
    
    sort(b+1,b+tot+1);
    tot=unique(b+1,b+tot+1)-(b+1);
    for (int i=1;i<=n;i++) update_pre(i,1);  //建樹 
    
    for (int i=1;i<=m;i++)
        if (!opt[i]) {
            printf("%d\n",b[query_pre(ql[i],qr[i],qv[i])]);
        } else {
            update_pre(ql[i],-1);  //先清除 
            a[ql[i]]=qv[i];
            update_pre(ql[i],1);   //再插入 
        }
    return 0;
}
View Code

可持久化線段樹入門小結