可持久化線段樹+主席樹+動態主席樹

 

可持久化線段樹php

 

總體仍是很容易理解的,網上的教程都挺不錯,因此只簡單介紹下ios

可持久化的原理在於,借用已經建過的線段樹的一部分數組

好比,咱們有一個數列$a=\{12,23,34,45,56,67,78,89\}$網絡

而咱們想要帶修改的維護這個數列中$[L,R]$的區間和數據結構

建一顆正常的、維護$a_1$~$a_8$區間和的線段樹就能解決了,這樣就是不修改的狀況ide

 

問題在於,若是想在這個的基礎上維護歷史版本,應當如何處理?函數

假設第一次修改,將$a_3$改成$90$學習

若是咱們據此從新創建一顆線段樹,能夠發現,只有不多的節點跟初始的線段樹有出入ui

若是說的更加確切,有出入的節點爲被修改點及其全部祖先spa

因此,咱們創建一顆新的線段樹,至關於向某個歷史版本插入一條長度爲logN的鏈

而對於這條鏈,每一個節點的一個兒子必定指向一個沒有出入的區間(即以前某個歷史版本的節點)、另外一個必定指向一個包含點修改的區間(新建立的節點),分開操做一下就好了

這樣,$M$次操做時,總體的時空消耗是$O(N+MlogN)$

模板題:洛谷P3919

雖然是可持久化數組,可是稍微修改一下(把修改和查詢換成區間)就是可持久化線段樹了

(註釋的是本身一開始犯的兩個錯誤)

#include <cstdio> #include <cstring> #include <cmath> #include <iostream>
using namespace std; const int MAX=1000005; int sz=1,cnt=0; struct Node { int val,l,r; Node() { val=l=r=0; } }t[MAX*25]; int n,m; int a[MAX]; int id[MAX]; void Build() { id[1]=1; for(int i=1;i<sz;i++) t[i].l=(i<<1),t[i].r=(i<<1)+1; for(int i=1;i<=n;i++) t[i+sz-1].val=a[i]; cnt=(sz<<1)-1; } inline void Modify(int k,int x,int l,int r,int y) { if(l==r) { t[cnt].val=y;//#2: Mistake t[cnt] as t[k]
        return; } int mid=(l+r)>>1; if(x<=mid) { t[cnt].r=t[k].r; t[cnt].l=++cnt; Modify(t[k].l,x,l,mid,y); } else { t[cnt].l=t[k].l; t[cnt].r=++cnt; Modify(t[k].r,x,mid+1,r,y); } } inline int Query(int k,int x,int l,int r) { if(l==r) return t[k].val; int mid=(l+r)>>1; return (x<=mid?Query(t[k].l,x,l,mid):Query(t[k].r,x,mid+1,r)); } int main() { // freopen("input.txt","r",stdin);
    scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); while(sz<n) sz<<=1; Build(); id[0]=1;//#1: Missing initial situation
    
    for(int i=1;i<=m;i++) { int ver,op,x,y; scanf("%d%d",&ver,&op); id[i]=++cnt; if(op==1) { scanf("%d%d",&x,&y); Modify(id[ver],x,1,sz,y); } else { scanf("%d",&x); t[cnt]=t[id[ver]]; printf("%d\n",Query(id[ver],x,1,sz)); } } return 0; }
View Code

 


 

重點仍是真正的應用...好比

主席樹

 

主席樹又叫函數式線段樹,能夠解決的一種問題是 動態區間第$k$小

就是這道經典題:POJ 2104

網上有些博客在介紹的一開始講的太本質了,致使反而有點難理解

***注意:線段樹外的區間指的就是元素位置的區間,而線段樹內的區間指的是元素離散化後的數值的區間***

咱們先考慮,如何經過線段樹,知道一個固定數列中第$k$小的數是多少【雖然這裏的作法顯得很笨,可是是主席樹的簡化版本

