演算法學習筆記 主席樹

2022-05-06 21:48:08 字數 3561 閱讀 2666

出題出掛了,來好好學主席樹了

線段樹沒了

對於使用線段樹,我們可以較好地解決「帶修改的全域性第k大(或小)問題」。但是對於某個區間進行求第k大(或小)操作就不是那麼容易了。

可持久化一詞在資料結構中十分常見。「可持久化」的意思就是「帶有歷史版本的」資料結構。而在我們所接觸到的基本資料結構中(如陣列、並查集、平衡樹等)都有各自的可持久化版本。

主席樹,又名「可持久化線段樹」。至於為什麼叫「主席樹」我也不是特別明白。其基本狀態就是乙個可以查詢多個歷史版本的線段樹。

p3834 【模板】可持久化線段樹 1(主席樹)

這是個非常經典的主席樹入門題——靜態區間第 \(k\) 小

如題,給定 \(n\) 個整數構成的序列,將對於指定的閉區間查詢其區間內的第 \(k\) 小值。

第一行包含兩個正整數 \(n,m\),分別表示序列的長度和查詢的個數。

第二行包含 \(n\) 個整數,表示這個序列各項的數字。

接下來 mm 行每行包含三個整數 \(l,r,k\) , 表示查詢區間 \([l,r]\) 內的第 \(k\) 小值。

輸出包含 \(m\) 行,每行乙個整數,依次表示每一次查詢的結果

根據上文所說,主席樹就是帶有歷史版本的線段樹。如果要維護歷史版本,最普通的思想就是建立多個完整的線段樹,並在其上面進行操作。

但是很明顯這個方案是不可行的。對於乙個時間複雜度在 \(o(nlogn)\) 級別上的演算法來說,資料通常在 \(10^5 - 10^6\)。如果維護的歷史版本過多,會導致空間複雜讀過大(每個歷史版本的空間都在 \(n<<2\) 即 \(n*4\) 的級別上)。

對於每次修改,幾乎沒有對全域性所有節點的修改。所以在進行修改時我們只需要將被修改的點建立乙個新的「存檔」就可以完成修改。

一般的我們只需要新建從根開始向下經過的每乙個點就行了。(如下圖所示)

模板題要求查詢區間內第k小。根據以往經驗我們可以選擇運用「權值線段樹」解決問題。

對於題目要求的區間查詢,我們可以運用字首和的思想解決問題,即用 $ [1,l-1] $ 中的資料與 \([1,r]\) 中的資料做差得出區間內的「權值線段樹」(而不用以**的複雜度為每次詢問建樹)

對於主席樹我們並不能像線段樹那樣通過公式(\(ls=now<<1,\ rs=now<<1|1\))計算出其左右兒子的下標,所以我們就需要建立起陣列(或結構體解決)記錄節點資訊

const int n=2e5+15;

int rt[n],ls[n<<5],rs[n<<5],sum[n<<5],tot;

然後就是建樹了。

建樹的時候對於模板題我們需要先建乙個空的樹,作為以後修改的基準點。

void build(int &o,int l,int r)
(真的和線段樹建樹一模一樣呢(大霧))

inline int modify(int o,int l,int r,int p)
模板題的查詢和線段樹的基本相同。在查詢的時候可以想象一顆線段樹,每個節點的值是以 \(rt[r]\) 為根的線段樹相應節點的值減去 \(rt[l-1]\) 為根的線段樹相應節點的值(可以模擬字首合理解)

對於這樣乙個線段樹就可以運用已有知識解決。

inline int query(int l,int r,int l,int r,int k)
主席樹可以較好地維護乙個「支援查詢歷史版本的」線段樹,對於可持久化陣列我們可以利用其支援歷史版本的特點,實現可持久化

如題,你需要維護這樣的乙個長度為 nn 的陣列,支援如下幾種操作

在某個歷史版本上修改某乙個位置上的值

訪問某個歷史版本上的某一位置的值

