1. 程式人生 > 實用技巧 >【填坑】可持久化線段樹\主席樹 (兩道模板題)

【填坑】可持久化線段樹\主席樹 (兩道模板題)

要解決的問題:

給定一個數列,每次查詢任意區間的第k小數

基本方案:

例如數列 1 3 2 3 6 1

對於每個1~i (1<=i<=n) 區間均建立一棵權值線段樹,記錄這個區間中每種數字的個數

1~4區間的線段樹如下

對於每個節點,權值均為區間1~4的數在節點代表的區間中出現的次數。

按此方法構造n棵線段樹,由字首和思想可知,我們可以用兩棵樹相減來得出給定區間的權值線段樹

若想求x~y區間的權值線段樹,用1~y的樹減去1~x-1的樹即可 (x<y)

必要的空間優化:

構造n棵線段樹太過浪費空間,事實上,在我們按順序構造線段樹時,有許多點的權值沒有變化,是可以重複利用的

如下圖:

序列為 4 3 2 3 6 1

節約了很多空間

具體實現:

  • a、b陣列,儲存輸入資料
  • sz:節點個數
  • rt陣列:儲存每棵線段樹的根節點編號
  • lc、rc陣列:記錄左兒子、右兒子編號,類似於動態開點
  • sum陣列:記錄節點權值
  • p:記錄離散化後序列長度,也是線段樹的區間最大長度

首先預處理,數列中的數可能不是連續的,離散化處理可以節約空間,q即為建立的樹的葉子節點的數量

1 for (int i = 1; i <= n; ++i) a[i] = read(), b[i] = a[i];//複製a陣列
2 sort(b + 1, b + 1 + n);
3 q = unique(b + 1
, b + 1 + n) - b - 1;//unique函式,返回值為去重後的序列長度

然後建立一棵點權均為0的空樹

 1 void build(int &rt, int l, int r)
 2 {
 3     rt = ++sz, sum[rt] = 0;//新點
 4     if (l == r) return;//葉子結點,退出
 5     int mid = (l + r) >> 1;//mid
 6     build(lc[rt], l, mid); build(rc[rt], mid + 1, r);//往下走
 7 }
 8 
 9 
10 build(rt[0], 1
, q);//空樹看成第0棵樹

按1~n的順序,把每個新點都當作根節點來建樹

1 for (int i = 1; i <= n; ++i)
2 {
3     p = lower_bound(b + 1, b + 1 + q, a[i]) - b;//找出新加入的點的位置,用lower_bound
4     rt[i] = update(rt[i - 1], 1, q);
5 } 

這是建樹用的update函式,用二分思想把新點所在區間的權值加一(所有子區間都要更新,每個需要更新的區間都要建立一個新的節點)

1 int update(int o, int l, int r)
2 {
3     int oo = ++sz;//新點
4     lc[oo] = lc[o], rc[oo] = rc[o], sum[oo] = sum[o] + 1;//繼承原點的資訊,權值+1
5     if (l == r) return oo;//葉子結點,退出
6     int mid = (l + r) >> 1;//mid
7     if (mid >= p) lc[oo] = update(lc[oo], l, mid); else rc[oo] = update(rc[oo], mid + 1, r);//新加入的節點在哪個區間,就走到哪個區間裡去
8     return oo;//返回值為新點編號
9 }

查詢操作,b陣列中儲存的是去重後的數列

1 while (m--)
2 {
3         int l = read(), r = read(), k = read();
4         printf("%d\n", b[query(rt[l - 1], rt[r], 1, q, k)]);//字首和思想,[1,r]-[1,l-1]=[l,r]
5 }

query函式,返回值為目標數在b陣列中的位置

1 int query(int u, int v, int l, int r, int k)
2 {//u、v為兩棵線段樹當前節點編號,相減就是詢問區間
3     int mid = (l + r) >> 1, x = sum[lc[v]] - sum[lc[u]];//sum相減,字首和思想,看在左側區間有多少個數
4                                                         //然後與k比較(因為已經排過序了)
5     if (l == r) return l;//葉子結點,找到kth目標,退出
6     if (x >= k) return query(lc[u], lc[v], l, mid, k); else return query(rc[u], rc[v], mid + 1, r, k - x);
7     //kth操作,排名<=左兒子的數的個數,說明在左兒子,進入左兒子;反之,目標在右兒子,排名需要減去左兒子的權值
8 }

注意,主席樹一般開32倍空間

例題:

洛谷P3834

完整模板

 1 #include <bits/stdc++.h>
 2 #define maxn 200010
 3 using namespace std;
 4 int a[maxn], b[maxn], n, m, q, p, sz;
 5 int lc[maxn << 5], rc[maxn << 5], sum[maxn << 5], rt[maxn << 5];
 6 //空間要注意
 7 
 8 inline int read(){
 9     int s = 0, w = 1;
10     char c = getchar();
11     for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
12     for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
13     return s * w;
14 }
15 
16 void build(int &rt, int l, int r){
17     rt = ++sz, sum[rt] = 0;
18     if (l == r) return;
19     int mid = (l + r) >> 1;
20     build(lc[rt], l, mid); build(rc[rt], mid + 1, r);
21 }
22 
23 int update(int o, int l, int r){
24     int oo = ++sz;
25     lc[oo] = lc[o], rc[oo] = rc[o], sum[oo] = sum[o] + 1;
26     if (l == r) return oo;
27     int mid = (l + r) >> 1;
28     if (mid >= p) lc[oo] = update(lc[oo], l, mid); else rc[oo] = update(rc[oo], mid + 1, r);
29     return oo;
30 }
31 
32 int query(int u, int v, int l, int r, int k){
33     int mid = (l + r) >> 1, x = sum[lc[v]] - sum[lc[u]];
34     if (l == r) return l;
35     if (x >= k) return query(lc[u], lc[v], l, mid, k); else return query(rc[u], rc[v], mid + 1, r, k - x);
36 }
37 
38 int main(){
39     n = read(), m = read();
40     for (int i = 1; i <= n; ++i) a[i] = read(), b[i] = a[i];
41     sort(b + 1, b + 1 + n);
42     q = unique(b + 1, b + 1 + n) - b - 1;
43     build(rt[0], 1, q);
44     for (int i = 1; i <= n; ++i){
45         p = lower_bound(b + 1, b + 1 + q, a[i]) - b;
46         rt[i] = update(rt[i - 1], 1, q);
47     } 
48     while (m--){
49         int l = read(), r = read(), k = read();
50         printf("%d\n", b[query(rt[l - 1], rt[r], 1, q, k)]);
51     }
52     return 0;
53 }

例二 hdu6601 (正在填坑)