【主席數】可持續化線段樹

幽靈軒發表於2020-10-28


本文參考: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
首先,每棵樹都是這樣的:
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,所以叫做主席樹

相關文章