咱們能夠將整個數列先離散化,而後對區間中的每個數進行統計

例如:數列$a=\{10,20,30,20,50,10,60,40\}$,離散化後獲得$b=\{1,2,3,2,5,1,6,4\}$

對於數列內每個離散化後的數,咱們創建一個基於數值的 區間和線段樹 統計它的出現次數

($7$、$8$是用來佔位的,能夠無視)

這樣,咱們能夠經過相似二分的思想找到第$k$小,而線段樹的節點已經幫助咱們將區間對半切分

假設咱們想找區間第$7$小:

step 1: 區間$[1,4]$內的數一共出現了$6$次,因此咱們能夠直接進入另外一區間$[5,8]$,而且找這個區間中的第$1$小

step 2: 區間$[5,6]$內的數一共出現了$2$次,因此$[5,8]$中的第$1$小必定也是$[5,6]$中的第$1$小

step 3: 區間$[5,5]$內的數一共出現了$1$次,因此$5$正是$[5,6]$中的第$1$小,即整個查詢區間中的第$7$小

 

有了這樣的鋪墊,咱們能夠考慮引入可持久化的部分了

對於詢問的某個區間$[L_i,R_i]$,咱們就至關於在處理 只加入$L_i$到$R_i$的元素時候,像上面問題同樣的區間第$k$小

因此爲何主席樹叫作函數式線段樹:咱們能夠經過前綴區間的相減來表示任意區間

用人話說,咱們將離散化後的數列$b$的$n$個元素依次加入線段樹中,進而產生$n+1$個歷史版本(第$0$個歷史版本是空線段樹,其他依次爲對$[1,1],[1,2],...,[1,n]$內元素的數值統計而成的線段樹)

經過這個方法,咱們就能表示區間$[L_i,R_i]$所產生的線段樹了:對於每一個節點,用第$R_i$版本的數值減去第$L_i-1$版本的數值(原理同用前綴和求區間和)

因而成功轉化爲了上面的問題

UPD:更新了一下代碼

#include <cstdio> #include <vector> #include <cstring> #include <algorithm>
using namespace std; const int N=200005; int root[N]; struct ChairmanTree { int tot; int ls[N*20],rs[N*20],cnt[N*20]; ChairmanTree() { tot=0; memset(root,0,sizeof(root)); memset(ls,0,sizeof(ls)); memset(rs,0,sizeof(rs)); memset(cnt,0,sizeof(cnt)); } void init() { memset(root,0,sizeof(root)); for(int i=1;i<=tot;i++) ls[i]=rs[i]=cnt[i]=0; tot=0; } void pushup(int k) { cnt[k]=cnt[ls[k]]+cnt[rs[k]]; } void add(int k,int x,int a,int b) { if(a==b) { cnt[++tot]=cnt[k]+1; return; } int cur=++tot,mid=(a+b)>>1; if(x<=mid) { ls[cur]=cur+1,rs[cur]=rs[k]; add(ls[k],x,a,mid); } else { ls[cur]=ls[k],rs[cur]=cur+1; add(rs[k],x,mid+1,b); } pushup(cur); } //在第k個版本插入x 
    void insert(int k,int x,int a,int b) { root[k]=tot+1; add(root[k-1],x,a,b); } //在區間(l,r]中1~x的個數 //記得詢問時l,r爲root 
    int order(int l,int r,int x,int a,int b) { if(a==b) return cnt[r]-cnt[l]; int mid=(a+b)>>1; if(x<=mid) return order(ls[l],ls[r],x,a,mid); else
            return cnt[ls[r]]-cnt[ls[l]]+order(rs[l],rs[r],x,mid+1,b); } //區間(l,r]中第k小的數(不存在返回-1) //記得詢問時l,r爲root 
    int kth(int l,int r,int k,int a,int b) { if(k>cnt[r]-cnt[l]) return -1; if(a==b) return a; int lp=cnt[ls[r]]-cnt[ls[l]],mid=(a+b)>>1; if(lp>=k) return kth(ls[l],ls[r],k,a,mid); else
            return kth(rs[l],rs[r],k-lp,mid+1,b); } }; int n,m; int a[N]; vector<int> v; ChairmanTree t; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); v.push_back(a[i]); } sort(v.begin(),v.end()); v.resize(unique(v.begin(),v.end())-v.begin()); for(int i=1;i<=n;i++) t.insert(i,lower_bound(v.begin(),v.end(),a[i])-v.begin()+1,1,v.size()); for(int i=1;i<=m;i++) { int x,y,k; scanf("%d%d%d",&x,&y,&k); printf("%d\n",v[t.kth(root[x-1],root[y],k,1,v.size())]); } return 0; }
View Code

模板(多測記得調用init(),其餘見註釋)

#include <cstdio> #include <cstring> #include <algorithm>
using namespace std; const int N=200005; int root[N]; struct ChairmanTree { int tot; int ls[N*20],rs[N*20],cnt[N*20]; ChairmanTree() { tot=0; memset(root,0,sizeof(root)); memset(ls,0,sizeof(ls)); memset(rs,0,sizeof(rs)); memset(cnt,0,sizeof(cnt)); } void init() { memset(root,0,sizeof(root)); for(int i=1;i<=tot;i++) ls[i]=rs[i]=cnt[i]=0; tot=0; } void pushup(int k) { cnt[k]=cnt[ls[k]]+cnt[rs[k]]; } void add(int k,int x,int a,int b) { if(a==b) { cnt[++tot]=cnt[k]+1; return; } int cur=++tot,mid=(a+b)>>1; if(x<=mid) { ls[cur]=cur+1,rs[cur]=rs[k]; add(ls[k],x,a,mid); } else { ls[cur]=ls[k],rs[cur]=cur+1; add(rs[k],x,mid+1,b); } pushup(cur); } //在第k個版本插入x 
    void insert(int k,int x,int a,int b) { root[k]=tot+1; add(root[k-1],x,a,b); } //在區間(l,r]中1~x的個數 //記得詢問時l,r爲root 
    int order(int l,int r,int x,int a,int b) { if(a==b) return cnt[r]-cnt[l]; int mid=(a+b)>>1; if(x<=mid) return order(ls[l],ls[r],x,a,mid); else
            return cnt[ls[r]]-cnt[ls[l]]+order(rs[l],rs[r],x,mid+1,b); } //區間(l,r]中第k小的數(不存在返回-1) //記得詢問時l,r爲root 
    int kth(int l,int r,int k,int a,int b) { if(k>cnt[r]-cnt[l]) return -1; if(a==b) return a; int lp=cnt[ls[r]]-cnt[ls[l]],mid=(a+b)>>1; if(lp>=k) return kth(ls[l],ls[r],k,a,mid); else
            return kth(rs[l],rs[r],k-lp,mid+1,b); } };
View Code

 


 

動態主席樹

 

上面是簡單的、在固定的數組上進行查詢的主席樹

若是在查詢的同時支持對數組的點修改,不就更加NB了嗎?

可是,這個功能的加入並不簡單...又是看了幾個博客強行理解了很久很久(有些講解對新手不是很友好orz)

首先說明一下,動態主席樹跟靜態主席樹在數據結構上已經有些差距了:動態主席樹說究竟是線段樹套線段樹(外層能夠簡化爲樹狀數組),而靜態主席樹是重複利用的線段樹,二者是有必定區別的

可是,動態主席樹用到了和靜態主席樹相似的實現思想,就是維護前綴和(元素出現次數的前綴和)

在上面的靜態主席樹中,咱們使用了可持久化線段樹來維護元素,而每一個前綴和是一顆線段樹:雖然不一樣歷史版本的線段樹節點之間有交叉以重複利用,但每一個歷史版本都有惟一且獨立的根節點

