主席樹詳解

2021-08-27 00:11:52 字數 3224 閱讀 7594

給定nn個數(intint範圍內),一共mm次詢問,每次都要詢問區間[l,r][l,r]的第kk大的數。 

其中n,m,l,rn,m,l,r均不超過2×1052×105,保證詢問有答案。

顯而易見,最暴力的辦法就是區間排序然後輸出排序後第kk個數。最壞情況的時間複雜度是o(nmlgn)o(nmlgn),不超時才怪。

於是針對這個問題,新的資料結構誕生了,也就是主席樹。 

主席樹本名可持久化線段樹,也就是說,主席樹是基於線段樹發展而來的一種資料結構。其字首」可持久化」意在給線段樹增加一些歷史點來維護歷史資料,使得我們能在較短時間內查詢歷史資料,圖示如下。 

圖中的橙色節點為歷史節點,其右邊多出來的節點是新節點(修改節點)。 

下面我們來講怎麼構建這個資料結構。

不同於普通線段樹的是主席樹的左右子樹節點編號並不能夠用計算得到,所以我們需要記錄下來,但是對應的區間還是沒問題的。

//節點o表示區間[l,r],修改點為p,修改值根據題意設定(此處我們先不談題目,只談資料結構)

int modify(int o, int l, int r, int p)

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

if(p <= mid) lc[oo] = modify(lc[oo], l, mid);

else rc[oo] = modify(rc[oo], mid+1, r);

//sum[oo] = sum[lc[oo]] + sum[rc[oo]];在該題中,不需要這樣做,但是很多情況下是要這樣更新的

return oo;

}

至於主席樹的區間修改,其實也不難,但是複雜度有點高,簡單點的題目一般只有點修改,有時候區間修改可以轉化為點修改(比如noip2012借教室,有區間修改的解法也有點修改的解法)。

int ql, qr;//查詢區間[l,r]

int query(int o, int l, int r)//節點o代表區間[l,r]

如果只按照上述做法去做的話,每次修改的時間複雜度是o(lgn)o(lgn),每次詢問的複雜度也是o(lgn)o(lgn)。

模板題就是主席樹的典型例題,詢問區間第kk大。先不說區間[l,r][l,r]吧,就說說[1,r][1,r]怎麼做。

由題意知道我們肯定要對區間進行排序,但是我們的排序不是每次詢問才排序,是初始化就排序離散化——針對數字較大但資料較小的情況(具體見方法)。排序離散化完畢後,以離散化數組建主席樹,設ii屬於區間[1,n][1,n],對原陣列的[1,i][1,i]區間的數做統計(例如下圖,區間中按離散化陣列順序統計11的個數、22的個數、33的個數、44的個數、88的個數、99的個數),有序地插入節點到離散化陣列的主席樹中,記錄好原陣列每個節點對應的線段樹起點,針對樣例有幾個示意圖。注意,這裡的橙色節點是新節點,與之前出現的那個圖不一樣。

我們按照上面的做法構建的主席樹是為了方便我們查詢第kk小值。因為我們是以離散陣列構建的主席樹,那麼從根節點出發,左子樹部分的數必定不大於右子樹部分的數。於是就可以將左兒子的節點個數xx與kk做比較,若k≤xk≤x,則第kk小值一定在左子樹裡面,若x≤kx≤k,則第kk小值一定在右子樹裡面,然後遞迴往下走,縮小範圍。值得注意的是,前者遞迴時,kk直接傳下去即可,後者遞迴時,需要將kk減去左子樹的數的個數再傳遞這個kk值。 

例如我們查詢[1,4][1,4]中第22小的值,圖示如下,綠色節點為該值存在的區間位置。 

需要注意的是,第二個綠色節點才是綠色根節點的左子樹,因為左子樹表示的區間是靠前的那一半。 

方法總結如下:

將原始陣列複製乙份,然後排序好,然後去掉多餘的數,即將資料離散化。推薦使用c++的stl中的unique函式;

離散化陣列為基礎,建乙個全00的線段樹,稱作基礎主席樹;

對原資料中每乙個[1,i][1,i]區間統計,有序地插入新節點(題目中ii每增加11就會多乙個數,僅需對主席樹對應的節點增加11即可);

對於查詢[1,r][1,r]中第kk小值的操作,找到[1,r][1,r]對應的根節點,我們按照線段樹的方法操作即可(這個根節點及其子孫構成的必定是一顆線段樹)。

現在我們真正來解決區間詢問[l,r][l,r]的問題。 

構建主席樹的方法是沒有問題的,問題正在於區間詢問怎麼寫。其實,解決方案就是將主席樹[1,r][1,r]減去主席樹[1,l−1][1,l−1]就行了。其實這個原因並不難想,首先看到主席樹的底層,全部是對數的統計。當主席樹[1,r][1,r]減去主席樹[1,l−1][1,l−1]時,統計也跟著減了,也就是說,現在統計記錄的是[l,r][l,r]區間。 

而我們不需要單獨減,只需要邊遞迴查詢邊減,具體見查詢部分**。

//初始的u和v分別代表的是點l-1和點r,l和r分別表示線段樹點代表的區間,初始的k如題

int query(int u, int v, int l, int r, int k)

至此,模板題也就解決了,下面是完整**。注意,修改點定義為了全域性變數。

#include #include #define m 200010

using namespace std;

int node_cnt, n, m;

int sum[m<<5], rt[m], lc[m<<5], rc[m<<5];//線段樹相關

int a[m], b[m];//原序列和離散序列

int p;//修改點

void build(int &t, int l, int r)

int modify(int o, int l, int r)

int query(int u, int v, int l, int r, int k)

int main()

while(m--)

return 0;

}

題目一開始的離散化複雜度為o(nlgn)o(nlgn),構建基礎主席樹複雜度為o(nlgn)o(nlgn),統計並插入的複雜度是o(nlgn+nlgn)=o(nlgn)o(nlgn+nlgn)=o(nlg⁡n),詢問的複雜度是o(mlgn)o(mlgn)。複雜度總和就是o((m+n)lgn)o((m+n)lgn)。

主席樹詳解

題目給你乙個序列,每次修改後算乙個新的版本,詢問某個版本中某個值 我們先以luogu p3919 模板 可持久化陣列 可持久化線段樹 平衡樹 作為模板講一下主席樹先學一下線段樹qaq 主席樹本名可持久化線段樹,也就是說,主席樹是基於線段樹發展而來的一種資料結構。其字首 可持久化 意在給線段樹增加一些...

主席樹詳解注釋

hdu 2665 可作為模板使用 題意 給出乙個整數序列 有若干個問詢 每次問詢 l,r,k 表示 l r 區間內第 k 大的值是多少 分析 比較裸的主席樹題目 首先先對於每個字首按權值建出主席樹 然後問詢的時候就可以通過減法得到區間 l,r 的資訊 由於儲存的是值域資訊 查詢k大值的時候就判斷左右...

區間第K值 主席樹詳解

序 這是一篇遲到的題解,機房的小夥伴們系統地學主席樹應該是七月份的時候,然而我沒趕上趟,當時壓根沒看懂主席樹是什麼東東。昨天晚上決定重新來過,於是請教了一位大神1113 這是他的部落格,不過好像因為手機驗證的原因很久沒有更新了 他告訴我了主席樹的始末,然後我就秒懂了,原來並沒有想象中的那麼複雜,相信...