學習筆記:可持久化線段樹(主席樹):靜態 + 動態

學習筆記:可持久化線段樹(主席樹):靜態 + 動態


前置知識:

  1. 線段樹。線段樹分享能夠看:@秦淮岸@ZYzzz@妄想の嵐がそこに
  2. 樹狀數組。$BIT$分享能夠看:@T-SherlockChicago@weishengkun
  3. 權值線段樹:至關於將線段樹當成一個,其中的每個點所表明的區間至關於一段值域。維護的值爲這段值域中的一些信息。

例如該圖,節點$2$表明的是值域爲$[1, 2]$的區間,節點$6$表明值域爲$[3, 4]$的區間...ios

  1. 可持久化概念:

可持久化實質上就是存儲該數據結構全部的歷史狀態,以達到高效的處理某些信息的目的。算法

靜態區間第$k$小

拋出問題

題目連接:給定長度爲$N$的序列$A$,有$M$次詢問,給定$l_i, r_i, k_i$,求在$[l_i, r_i]$區間內第$k_i$小的數是多少。數組

$N <= 10^5, M <= 10^4$數據結構

先考慮如何求總序列第$k$小

咱們能夠創建一顆權值線段樹,每一個點存儲的信息爲該值域區間存在的數的個數學習

由於線段樹的性質,因此每一個點的左子樹的值域區間 $ <= $ 右子樹的值域區間。優化

因此咱們先看左子樹區間有多少個數,記爲$cnt_{left}$。ui

  • 若是$k_i <= cnt_{left}$,說明第$k_i$小的數必定在左子樹的值域內,因此問題便轉換爲了「在左子樹的值域內找第$k_i$小的數」。
  • 不然,說明第$k_i$小的數必定在左子樹的值域內,考慮到左子樹已經有$cnt_{left}$個最小的數,問題便轉換爲了「在右子樹的值域內找第$k_i - cnt_{left}$小的數」

問題轉換到任意區間

咱們要用$[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;
}

動態區間第$k$小

拋出問題

題目連接

給定長度爲$N$的序列$A$,有$M$次詢問:

  1. 給定$l_i, r_i, k_i$,求在$[l_i, r_i]$區間內第$k_i$小的數是多少。
  2. 給定$x_i, val_i$,將$A[x_i]$的值改成$val_i$。

$N <= 10^5, M <= 10^5$

解決方案:主席樹 + 樹狀數組思路優化

注:這道題也有樹套樹和總體二分的作法,這裏講解的是主席樹 + 樹狀數組思路優化。

考慮到修改操做對每棵權值線段樹的影響是:

  1. 設修改前的值爲$w$,則$[1, x] (x_i <= x <= n)$的線段樹都把值域爲$w$的點$-1$
  2. $[1, x] (x_i <= x <= n)$的線段樹都把值域爲$val_i$的點$+1$

這樣作的時間複雜度太高,咱們能夠考慮用樹狀數組的二進制思想進行優化:

$T[i]$這顆線段樹表明$[i - lowbit(x) + 1, x]$這段區間建成的線段樹:

  1. 修改操做,最多修改$log_2n$顆線段樹便可。
  2. 查詢操做,用不超過$2 * log_2n$顆線段樹就能拼(前綴和)出$[l_i, r_i]$的線段樹。

注意,在查詢時的代碼實現:

  1. 用$X$數組存儲拼出$[1, x - 1]$的全部點。
  2. 用$Y$數組存儲拼出$[1, y]$的全部點。

而後用普通主席樹的方法,讓全部的跟着跳,對位相減便可。


時間複雜度$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;
}

參考:

  1. 主席樹 - 孤獨·粲澤
  2. 淺談權值線段樹到主席樹 - alpha1022
  3. 算法競賽進階指南
  4. 動態第K大&主席樹 - Gitfan
  5. 題解 P2617 【Dynamic Ranking】 - zcysky
相關文章
相關標籤/搜索