本文參考:https://blog.csdn.net/ModestCoder_/article/details/90107874
預備知識
線段樹,權值線段樹,字首和思想,等等
引入
約定:下文中的第\(k\)小/大,寫作\(kth\)
給定一段區間,靜態求區間\(kth\)
思想的推進
思考優化策略
給定n個數,可以對於每個點i都建一棵權值線段樹,維護1~i這些數,統計每個不同的數出現的個數(權值線段樹以值域作為區間)
這樣,n棵線段樹就建出來了,第i棵線段樹代表1~i這個區間
例如,一列數,n為6,數分別為1 3 2 3 6 1
首先,每棵樹都是這樣的:
以第4顆線段樹為例,1~4四個數分別為1 3 2 3
主席樹的本質,就是權值線段樹
節點類似於,桶排序中的桶
因為是同一個問題,n棵權值線段樹的形狀是一模一樣的,只有節點的權值不一樣
所以這樣的兩棵線段樹之間是可以相加減的(兩顆線段樹相減就是每個節點對應相減)
想想,第x棵線段樹減去第y棵線段樹會發生什麼?
第x棵線段樹代表的區間是[1,x]
第y棵線段樹代表的區間是[1,y]
兩棵線段樹一減
設\(x>y,[1,x]-[1,y]=[y+1,x]\)
所以兩個區間相減,可以得到一個新的區間的線段樹
這樣一來,任意一個區間的線段樹,都可以由我這n個基礎區間表示出來了
這就是非常經典的字首和思想
這樣任意一個區間,都有一個對應的線段樹
我們只需要在該區間,找\(kth\)的值就行
這就是主席樹的一個核心思想:字首和思想
現在還有一個嚴峻的問題,就是n棵線段樹空間太大了!
如何優化空間複雜度,是主席樹另一個核心思想
我們發現這n棵線段樹中,有很多重複的點,這些重複的點浪費了大部分的空間,所以考慮如何去掉這些冗餘點
假設現在有一棵線段樹,序列往右移一個單位,建一棵新的線段樹
對於一個兒子節點的值域區間,如果權值有變化,那麼新建一個節點,否則,連到原來的那個節點上
這樣說可能有點抽象
現在來看幾個例子
序列4 3 2 3 6 1
區間[1,1]的線段樹(藍色節點為新節點)
區間[1,2]的線段樹(橙色節點為新節點)
區間[1,3]的線段樹(紫色節點為新節點)
當然,讀到這裡你會被主席樹的思想給秀到,畢竟太優秀了
主席樹的思想就講到這邊,接下來講講程式碼
變數含義
a、b陣列,一般儲存輸入資料
sz:節點個數
rt陣列:儲存每棵線段樹的根節點編號
lc、rc陣列:記錄左兒子、右兒子編號,類似於動態開點
sum陣列:記錄節點權值
q:記錄離散化後序列長度,也是線段樹的區間最大長度
權值線段樹的為什麼都需要離散化?因為權值線段樹的每個節點是一個桶,葉子節點統計一個數值在區間出現的次數,如果沒有離散化,會多建很多不必要的節點,浪費大量的空間。
主席樹的本質也是權值線段樹,所以主席樹也需要離散化
主席樹
主席樹又名可持久化線段樹,顧名思義,它可以把問題的歷史資訊全部記錄下來,實現可持久化
首先,數可能會很大,然而n卻只有200000,所以要離散化,用到unique函式
for (int i = 1; i <= n; ++i) a[i] = read(), b[i] = a[i];//複製a陣列
sort(b + 1, b + 1 + n);
q = unique(b + 1, b + 1 + n) - b - 1;//unique函式,返回值為去重後的序列長度
建一棵空樹,雖然我也不知道為什麼,但是大家都這麼幹,雖說不建也沒關係,以防萬一?反正建一下也不會錯
build(rt[0], 1, q);//空樹看成第0棵樹
1~n依次建樹
p代表a[i]在離散化去重後b中對應的下標
for (int i = 1; i <= n; ++i) {
p = lower_bound(b + 1, b + 1 + q, a[i]) - b;//找出新加入的點的位置,用lower_bound
rt[i] = update(rt[i - 1], 1, q);
}
查詢操作
while (m--) {
int l = read(), r = read(), k = read();
printf("%d\n", b[query(rt[l - 1], rt[r], 1, q, k)]);//字首和思想,[1,r]-[1,l-1]=[l,r]
}
build函式
void build(int& rt, int l, int r) {
rt = ++sz, sum[rt] = 0;//新點
if (l == r) return;//葉子結點,退出
int mid = (l + r) >> 1;//mid
build(lc[rt], l, mid);
build(rc[rt], mid + 1, r);//往下走
}
update函式
int update(int o, int l, int r) {
int oo = ++sz;//新點
lc[oo] = lc[o], rc[oo] = rc[o], sum[oo] = sum[o] + 1;//繼承原點的資訊,權值+1
if (l == r) return oo;//葉子結點,退出
int mid = (l + r) >> 1;//mid
if (p<=mid) lc[oo] = update(lc[oo], l, mid);
else rc[oo] = update(rc[oo], mid + 1, r);//新加入的節點在哪個區間,就走到哪個區間裡去
return oo;//返回值為新點編號
}
int query(int u, int v, int l, int r, int k) {//u、v為兩棵線段樹當前節點編號,相減就是詢問區間
int mid = (l + r) >> 1, x = sum[lc[v]] - sum[lc[u]];//sum相減,字首和思想
if (l == r) return l;//葉子結點,找到kth目標,退出
if (x >= k) return query(lc[u], lc[v], l, mid, k);
else return query(rc[u], rc[v], mid + 1, r, k - x);
//kth操作,排名<=左兒子的數的個數,說明在左兒子,進入左兒子;
//反之,目標在右兒子,排名需要減去左兒子的權值
}
注意:線段樹一般開4倍n的空間,而主席樹開32倍的空間
程式碼實現
#include <bits/stdc++.h>
#define maxn 200010
using namespace std;
int a[maxn], b[maxn], n, m, q, p, sz;
int lc[maxn << 5], rc[maxn << 5], sum[maxn << 5], rt[maxn << 5];
//空間要注意
inline int read() {
int s = 0, w = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
return s * w;
}
void build(int& rt, int l, int r) {
rt = ++sz, sum[rt] = 0;
if (l == r) return;
int mid = (l + r) >> 1;
build(lc[rt], l, mid); build(rc[rt], mid + 1, r);
}
int update(int o, int l, int r) {
int oo = ++sz;
lc[oo] = lc[o], rc[oo] = rc[o], sum[oo] = sum[o] + 1;
if (l == r) return oo;
int mid = (l + r) >> 1;
if (mid >= p) lc[oo] = update(lc[oo], l, mid); else rc[oo] = update(rc[oo], mid + 1, r);
return oo;
}
int query(int u, int v, int l, int r, int k) {
int mid = (l + r) >> 1, x = sum[lc[v]] - sum[lc[u]];
if (l == r) return l;
if (x >= k) return query(lc[u], lc[v], l, mid, k); else return query(rc[u], rc[v], mid + 1, r, k - x);
}
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++i) a[i] = read(), b[i] = a[i];
sort(b + 1, b + 1 + n);
q = unique(b + 1, b + 1 + n) - b - 1;
build(rt[0], 1, q);
for (int i = 1; i <= n; ++i) {
p = lower_bound(b + 1, b + 1 + q, a[i]) - b;
rt[i] = update(rt[i - 1], 1, q);
}
while (m--) {
int l = read(), r = read(), k = read();
printf("%d\n", b[query(rt[l - 1], rt[r], 1, q, k)]);
}
return 0;
}
複雜度分析
時間複雜度
建樹 \(O(nlogn)\)
每次新增一顆新的線段樹,就需要建立\(\log_{2}{n}\)個節點,總共需要加入n棵
詢問 \(O(mlogn)\)
總複雜度\(O((n+m)logn)\)
空間複雜度
一般為\(O(nlog^2 n)\)
後記
發明主席樹的人叫黃嘉泰,縮寫是HJT,所以叫做主席樹