例如該圖,節點$2$表明的是值域爲$[1, 2]$的區間,節點$6$表明值域爲$[3, 4]$的區間...ios
可持久化實質上就是存儲該數據結構全部的歷史狀態,以達到高效的處理某些信息的目的。算法
題目連接:給定長度爲$N$的序列$A$,有$M$次詢問,給定$l_i, r_i, k_i$,求在$[l_i, r_i]$區間內第$k_i$小的數是多少。數組
$N <= 10^5, M <= 10^4$數據結構
咱們能夠創建一顆權值線段樹,每一個點存儲的信息爲該值域區間存在的數的個數。學習
由於線段樹的性質,因此每一個點的左子樹的值域區間 $ <= $ 右子樹的值域區間。優化
因此咱們先看左子樹區間有多少個數,記爲$cnt_{left}$。ui
咱們要用$[l_i, r_i]$ 區間的數創建權值線段樹。spa
咱們發現能夠用前綴和來維護:code
只要用預處理大法分別以$[1, l_i]$和$[1, r_i]$的數創建權值線段樹,每一個點的值對位相減便可。blog
發現以$[1, x]$和$[1, x + 1]$區間內的數所創建的權值線段樹的差別僅在一條鏈上:($A[x + 1]$的次數$+1$)。
也就是不超過$log_2n$個點。咱們能夠考慮動態開點:
這樣便可預處理出$[1, x] (1 <= x <= n)$全部的權值線段樹了。
時間複雜度$O(nlog_2n)$,空間複雜度$O(2n + nlog_2n)$。
注意:因爲值域很大,咱們須要離散化一下。
#include <cstdio> #include <iostream> #include <algorithm> using namespace std; const int N = 100005; //d 爲離散化數組 int n, m, len, a[N], d[N]; //T[i] 爲 [1, i] 區間的權值線段樹的根節點 int T[N], tot = 0; //線段樹的每一個點 struct SegTree{ int l, r, v; }t[N * 20]; //建樹 int build(int l, int r){ int p = ++tot, mid = (l + r) >> 1; if(l < r) { t[p].l = build(l, mid); t[p].r = build(mid + 1, r); } t[p].v = 0; return p; } //增長一個數 pre 爲上一個的根節點。 int update(int pre, int l, int r, int v){ int p = ++tot, mid = (l + r) >> 1; t[p].l = t[pre].l, t[p].r = t[pre].r, t[p].v = t[pre].v + 1; if(l < r){ //應該更新哪個值域區間 if(v <= mid) t[p].l = update(t[pre].l, l, mid, v); else t[p].r = update(t[pre].r, mid + 1, r, v); } return p; } //查詢 int query(int x, int y, int l, int r, int k){ //找到了 if(l == r) return l; //對位相減 int sum = t[t[y].l].v - t[t[x].l].v, mid = (l + r) >> 1; if(k <= sum) return query(t[x].l, t[y].l, l, mid, k); else return query(t[x].r, t[y].r, mid + 1, r, k - sum); } int main(){ scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%d", a + i), d[i] = a[i]; //離散化 sort(d + 1, d + 1 + n); len = unique(d + 1, d + 1 + n) - (d + 1); for(int i = 1; i <= n; i++) a[i] = lower_bound(d + 1, d + 1 + len, a[i]) - d; T[0] = build(1, len); for(int i = 1; i <= n; i++) T[i] = update(T[i - 1], 1, len, a[i]); //回答 while(m--){ int l, r, k; scanf("%d%d%d", &l, &r, &k); int ans = query(T[l - 1], T[r], 1, len, k); printf("%d\n", d[ans]); } return 0; }
題目連接:
給定長度爲$N$的序列$A$,有$M$次詢問:
$N <= 10^5, M <= 10^5$
注:這道題也有樹套樹和總體二分的作法,這裏講解的是主席樹 + 樹狀數組思路優化。
考慮到修改操做對每棵權值線段樹的影響是:
這樣作的時間複雜度太高,咱們能夠考慮用樹狀數組的二進制思想進行優化:
$T[i]$這顆線段樹表明$[i - lowbit(x) + 1, x]$這段區間建成的線段樹:
注意,在查詢時的代碼實現:
而後用普通主席樹的方法,讓全部的跟着跳,對位相減便可。
時間複雜度$O(nlog^2n)$, 空間複雜度$O(2n + (n + m)log^2n)$
#include <cstdio> #include <iostream> #include <algorithm> using namespace std; //P爲最多可能的線段樹點數 const int N = 100005, P = N * 441, L = 20; //操做序列 struct Ops{ int i, j, k; }op[N]; //線段樹 struct SegTree{ int l, r, v; }t[P]; //d數組爲離散化數組 int n, m, len = 0, a[N], d[N << 1]; //T[i] 以 [i - lowbit(x) + 1, x] 這段區間的線段樹的根節點 //X[i]、Y[i]表明多個點跟着跳,相似於普通版的$x, y$。 int T[N], tot = 0, X[L], Y[L], cx, cy; char s[2]; //建樹 int build(int l, int r){ int p = ++tot, mid = (l + r) >> 1; t[p].v = 0; if(l < r){ t[p].l = build(l, mid); t[p].r = build(mid + 1, r); } return p; } //更新 int update(int pre, int l, int r, int x, int v){ int p = ++tot, mid = (l + r) >> 1; t[p].l = t[pre].l, t[p].r = t[pre].r, t[p].v = t[pre].v + v; if(l < r){ if(x <= mid) t[p].l = update(t[pre].l, l, mid, x, v); else t[p].r = update(t[pre].r, mid + 1, r, x, v); } return p; } //把 [1, i] (x <= i <= n) 的線段樹中值域爲 a[x] 的次數 += v void inline add(int x, int v){ int val = lower_bound(d + 1, d + 1 + len, a[x]) - d; for(; x <= n; x += x & -x) T[x] = update(T[x], 1, len, val, v); } //查詢 int query(int l, int r, int k){ if(l == r) return l; int mid = (l + r) >> 1, sum = 0; //前綴和 for(int i = 1; i <= cx; i++) sum -= t[t[X[i]].l].v; for(int i = 1; i <= cy; i++) sum += t[t[Y[i]].l].v; if(k <= sum){ //跟着跳 for(int i = 1; i <= cx; i++) X[i] = t[X[i]].l; for(int i = 1; i <= cy; i++) Y[i] = t[Y[i]].l; return query(l, mid, k); }else{ //跟着跳 for(int i = 1; i <= cx; i++) X[i] = t[X[i]].r; for(int i = 1; i <= cy; i++) Y[i] = t[Y[i]].r; return query(mid + 1, r, k - sum); } } int main(){ scanf("%d%d", &n, &m); for(int i = 1; i <= n; i++) scanf("%d", a + i), d[++len] = a[i]; for(int i = 1; i <= m; i++){ scanf("%s", s); if(s[0] == 'Q') { scanf("%d%d%d", &op[i].i, &op[i].j, &op[i].k); }else{ scanf("%d%d", &op[i].i, &op[i].j); d[++len] = op[i].j; op[i].k = 0; } } //離散化 sort(d + 1, d + 1 + len); len = unique(d + 1, d + 1 + len) - (d + 1); //這裏建樹,將每個根節點初始化成1。 T[0] = build(1, len); for(int i = 1; i <= n; i++) T[i] = 1; //創建可持久化線段樹 for(int i = 1; i <= n; i++) add(i, 1); //處理詢問 for(int i = 1; i <= m; i++){ if(op[i].k){ //是查詢操做 cx = 0; cy = 0; //把須要跳的點扔進去 for(int j = op[i].i - 1; j; j -= j & -j) X[++cx] = T[j]; for(int j = op[i].j; j; j -= j & -j) Y[++cy] = T[j]; printf("%d\n", d[query(1, len, op[i].k)]); }else{ //修改操做 add(op[i].i, -1); a[op[i].i] = op[i].j; add(op[i].i, 1); } } return 0; }
參考: