良心的可持久化線段樹教程


良心的可持久化線段樹教程


在O~I~中輾轉了千~百天,終於可以隨手寫出各種打標記的、不打標記的、一維的、二維的、求最值的、求和的、求第k大的線段樹之后——

我們來學習可持久化線段樹吧!

什么是可持久化線段樹?

可持久化線段樹最大的特點是:可以訪問歷史版本。例如,我對線段樹進行了1000次修改操作,突然問你第233次修改之后某個區間的區間和是多少——這個問題可持久化線段樹就可以正常地回答出來。這個性質有許多奇妙的應用。

那么如何實現這樣的一棵線段樹呢?

想象一棵普通的線段樹,我們要對它進行單點修改,需要修改\(\log n\)個點。每次修改的時候,我們絲毫不修改原來的節點,而是在它旁邊新建一個節點,把原來節點的信息(如左右兒子編號、區間和等)復制到新節點上,並對新節點進行修改。

那么如何查詢歷史版本呢?只需記錄每一次修改對應的新根節點編號(根據上面描述的操作,根節點每次一定會新建一個的),每次詢問從對應的根節點往下查詢就好了。

可持久化線段樹的代碼實現

我們以維護區間和的可持久化線段樹為例,下面實現的這棵樹支持:單點修改;單點查詢。

要定義的數組:

int idx; //index,記錄目前一共建過多少節點
int sum[M], lson[M], rson[M]; //區間和、左兒子、右兒子
int root[N]; //每次修改對應的根節點編號

假設這道題一開始序列全是0,首先我們把一棵空的樹建出來:

void build(int &k, int l, int r){
    //k傳的是地址,這樣在這一層函數中修改k就可以直接修改上一層的lson或rson了
    k = ++idx; //為新節點編號
    if(l == r) return; //一定要在創建完新節點之后再return
    int mid = (l + r) >> 1;
    build(lson[k], l, mid);
    build(rson[k], mid + 1, r);
}

接下來實現修改操作,把位置p上的數增加x。

//old是這個位置原來的節點,k是當前要創建的新節點(的地址)
void change(int old, int &k, int l, int r, int p, int x){
    k = ++idx; //修改的時候要創建新點
    lson[k] = lson[old], rson[k] = rson[old];
    sum[k] = sum[old] + x; //先把原來節點的信息復制過來,順便修改區間和
    if(l == r) return; //仍然要記得先建點后return
    int mid = (l + r) >> 1;
    if(p <= mid) change(lson[k], lson[k], l, mid, p, x);
    else change(rson[k], rson[k], mid + 1, r, p, x);
}

下面進行區間和查詢(這個函數和普通線段樹幾乎完全一樣)。

int query(int k, int l, int r, int ql, int qr){
    if(ql <= l && qr >= r) return sum[k];
    int mid = (l + r) >> 1, ans = 0;
    if(ql <= mid) ans += query(lson[k], l, mid, ql, qr);
    if(qr > mid) ans += query(rson[k], mid + 1, r, ql, qr);
    return ans;
}

然后一棵可持久化線段樹就完成了!

可持久化線段樹注意事項:

  1. 取地址符
  2. 先建點,后if(l==r)return
  3. 空間要開\((n + Q\log n)\)那么大

可持久化線段樹的應用:區間第k大

例題:51nod 1175 區間中第k大的數

給出一個序列,每次詢問一個區間[l, r]和數字k,問區間中第k大的數是多少。

這個經典的問題還可以用划分樹解決——這玩意我還寫過,毫無疑問地寫跪了。

好在可持久化線段樹寫這個也非常方便好寫,只是需要先離散化處理一下,並且對詢問離線。

下面介紹的解法巧妙利用了可持久化線段樹支持查找歷史版本的特點:

首先把詢問按照右端點排序。
建立一棵可持久化線段樹,維護每個數出現的次數(若數范圍大,需要離散化)。
從左往右掃一遍序列,在可持久化線段樹中給對應的數+1。
當處理到某個詢問(l, r)的右端點時,發現:對於任意一個數,(右端點加入后的線段樹上的值 - 左端點加入前的線段樹上的值)就是區間中這個數出現的次數。那么在這棵“減出來的”線段樹上進行類似約瑟夫問題的“求第k大數”查詢即可。

代碼:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
#define INF 0x3f3f3f3f
#define space putchar(' ')
#define enter putchar('\n')
template <class T>
bool read(T &x){
    char c;
    bool op = 0;
    while(c = getchar(), c < '0' || c > '9')
        if(c == '-') op = 1;
        else if(c == EOF) return 0;
    x = c - '0';
    while(c = getchar(), c >= '0' && c <= '9')
        x = x * 10 + c - '0';
    if(op) x = -x;
    return 1;
}
template <class T>
void write(T x){
    if(x < 0) putchar('-'), x = -x;
    if(x >= 10) write(x / 10);
    putchar('0' + x % 10);
}

const int N = 50005, M = 2000005;
int n, Q, ans[N], a[N], lst[N], cnt, num[N]; //用於離散化
int idx, sum[M], lson[M], rson[M], root[N];
struct Query{
    int id, l, r, x;
    bool operator < (const Query &b) const{
        return r < b.r;
    }
} q[N];

void build(int &k, int l, int r){
    k = ++idx;
    if(l == r) return;
    int mid = (l + r) >> 1;
    build(lson[k], l, mid);
    build(rson[k], mid + 1, r);
}
void change(int old, int &k, int l, int r, int p, int x){
    k = ++idx;
    lson[k] = lson[old], rson[k] = rson[old];
    sum[k] = sum[old] + x; //先復制一波之前的節點,順便修改區間和
    if(l == r) return;
    int mid = (l + r) >> 1;
    if(p <= mid) change(lson[k], lson[k], l, mid, p, x);
    else change(rson[k], rson[k], mid + 1, r, p, x);
}
int query(int new_k, int old_k, int l, int r, int x){ //查詢第x小
    if(l == r) return l;
    int mid = (l + r) >> 1, sum_right = sum[rson[new_k]] - sum[rson[old_k]];
    if(sum_right >= x)
        return query(rson[new_k], rson[old_k], mid + 1, r, x);
    else
        return query(lson[new_k], lson[old_k], l, mid, x - sum_right);
}
int find(int x){
    return lower_bound(num + 1, num + cnt + 1, x) - num;
}

int main(){
    read(n);
    for(int i = 1; i <= n; i++)
        read(lst[i]), a[i] = lst[i];
    sort(lst + 1, lst + n + 1);
    for(int i = 1; i <= n; i++)
        if(i == 1 || lst[i] != lst[i - 1])
            num[++cnt] = lst[i];
    build(root[0], 1, cnt);
    read(Q);
    for(int i = 1; i <= Q; i++){
        q[i].id = i;
        read(q[i].l), q[i].l++;
        read(q[i].r), q[i].r++;
        read(q[i].x);
    }
    sort(q + 1, q + Q + 1);
    for(int i = 1, j = 1; i <= n; i++){
        change(root[i - 1], root[i], 1, cnt, find(a[i]), 1);
        while(q[j].r == i)
            ans[q[j].id] = query(root[i], root[q[j].l - 1], 1, cnt, q[j].x), j++;
    }
    for(int i = 1; i <= Q; i++)
        printf("%d\n", num[ans[i]]);
    return 0;
}

博主蒟蒻,歡迎指正!

參考:FAreStorm 【填坑】可持久化線段樹解決無修改的區間k大問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM