衆所周知,線段樹是一個很是好用也好寫的數據結構,ios
所以,咱們今天的前置技能:線段樹.數組
然而,可持久化究竟是什麼東西?數據結構
別急,咱們一步一步來...ui
首先,一道簡化的模型:spa
給定一個長度爲\(n\)的序列,\(m\)個操做,支持兩種操做:code
同時,每一個操做都會生成一個新的版本(也就是說修改是改的一個新的版本,而查詢是直接\(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; }
接下來,就是咱們喜聞樂見的主席樹了(話說有哪位\(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; }
之後可能還會更新(埋個坑在這)...
還真的更新了...
以前,咱們講了可持久化線段樹的單點修改對吧.
然而,區間修改去哪了?
可是咱們仔細想一想,
對於每一個版本的線段樹,
它們是共用了一些節點,
因此在\(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; }