這就有點像咱們求數列的區間和了:對於一個靜態的數組$a_i$,咱們先計算前綴和$pre_i=pre_{i-1}+a_i$,而後經過$pre_R-pre_{L-1}$來求$[L,R]$的區間和;可是若是想求一個帶修改的數組的區間和,必須使用高級數據結構,例如線段樹/樹狀數組

在這裏也是類似的,只不過區間中的元素從簡單的數字變成了記錄數值出現次數的線段樹了

因而,咱們能夠考慮 外層是線段樹/樹狀數組、內層是記錄數值出現次數的區間和線段樹 這樣的結構

  • 外層維護的是元素位置的區間:若是咱們想查詢$[L,R]$的第$k$小,咱們首先找的是外層的對應$[1,R]$、$[1,L-1]$前綴和的幾段區間(外層的節點,就是內層線段樹的根節點)【外層的線段樹的做用,是爲了幫助咱們找到位置區間對應的幾顆內層線段樹
  • 內層維護的是數值的出現次數:每棵線段樹表示,在根節點對應的外層區間中,每一個數值出現的次數

先不談直觀上是$O(N^2)$的空間消耗(默認已經以原數組爲基礎初始化過了):後面會有辦法解決這個問題;考慮一下使用這樣結構的可行性

 

【修改】

若是將位置$p_i$的數$x$修改成$y$,咱們在外層線段樹發現$p_i$的位置一共被$logN$個區間(節點)包含;同時,以每一個節點爲根節點的線段樹中,分別各有$logN$的節點的值被$x$、$y$影響

因而,對於外層每一個包含$p_i$的節點,咱們都應該在以其爲根節點的內層線段樹中將數值$x$的次數$+1$、將數值$y$的次數$-1$,並一直更新到內層線段樹的根節點

這樣,一次修改的複雜度是$O((logN)^2)$級別的

【查詢】

若是外層是線段樹,對於每次區間$[L,R]$的查詢,咱們都須要先在外層鎖定僅包含區間$[L,R]$的內層根節點,這組節點最多有$logN$個

而後咱們就能夠轉化爲靜態主席樹的簡單版本了,只不過這棵線段樹的每一個節點的數值 都是 以這組以節點爲根的線段樹 相同位置的節點 的數值之和(或者說,咱們把這組線段樹直接數值意義上的疊加在一塊兒)

而後就是同上用相似二分的方法求區間第$k$小,就再也不贅述了

若是外層是樹狀數組,對於每次查詢,咱們都須要先在外層分別鎖定僅包含區間$[1,L-1]$、$[1,R]$的兩組節點,每組節點最多有$logN$個

可是疊加成一顆線段樹時,要減去$[1,L-1]$這組的疊加,加上$[1,R]$這組的疊加,後面仍是同樣的求區間第$k$小

這樣,一次查詢的複雜度也是$O((logN)^2)$級別的

 

如今咱們回到一開始的問題:如何解決爆炸的空間?

若是把內層線段樹的節點所有事先開好的話,就的確是$O(N^2)$的了;但事實上,咱們一共能訪問到內層線段樹的多少節點呢?

每次修改(基於原始數組初始化至關於修改$N$次),同時間複雜度同樣,是$(logN)^2$級別的

每次查詢,仍然同時間複雜度同樣,是$(logN)^2$級別的【可是查詢並不會對任何內、外層節點帶來修改,因此沒有必要開點】

這樣一來,咱們真正能訪問到的,一共也就$N\cdot (logN)^2$個內層線段樹的節點;剩下來的,想都不用想,全是$0$,對於咱們的查詢並不會產生影響

因此,能夠經過相似可持久化線段樹的動態開點解決

 

模板題:洛谷P2617 (比原出處數據更強,甚至直接卡掉$O(N\cdot (logN)^3)$的線段樹套平衡樹)

說一下對個人坑點...其實動態主席樹的實如今常數上是沒多大區別的(線段樹和樹狀數組差很少),我對着本身TLE的代碼、抱着別人AC的代碼,反反覆覆查了一天半都沒找到個因此然

而後,發現個人離散化用的是$map$...在每次修改的時候也是直接用的map來找到離散化後的數(修改時一共調用了$3N$次$map$:初始、加、減各$N$次)

將離散化的互相對應關係用數組從新存了下,時間就直接降到了原來的一半,也就是說:$map$的常數約等於一個$logN$

細思極恐

不過在這裏強烈推薦樹狀數組:在外層的各類定位能夠直接經過加減$lowbit$的$for$循環完成,而線段樹須要遞歸

(又註釋了一些制杖bug)

#include <cstdio> #include <cstring> #include <cmath> #include <cstdlib> #include <map>
using namespace std; const int MAX=100005; int tot=1,sz=1; int t[MAX*400],l[MAX*400],r[MAX*400]; int n,m; int a[MAX]; inline int lowbit(int x) { return x&(-x); } inline void Insert(int k,int p,int a,int b,int x) { if(a==b) { t[k]+=x; return; } int mid=(a+b)>>1; if(p<=mid) { if(!l[k]) l[k]=++tot; Insert(l[k],p,a,mid,x); } else { if(!r[k]) r[k]=++tot; Insert(r[k],p,mid+1,b,x); } t[k]=t[l[k]]+t[r[k]]; } inline void Add(int k,int p,int x) { for(int i=k;i<=n;i+=lowbit(i))//#1: Need setting limits
        Insert(i,p,1,sz,x); } int idx; map<int,int> mp; int rev[MAX<<1];//#4: Forget to expand the size

void Build() { while(tot<n) tot<<=1; for(int i=1;i<=n;i++) Add(i,a[i],1); } int lsz,rsz; int vl[MAX],vr[MAX]; inline int Query(int a,int b,int x) { if(a==b) return a; int mid=(a+b)>>1,sum=0;//#2: Counting left value
    for(int i=1;i<=lsz;i++) sum-=t[l[vl[i]]]; for(int i=1;i<=rsz;i++) sum+=t[l[vr[i]]]; if(sum>=x)//#3: Reverse the operator
 { for(int i=1;i<=lsz;i++) vl[i]=l[vl[i]]; for(int i=1;i<=rsz;i++) vr[i]=l[vr[i]]; return Query(a,mid,x); } else { for(int i=1;i<=lsz;i++) vl[i]=r[vl[i]]; for(int i=1;i<=rsz;i++) vr[i]=r[vr[i]]; return Query(mid+1,b,x-sum); } } inline void Locate(int x,int y) { lsz=rsz=0; for(int i=x;i;i-=lowbit(i)) vl[++lsz]=i; for(int i=y;i;i-=lowbit(i)) vr[++rsz]=i; } char op[MAX]; int x[MAX],y[MAX],k[MAX]; int main() { // freopen("input.txt","r",stdin);
    scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]),mp[a[i]]=1; for(int i=1;i<=m;i++) { op[i]=getchar(); while(op[i]!='C' && op[i]!='Q') op[i]=getchar(); if(op[i]=='Q') scanf("%d%d%d",&x[i],&y[i],&k[i]); else scanf("%d%d",&x[i],&y[i]),mp[y[i]]=1; } for(map<int,int>::iterator it=mp.begin();it!=mp.end();it++) it->second=++idx,rev[idx]=it->first; while(sz<idx) sz<<=1; for(int i=1;i<=n;i++) a[i]=mp[a[i]]; for(int i=1;i<=m;i++) if(op[i]=='C') y[i]=mp[y[i]]; Build(); for(int i=1;i<=m;i++) if(op[i]=='C') { Add(x[i],a[x[i]],-1);//#5: Mistake x[i] as i
            a[x[i]]=y[i]; Add(x[i],a[x[i]],1); } else { Locate(x[i]-1,y[i]); printf("%d\n",rev[Query(1,sz,k[i])]); } return 0; }
View Code

 


 

這樣,可持久化線段樹的概念就算是基本學完了(雖然動態主席樹關聯並無那麼大)←說的好像其餘可持久化數據結構就會了同樣

真正的難點是將可持久化的思想靈活運用到各類各樣刁鑽的題目當中

有時間的話再補些不錯的題目上來orz

這算是我正式開始學習數據結構的入門吧...雖然都是大佬們隨便玩的東西,我枯了

 

HDU 6703 ($array$,$2019CCPC$網絡選拔賽)

這道題主要用到了兩個轉化的思想

第一個是,選$[1,r]$中不出現的數,能夠轉化爲選$[r+1,n]$中出現的數(因爲$a_i$爲$1$到$n$的一個排列)

第二個是,將$a_i$加上$10^7$,至關於原來小於$n$的值對於任意$r$都能被選取,能夠直接向後插入到主席樹上

選取不小於$k$的最小值,能夠經過先查詢$k$的order(小於$k$的數的個數)、再查詢第$order+1$小值完成

#include <cstdio> #include <cstring> #include <algorithm>
using namespace std; const int N=200005; int root[N]; struct ChairmanTree { int tot; int ls[N*20],rs[N*20],cnt[N*20]; ChairmanTree() { tot=0; memset(root,0,sizeof(root)); memset(ls,0,sizeof(ls)); memset(rs,0,sizeof(rs)); memset(cnt,0,sizeof(cnt)); } void init() { memset(root,0,sizeof(root)); for(int i=1;i<=tot;i++) ls[i]=rs[i]=cnt[i]=0; tot=0; } void pushup(int k) { cnt[k]=cnt[ls[k]]+cnt[rs[k]]; } void add(int k,int x,int a,int b) { if(a==b) { cnt[++tot]=cnt[k]+1; return; } int cur=++tot,mid=(a+b)>>1; if(x<=mid) { ls[cur]=cur+1,rs[cur]=rs[k]; add(ls[k],x,a,mid); } else { ls[cur]=ls[k],rs[cur]=cur+1; add(rs[k],x,mid+1,b); } pushup(cur); } //在第k個版本插入x 
    void insert(int k,int x,int a,int b) { root[k]=tot+1; add(root[k-1],x,a,b); } //在區間(l,r]中1~x的個數 //記得詢問時l,r爲root 
    int order(int l,int r,int x,int a,int b) { if(a==b) return cnt[r]-cnt[l]; int mid=(a+b)>>1; if(x<=mid) return order(ls[l],ls[r],x,a,mid); else
            return cnt[ls[r]]-cnt[ls[l]]+order(rs[l],rs[r],x,mid+1,b); } //區間(l,r]中第k小的數(不存在返回-1) //記得詢問時l,r爲root 
    int kth(int l,int r,int k,int a,int b) { if(k>cnt[r]-cnt[l]) return -1; if(a==b) return a; int lp=cnt[ls[r]]-cnt[ls[l]],mid=(a+b)>>1; if(lp>=k) return kth(ls[l],ls[r],k,a,mid); else
            return kth(rs[l],rs[r],k-lp,mid+1,b); } }; int n,m; int a[N]; ChairmanTree t; int main() { int T; scanf("%d",&T); while(T--) { t.init(); scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); t.insert(i,a[i],1,n); } int sz=n,ans=0; for(int i=1;i<=m;i++) { int op,x,y; scanf("%d%d",&op,&x); x^=ans; if(op==1) { if(a[x]<=n) { t.insert(++sz,a[x],1,n); a[x]+=n; } } else { scanf("%d",&y); y^=ans; int num=y-1?t.order(root[x],root[sz],y-1,1,n):0; ans=t.kth(root[x],root[sz],num+1,1,n); if(ans==-1) ans=n+1; printf("%d\n",ans); } } } return 0; }
View Code

 

(完)

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息