主席樹是一種可持久化線段樹、其發明者orz 黃嘉泰 拼音縮寫與某屆主席一樣、於是這個數據結構被戲稱為主席樹。
所謂的“持久化數據結構”、就是保存這個數據結構的所有歷史版本、同時利用它們之間的共用數據減少時間和空間的消耗。
由於線段樹在區間長度固定的情況下結構都是一致的、主席樹能夠通過兩顆線段樹相減來得到某一區間的信息。
至於主席樹的作用、其能夠查詢不修改的區間K大值、區間不同數的個數、套個樹狀數組還能查詢動態K大值......
給出幾篇文章以便學習 ==> 鏈接II、鏈接II、鏈接III
一些題目 :
HDU 2665 (可作為模板使用)
題意 : 給出一個整數序列、有若干個問詢、每次問詢 (L, R, K) 表示 L~R 區間內第 K 大的值是多少
分析 : 比較裸的主席樹題目
首先先對於每個前綴按權值建出主席樹、然后問詢的時候就可以通過減法得到區間 (L, R) 的信息
由於存儲的是值域信息、查詢K大值的時候就判斷左右子區間的元素個數與K的大小便能知道往哪個方向走

#include<bits/stdc++.h> using namespace std; const int maxn = 1e5 + 10; struct NODE{ int sum, L, R; NODE(){}; NODE(int _sum, int _L, int _R): sum(_sum),L(_L),R(_R){}; }T[maxn*18]; int Tcnt = 0; int root[maxn]; int arr[maxn], N; int uni[maxn], uniLen; int newNode(int sum, int L, int R) { T[++Tcnt] = NODE(sum, L, R); return Tcnt; } inline void Insert(int &root, int pre, int pos, int L, int R) { root = newNode(T[pre].sum+1, T[pre].L, T[pre].R); if(L == R) return ; int M = L + ((R-L)>>1); if(pos <= M) Insert(T[root].L, T[pre].L, pos, L, M); else Insert(T[root].R, T[pre].R, pos, M+1, R); } int Kth(int x, int y, int L, int R, int K) { if(L == R) return L; int M = L + ((R-L)>>1); int L_sum = T[T[y].L].sum - T[T[x].L].sum; if(K <= L_sum) Kth(T[x].L, T[y].L, L, M, K); else Kth(T[x].R, T[y].R, M+1, R, K - L_sum); } int main(void) { int nCase; scanf("%d", &nCase); while(nCase--){ T[0] = NODE(0, 0, 0);///將 0 號節點的左右子樹指向自己 Tcnt = root[0] = 0;///便不用顯式建樹 int Q; scanf("%d %d", &N, &Q); for(int i=1; i<=N; i++){ scanf("%d", &arr[i]); uni[i-1] = arr[i]; } sort(uni, uni+N); uniLen = unique(uni, uni+N) - uni;///離散化 for(int i=1; i<=N; i++){ int pos = lower_bound(uni, uni+uniLen, arr[i]) - uni; pos++; Insert(root[i], root[i-1], pos, 1, uniLen+1); } int l, r, k; while(Q--){ scanf("%d %d %d", &l, &r, &k); int pos = Kth(root[l-1], root[r], 1, uniLen+1, k); printf("%d\n", uni[--pos]); } } return 0; }
題意 : 給出 n 個整數、每次問詢一個區間 (L, R) 問這個區間內不同數的個數是多少?
分析 :
這題很久之前用線段樹離線做過 ==> Click here
如果使用主席樹的話就能做到在線回答問詢
具體做法的核心思路其實和線段樹離線的時候差不多
但是這里主席樹存儲的並不是值域信息、而是區間具體每個位置是否包含一種數
也就是這題主席樹區間代表的信息和普通線段樹所代表的信息一樣
主席樹每次按照前綴建樹、建樹的時候保證每一種數只保留最右邊的位置
然后對於問詢 (l, r) 只要在 root[r] 這顆樹上查詢端點 l 右邊的所有 sum 值的和即可

#include<bits/stdc++.h> using namespace std; const int maxn = 1e5 + 10; struct NODE{ int sum, L, R; NODE(){}; NODE(int _sum, int _L, int _R): sum(_sum),L(_L),R(_R){}; }T[maxn*18]; int Tcnt = 0; int root[maxn]; int arr[maxn], N; int newNode(int sum, int L, int R) { T[++Tcnt] = NODE(sum, L, R); return Tcnt; } inline void Insert(int &root, int pre, int pos, int val, int L, int R) { root = newNode(T[pre].sum+val, T[pre].L, T[pre].R); if(L == R) return ; int M = L + ((R-L)>>1); if(pos <= M) Insert(T[root].L, T[pre].L, pos, val, L, M); else Insert(T[root].R, T[pre].R, pos, val, M+1, R); } int query(int rt, int pos, int L, int R) { if(L == R) return T[rt].sum; int ret = 0; int M = L + ((R-L)>>1); if(pos <= M) ret += query(T[rt].L, pos, L, M) + T[T[rt].R].sum;///遞歸進入左邊區間的時候、要把右邊區間的和加上 else ret += query(T[rt].R, pos, M+1, R); return ret; } map<int, int> mp; int main(void) { T[0] = NODE(0, 0, 0); root[0] = 0; Tcnt = 0; scanf("%d", &N); for(int i=1; i<=N; i++) scanf("%d", &arr[i]); for(int i=1; i<=N; i++){ if(mp.count(arr[i])){ int tmpRoot; Insert(tmpRoot, root[i-1], i, 1, 1, N); Insert(root[i], tmpRoot, mp[arr[i]], -1, 1, N); }else Insert(root[i], root[i-1], i, 1, 1, N); mp[arr[i]] = i; } int Q; scanf("%d", &Q); while(Q--){ int l, r; scanf("%d %d", &l, &r); printf("%d\n", query(root[r], l, 1, N)); } return 0; }