【資料結構】淺談主席樹
阿新 • • 發佈:2020-07-14
## 前置知識
①線段樹
②權值線段樹
③桶的思想
④字首和思想
(以上幾個前置知識我也希望我能有時間寫寫自己的部落格講解一下【如果有時間的話嗚噫嗚噫~)
## 模板題
先上幾道模板題壓壓驚,有從別的博主那裡piao來的,也有自己做到的~
因為深刻感受到了,要學習一個東西,最好還是先看看部落格,看看思想,看看程式碼實現
然後!拿著你熱乎的手敲模板去A它個幾道模板題考驗一下你的板子,再繼續深刻理解一下這個演算法的精髓,哦~完美!
[P3919 【模板】可持久化線段樹 1(可持久化陣列)](https://www.luogu.com.cn/problem/P3919)
[P3834 【模板】可持久化線段樹 2(主席樹)](https://www.luogu.com.cn/problem/P3834)
[P1801 黑匣子](https://www.luogu.com.cn/problem/P1801)
[P5838 [USACO19DEC]Milk Visits G](https://www.luogu.com.cn/problem/P5838)(這道題不算模板,但是還蠻有意思,maybe你模板題都寫過之後可以思考一下這道題,然後敲一敲檢驗一下自己是不是真的會了,這道題是dfs+lca+主席樹的,但是解法不僅限於這一種哦~【隊裡大佬就有種超級巧妙超級厲害的解法dfs+lca就可以啦,下次有空也打算把題解寫寫部落格~】)
## 主席樹的含義
主席樹就是可持久化線段樹,它是一種可持久化的資料結構。
那什麼叫做可持久化的資料結構呢?
> 可持久化資料結構就是**支援歷史詢問**的資料結構。
>
> 比如一共有54115411次操作,我問你第251251次操作之後這個資料結構長啥樣,你能在約束的時間空間內回答出來就算支援了可持久化,否則就不算。
>
> 一種很××的做法就是每次更改構之後我都把它儲存下來,然後你問哪次我就去哪次裡面找就是了。但是這顯然在空間上非常不優秀。
>
> 然後前輩們發現,每次修改只會讓該資料結構**某部分與之前不同**,那就只需要記錄這不同的部分就行了。
>
> ——引用自[淺談主席樹](https://www.cnblogs.com/AKMer/p/9956734.html)
## 能解決哪些問題
本質上是為了做 **給你一個序列,每次修改後算一個新的版本,詢問某個版本中某個值** 這個問題的,但是這個問題衍生開來可以演變出很多問題,比如很經典的主席樹模板題**區間第k大問題**。
## 主席樹的原理
普通的線段樹能夠維護**當前狀態**,而主席樹能夠維護 **當前狀態+歷史狀態**
我根據身邊老闆的反應以及自己第一次接觸線段樹的感受,猜測應該很多人第一次聽到歷史狀態這個詞都會特別懵逼,那就舉個例子來具體說明吧~
### 栗子
比如有一個數組,有n個數,分別是a~1~,a~2~,...,a~n~,有以下幾種操作:
只要修改一次陣列的值就會變成一個新的狀態(第一次更新後為第一個狀態,第二次更新後為第二個狀態,以此類推
①對第 i 個狀態進行單點修改,把第 i 個狀態時,第pos個位置變為k。
②查詢第 i 個狀態時,第pos個位置上的值。
像我們平時用線段樹做的題目,那都是在當前基礎上進行修改,這個基礎就是前面進行過的所有操作綜合的結果,要問你第某某次修改之後,第某某個結點的值,我想你必然是答不出來了。
### 暴力
現在我們先來想一個非常暴力的方法來解決這個想要在歷史版本上進行修改/查詢操作的問題吧~
既然我們可以做到在當前狀態下修改啊、查詢啊,說明對於修改查詢其實不是問題,我們對於當前狀態進行修改查詢需要一顆線段樹來維護。那對於上面提出的歷史版本的問題,我們就用好多好多顆線段樹就可以解決啦。
想象一下,你每修改一次(陣列發生改變)後,你就用一顆新的線段樹來儲存修改後這個陣列的狀態。一棵線段樹對應一個歷史版本,那你需要在某個歷史版本上進行修改或者查詢操作的時候是不是隻需要找到這個歷史版本對應的這棵線段樹,然後在這棵線段樹上操作就完事了。
當然,這樣問題倒是解決了,空間也是爆炸了,還是炸的稀碎的那種hhhhhhhh
那怎麼優化呢?
### 空間優化(核心思想一)
那就要用到**主席樹的第一個核心思想**——空間優化
因為我們知道線段樹是一個二叉樹維護狀態,你每一次修改最多會修改掉logN個結點(N是整棵線段樹的節點總數),也就是修改掉從你修改的這個葉子結點一路往上走,走到根結點的這條鏈會發生變化,其他結點都沒有發生變化。因此每一次修改就只需要新建logN個結點供新的這棵線段樹使用,其他的結點跟之前的線段樹共用就可以啦,這樣是不是一下子就省了好多好多空間!
這樣,如果有m次修改,那 **空間複雜度就是N+mlogN** 的,是不是非常理想,非常誘人的一個空間複雜度!
(菜雞第一次自己正兒八經算空間複雜度,如有不對之處,還請各位大佬不吝賜教~)
## 圖解主席樹
舉個栗子說明一下剛剛說的空間優化的過程哈
**序列 4 3 2 3 6 1**
根據**權值線段樹 **的思想,以**值域**作為線段樹的根結點的區間
建一棵如下圖的權值線段樹
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190511122617292.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01vZGVzdENvZGVyXw==,size_16,color_FFFFFF,t_70)
一開始build完一棵初始的樹,都是空的,裡面啥子都沒得。
然後開始把我們序列裡的點一個一個插入進去,先插入第一個數4
首先,先新建一個點作為根節點,因為不管修改哪一個點,根節點一定會被修改掉,因為根節點是掌管整個值域的。
然後看看4是屬於原先那棵樹的哪個兒子呀——右兒子,所以我們要新建一個結點作為新的根節點的右兒子,左兒子沒有被修改,所以新的根結點跟之前版本的根結點共用一下就可以啦
遞迴到下面也是同理哦,被修改了的話就新建一個,沒有的話就共用,nice!
![img](https://img-blog.csdnimg.cn/20190511125552180.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01vZGVzdENvZGVyXw==,size_16,color_FFFFFF,t_70)
插入第2個數 3 的時候是在已經插入了4這個數的基礎上修改,也就是在藍色點的基礎上修改,原理同上
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190511130206210.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01vZGVzdENvZGVyXw==,size_16,color_FFFFFF,t_70)
同上
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190511130727560.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01vZGVzdENvZGVyXw==,size_16,color_FFFFFF,t_70)
圖解大概就是這樣哦
甚至可以結合程式碼來康康!可能會理解得更快一點我猜
圖源[【學習筆記】主席樹](https://blog.csdn.net/ModestCoder_/article/details/90107874)
## 程式碼實現
講完了主席樹的核心思想,就得講講程式碼實現了。
個人學習主席樹最痛苦的經歷就是看不懂博主們的模板~嗚噫嗚噫
所以我覺得學會思想是一回事,把思想跟程式碼結合起來理解就是另外一回事了
所以講程式碼也是很重要的!
所以,我掏出了我四十米的大刀(啊呸,長長的主席樹板子
往上面加上了羅裡吧嗦的註釋
希望能夠幫助各位理解吧
不過感覺這樣比較適合初學者理解程式碼
用的時候這麼多註釋好不優雅哦hhhhhhhhh
【忘記說了】這個板子是直接拉了一個模板題的程式碼,大家可以根據這道題目理解康康
[傳送門](https://www.luogu.com.cn/problem/P3919)
```cpp
#include
#include
#include
#include
#include
#include