1. 程式人生 > 實用技巧 >主席樹(親媽級註釋)

主席樹(親媽級註釋)

動態主席樹

本文介紹動態主席樹,學習之前,你必須需要先了解靜態主席樹,本文只介紹相對於靜態主席樹多出來的部分。

動態主席樹已經算是另一種資料結構了。這裡以ZOJ2112為例。

首先我們需要知道主席樹是一種離線資料結構,意思是我們不能一邊詢問一邊修改或輸出。所以我們把詢問儲存起來,這裡主要是因為我們要儲存改變的值,不然無法離散化,所以就離線了。這裡要改變一個值,例如把2變成6,那麼2的數量就要少一個,6的數量就要多一個,可我們又不能再開一個線段樹,所以我們考慮將每一個節點都變成一棵樹,每對他進行操作時就是對這棵樹進行操作。下面給出詳細程式碼及其註釋。

#pragma GCC optimize(2)
#include <algorithm>
#include <iostream>
#include <cstdio>
#define sc(a) scanf("%d", &a)
#define _ff(i, a, b) for (int i = (a); i <= (b); ++i)
#define lowbit(x) x&(-x)
using namespace std;
//由於是離線操作,線段樹最多有(N+詢問的個數)棵
const int maxn = 6e4 + 5;
const int maxm = 1e4 + 5;
const int maxk = maxn * 32;
//a陣列為初始陣列,b陣列用來離散化
int a[maxn], b[maxn];
//rt陣列記錄每棵主席樹根節點的下標
int sum[maxk], ls[maxk], rs[maxk], rt[maxn];
int num, n, tot;
/*
    樹狀陣列ta每個節點記錄的是每棵線段樹的根節點    
    ul陣列記錄詢問區間下界的節點下移,ur陣列記錄詢問區間上界的節點下移
*/
int ta[maxn], ul[maxn], ur[maxn];
struct Query{
    int l, r, k;
    bool flag;
}q[maxm];

void build(int &node, int l, int r) {
    node = ++tot;
    sum[node] = 0;
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(ls[node], l, mid);
    build(rs[node], mid + 1, r);
}
void update(int &node, int pre, int l, int r, int pos, int val) {
    node = ++tot;
    sum[node] = sum[pre] + val;
    ls[node] = ls[pre], rs[node] = rs[pre];
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) update(ls[node], ls[pre], l, mid, pos, val);
    else update(rs[node], rs[pre], mid + 1, r, pos, val);
}
//傳入的是在初始數組裡的下標
void add(int x, int val) {
    //記錄其在離散化數組裡的下標
    int pos = lower_bound(b + 1, b + 1 + num, a[x]) - b;
    /*
        這裡注意主席樹每棵線段樹的根節點的下標對應的值與初始陣列a下標對應的值是一樣的
        因為主席樹的構建是按照初始陣列的順序構造的
    */
    for (; x <= n; x += lowbit(x)) update(ta[x], ta[x], 1, num, pos, val);
        
}
//查詢x位置的字首和,f為真代表查詢上界,f為假查詢下界
int getsum(int x, bool f) {
    int res = 0;
    for (; x; x -= lowbit(x)) {
        
        if (f) res += sum[ls[ur[x]]];
        else res += sum[ls[ul[x]]];
    }
    return res;
}
/*
    st代表詢問區間的下界、ed代表詢問區間的上界
    stpos代表第st棵線段樹的根節點、edpos代表第ed棵線段樹的根節點
*/
int query(int st, int ed, int stpos, int edpos, int l, int r, int pos) {
    if (l == r) return l;
    /*
        利用字首和求區間和,分別計算權值線段樹(構成主席樹的每棵線段樹都是一棵權值線段樹)和樹狀陣列
        只需要計算左區間裡數字的個數,如果pos小於等於左區間的數字的個數,則去查詢左區間,反之查詢右區間,
        查詢右區間時,對應的pos要減去左區間數字的個數,因為找的是整個區間的第k個數。
    */
    int tmp = sum[ls[edpos]] - sum[ls[stpos]] + getsum(ed, true) - getsum(st, false);
    int mid = (l + r) >> 1;
    if (pos <= tmp) {
        //查詢的點在左區間
        //樹狀陣列的節點跟隨主席樹的節點下移
        for (int i = ed; i; i -= lowbit(i)) ur[i] = ls[ur[i]];//上界下移
        for (int i = st; i; i -= lowbit(i)) ul[i] = ls[ul[i]];//下界下移
        return query(st, ed, ls[stpos], ls[edpos], l, mid, pos);
    } else {
        //查詢的點在右區間
        //同上
        for (int i = ed; i; i -= lowbit(i)) ur[i] = rs[ur[i]];
        for (int i = st; i; i -= lowbit(i)) ul[i] = rs[ul[i]];
        return query(st, ed, rs[stpos], rs[edpos], mid + 1, r, pos - tmp);
    }
}
signed main() {
    // freopen("a.in", "r", stdin);
    char op[5]; int cntb, lbw, Q;
    int kase; sc(kase);
    while (kase--) {
        cntb = 0, tot = 0; sc(n), sc(Q);
        _ff(i, 1, n) {
            sc(a[i]);
            b[++cntb] = a[i];
        }
        //主席樹是離線的,這也是他的侷限所在。
        _ff(i, 1, Q) {
            scanf("%s", op);
            if (op[0] == 'C') {
                scanf("%d%d", &q[i].l, &q[i].r);
                q[i].flag = false;
                b[++cntb] = q[i].r;
            } else {
                scanf("%d%d%d", &q[i].l, &q[i].r, &q[i].k);
                q[i].flag = true;
            }
        }
        sort(b + 1, b + 1 + cntb);
        num = unique(b + 1, b + 1 + cntb) - b - 1;
        build(rt[0], 1, num);
        _ff(i, 1, n) {
            lbw = lower_bound(b + 1, b + 1 + num, a[i]) - b;
            update(rt[i], rt[i - 1], 1, num, lbw, 1);
        }
        _ff(i, 1, n) ta[i] = rt[0];
        _ff(i, 1, Q) {
            if (q[i].flag) {
                //flag為真代表查詢
                //查詢並記錄查詢上界和下界的根節點
                for (int j = q[i].r; j; j -= lowbit(j)) ur[j] = ta[j];
                for (int j = q[i].l - 1; j; j -= lowbit(j)) ul[j] = ta[j];
                //查詢返回的是其在離散數組裡的下標
                int ans = query(q[i].l - 1, q[i].r, rt[q[i].l - 1], rt[q[i].r], 1, num, q[i].k);
                printf("%d\n", b[ans]);
            } else {
                //把x變成y,相當於刪去一個x,增加一個y,此消彼長
                add(q[i].l, -1);
                a[q[i].l] = q[i].r;
                add(q[i].l, 1);
            }
        }
    }
    return 0;
}