此外,每進行一次操作(對於操作2,即為生成乙個完全一樣的版本,不作任何改動),就會生成乙個新的版本。版本編號即為當前操作的編號(從1開始編號,版本0表示初始狀態陣列)

輸入的第一行包含兩個正整數 \(n,m\), 分別表示陣列長度和操作的個數

第二行包含 \(n\) 個整數,依次為初始狀態下陣列各位的值 (依次為 \(a_i\), \(1\leq i \leq n\) )

接下來 \(m\) 行包含 \(3\) 或 \(4\) 個整數,代表兩種操作之一 (\(i\) 為基於的歷史版本號):

\(1.\) 對於操作 \(1\) , 格式為 $ v_i\ 1\ loc_i\ value_i$ 即在版本 \(v_i\) 的基礎上, 將 \(a_\) 修改為 \(value_i\)

\(2.\) 對於操作 \(2\) , 格式為 $ v_i\ 2\ loc_i\ $ 即訪問版本 \(v_i\) 中 \(a_\) 的值, 生成一樣版本的物件應為 \(v_i\)

輸出包含若干行,依次為每個操作 \(2\) 的結果。

利用主席樹解決

我們可以原陣列存放在\(rt[0]\)的樹的葉子節點,對於每一次修改直接修改即可

#include#include#include#include#include#include#includeusing namespace std;

#define rg register

#define ll long long

#define ull unsigned long long

namespace enterprise

const int n=1e6+15;

int val[n<<5],rt[n],ls[n<<5],rs[n<<5],a[n],tot;

int n,m;

inline int build(int l,int r)//遍歷到葉子節點賦值

int mid=(l+r)>>1;

ls[o]=build(l,mid);

rs[o]=build(mid+1,r);

return o; }

inline int change(int pre,int l,int r,int x,int v)

int mid=(l+r)>>1;

if(x<=mid) ls[o]=change(ls[pre],l,mid,x,v);

else rs[o]=change(rs[pre],mid+1,r,x,v);

return o; }

inline int query(int now,int l,int r,int x)

inline void main()else

} }}signed main()

主席樹的基本操作的線段樹思想大體一致,時間複雜度也基本一致。

對於建樹,時間複雜度為 \(o(nlogn)\)

由於這裡查詢和修改都是單點操作,所以時間複雜度為 \(o(logn)\)

所總體時間複雜度基本為 \(o((m+n)logn)\)

對於 \(rt[0]\) 來說,空間複雜度=正常線段樹空間複雜度,即最壞 \(o(4n)\)

對於每次修改,由於僅修改了 \(logn\) 個節點,所以所有新建版本的最壞複雜度為 \(o(mlogn)\)

一般的對於所有節點及其對應值(如節點值,左右兒子等)我們可以開20倍空間,或者 \(maxn<<5\) 處理。 各位大佬肯定已經知道了。

主席樹學習筆記

問題 給定乙個n個數的序列,q次詢問第x個數到第y個數中的第k最值。我們假定是第k小。為了使討論更加簡便,我們假定序列的每個數都是不大於n的正整數。當然一般題目中元素範圍很大,但是可以用離散化預處理來做到這一點。考慮乙個比較高階的做法 令g i j 為前i個數中,值為j的數的個數。很容易用o n 2...

主席樹 學習筆記

主席樹就是權值線段樹的乙個集合 模板題為求某個區間的第 k 大,權值線段樹也有求第 k 大這個功能,但是不能維護區間,只能求整個全域性第 k 大 所以學習這個之前,務必先搞懂權值線段樹 所以 都是這道題的 模板 可持久化線段樹 1 主席樹 1 先從建樹開始說起 int build int l,int...

主席樹學習筆記

學習博文 主席樹總結 p3834 模板 可持久化線段樹 2 主席樹 給出乙個序列,每次詢問給定區間內第k小的值。主席樹模板。考慮最簡單的情況,也就是查詢區間固定。首先對資料進行離散化,用線段樹維護。每個節點對應離散化後值域的數的總個數 size.從上到下進行查詢時,判斷當前節點左子樹的 size 和...