【填坑】可持久化線段樹\主席樹 (兩道模板題)
阿新 • • 發佈:2020-09-16
要解決的問題:
給定一個數列,每次查詢任意區間的第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 (正在填坑)