平衡樹學習筆記

DE_aemmprty發表於2024-04-13

不持續更新。

1 FHQ-Treap

1.1 前置知識

BST

Heap

FHQ-Treap 一般使用小根堆。

1.2 FHQ-Treap 簡述

FHQ-Treap 是一種基於分裂和合並操作的平衡樹。它沒有旋轉,極易上手,非常適合 cainiaoshanglu

1.3 FHQ-Treap 核心思想

我們對於一個點儲存兩個權值 \(t_i, a_i\), 其中 \(a_i\) 滿足小根堆性質,\(t_i\) 滿足 BST 性質。 我們可以對於 \(a_i\) 進行隨機賦值, 使得期望時間複雜度為 \(\mathcal{O}(\log{n})\) 的.

FHQ-Treap 基於合併與分裂函式, 輕易的實現了 P3369 的六種功能, 即易懂又他媽的好寫.

1.4 FHQ-Treap 基礎操作

1.4.1 更新操作

用於更新節點資訊改變後節點的值。

void pushUp(int x) {
    t[x].siz = t[t[x].ch[0]].siz + t[t[x].ch[1]].siz + 1;
}

1.4.2 合併操作

滿足 \(a_i\),即小根堆來合併.

發現由於之前將 \(p, q\) 分裂開了,所以 \(q\) 裡面的所有值都大於 \(p\) 的。也就是說,我們只需要確定父子關係即可合併。

假設現在兩棵樹合併到 \(x, y\).

  • 若當前 \(a_x < a_y\),則顯然 \(y\)\(x\) 的右兒子。

  • 若當前 \(a_x > a_y\),則顯然 \(x\)\(y\) 的左兒子。

遞迴合併即可。

int merge(int x, int y) {
    if (!x || !y) return x + y;
    if (t[x].a < t[y].a) {
        t[x].ch[1] = merge(t[x].ch[1], y);
        pushUp(x);
        return x;
    } else {
        t[y].ch[0] = merge(x, t[y].ch[0]);
        pushUp(y);
        return y;
    } 
}

實現細節:注意,這裡的合併函式是預設了 \(x\) 點子樹的所有權值小於等於 \(y\) 點子樹的。

1.4.3 分裂操作

我們透過 \(t_x\),即 BST 來分裂。

假設我們要把 \(\geq p\) 的分裂開來,那麼假設現在走到 \(x\)

  • 如果 \(t_x \geq p\),那麼將 \(x\) 及其右子樹全部連到左樹上,繼續遞迴左兒子。

  • 如果 \(t_x < p\),那麼將 \(x\) 及其左子樹全部連到右樹上,繼續遞迴右兒子。

一般有兩種分裂方式,一種是 按權值 \(t_x\),一種是 按子樹大小 \(siz_x\)。這裡兩種都放一下。

按照權值分裂:

void split(int now, int k, int &x, int &y) {
    if (now == 0) return x = y = 0, void();
    if (t[now].t >= k) x = now, split(t[now].ch[0], k, t[now].ch[0], y);
    else               y = now, split(t[now].ch[1], k, x, t[now].ch[1]);
    pushUp(now);
}

按照大小分裂:

void split(int now, int k, int &x, int &y) {
    if (now == 0) return x = y = 0, void();
    if (k <= t[t[now].ch[0]].siz) x = now, split(t[now].ch[0], k, t[x].ch[0], y);
    else                          y = now, split(t[now].ch[1], k - t[t[now].ch[0]].siz - 1, x, t[x].ch[1]);
    pushUp(now);
}

1.5 FHQ-Treap 複合操作

1.5.1 插入操作

假設插入的權值為 \(v\)

我們先按照權值對 FHQ-Treap 進行分裂,把平衡樹分成 \(\geq v\)\(< v\) 的兩部分。最後分別與新建點合併即可。

void Insert(int key) {
    int p, q;
    split(rt, key, p, q);
    p = merge(newNode(key), p);
    rt = merge(q, p);
}

1.5.2 刪除操作

我們考慮把平衡樹分裂成 \(\geq v\)\(< v\) 的兩部分。

對於 \(\geq v\) 的部分,我們再把他分裂成 \(> v\)\(= v\) 的兩部分。

對於 \(= v\) 的部分,我們可以刪除它的根 —— 把它的兩個兒子合併。最後全部合在一起即可。

void Delete(int key) {
    int p, q, o;
    split(rt, key, p, q);
    split(p, key + 1, p, o);
    o = merge(t[o].ch[0], t[o].ch[1]);
    p = merge(o, p);
    rt = merge(q, p);
}

1.5.3 查詢操作

類似於線段樹上二分,不多敘述。

int Query(int val) {
    int res = 0, now = rt;
    while (now) {
        if (t[now].t >= val) now = t[now].ch[0];
        else {
            res += t[t[now].ch[0]].siz + 1;
            now = t[now].ch[1];
        }
    }
    return res + 1;
}

1.5.4 排名操作

類似於線段樹上二分,不多敘述。

int Rank(int x) {
    int now = rt; x --;
    while (now) {
        if (t[t[now].ch[0]].siz > x) {
            now = t[now].ch[0];
        } else if (x == t[t[now].ch[0]].siz) {
            return t[now].t;
        } else {
            x -= t[t[now].ch[0]].siz + 1;
            now = t[now].ch[1];
        }
    }
    return -1;
}

1.5.5 字首查詢

我們把平衡樹分裂成 \(\geq v\)\(< v\),在 \(< v\) 的部分去暴力跑最小值即可。

int Precursor(int val) {
    int p, q;
    split(rt, val, p, q);
    int x = q, res = -1;
    while (x) {
        res = t[x].t;
        if (t[x].ch[1]) x = t[x].ch[1];
        else break;
    }
    rt = merge(q, p);
    return res;
}

1.5.6 字尾查詢

我們把平衡樹分裂成 \(> v\)\(\leq v\),在 \(< v\) 的部分去暴力跑最大值即可。

int Suffix(int val) {
    int p, q;
    split(rt, val + 1, p, q);
    int x = p, res = -1;
    while (x) {
        res = t[x].t;
        if (t[x].ch[0]) x = t[x].ch[0];
        else break;
    }
    rt = merge(q, p);
    return res;
}

1.6 FHQ-Treap 維護區間資訊

1.6.1 FHQ-Treap 維護區間思想

因為在維護區間的時候,一般把權值的中序遍歷視為這個序列當前的順序,所以 一般情況下,維護區間資訊的 FHQ 權值不滿足小根堆。

於是在分裂時,我們就只能 按照大小分裂

由於 FHQ-Treap 的分裂比較厲害,我們可以透過兩次分裂輕鬆的把代表 \([l, r]\) 的子樹給裂出來。

但是如果直接整個子樹更新,時間複雜度肯定爆炸。所以我們要考慮學習線段樹,在平衡樹上打 tag 即可。

以下是一個區間翻轉的例子。

void Reverse(int l, int r) {
    long long x, y, z;
    split(rt, r, x, y);
    split(y, l - 1, z, y);
    t[z].rev ^= 1;
    y = merge(y, z);
    rt = merge(y, x);
}

1.6.2 維護區間資訊需要注意的點

  • 一定要清空 \(0\) 號節點。由於在沒有兒子的時候兒子變數儲存的是 \(0\),導致在 pushUp 的時候有可能會把 \(0\) 節點的資訊(也就是本來沒有的)給 pushUp 到正常節點上

1.7 FHQ-Treap 經典例題

I 序列終結者

FHQ-Treap 板子題。用來練一下手。

/*******************************
| Author:  DE_aemmprty
| Problem: P4146 序列終結者
| Contest: Luogu
| URL:     https://www.luogu.com.cn/problem/P4146
| When:    2024-04-03 19:10:26
| 
| Memory:  128 MB
| Time:    1000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;

long long read() {
    char c = getchar();
    long long x = 0, p = 1;
    while ((c < '0' || c > '9') && c != '-') c = getchar();
    if (c == '-') p = -1, c = getchar();
    while (c >= '0' && c <= '9')
        x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
    return x * p;
}

const int N = 5e4 + 7;
mt19937 rnd(time(0));

int n, m;

struct FHQ {
    long long t, a, ch[2], val;
    long long rev, add, siz, mx;
} t[N];

struct FHQ_Treap {
    int cnt, rt;
    void init() { cnt = 0, rt = 0; t[0].mx = -2e18;}
    int newNode(int v) {
        t[++ cnt] = {v, rnd(), {0, 0}, 0, 0, 0, 1, 0};
        return cnt;
    }
    void pushUp(int x) {
        t[x].rev = t[x].add = 0;
        t[x].mx = max(t[x].val, max(t[t[x].ch[0]].mx, t[t[x].ch[1]].mx));
        t[x].siz = t[t[x].ch[0]].siz + t[t[x].ch[1]].siz + 1;
    }
    void pushDown(int x) {
        if (t[x].rev) {
            t[t[x].ch[0]].rev ^= 1;
            t[t[x].ch[1]].rev ^= 1;
            swap(t[x].ch[0], t[x].ch[1]);
            t[x].rev = 0;
        }
        t[t[x].ch[0]].add += t[x].add, t[t[x].ch[1]].add += t[x].add;
        t[t[x].ch[0]].mx += t[x].add,  t[t[x].ch[1]].mx += t[x].add;
        t[t[x].ch[0]].val += t[x].add, t[t[x].ch[1]].val += t[x].add;
        t[x].add = 0;
    }
    int merge(int x, int y) {
        if (!x || !y) return x + y;
        pushDown(x), pushDown(y);
        if (t[x].a < t[y].a) {
            t[x].ch[1] = merge(t[x].ch[1], y);
            pushUp(x);
            return x;
        }
        else {
            t[y].ch[0] = merge(x, t[y].ch[0]);
            pushUp(y);
            return y;
        }
    }
    void split(int now, int k, long long &x, long long &y) {
        if (!now) return x = y = 0, void();
        pushDown(now);
        if (t[t[now].ch[0]].siz >= k) x = now, split(t[now].ch[0], k, t[now].ch[0], y);
        else                          y = now, split(t[now].ch[1], k - t[t[now].ch[0]].siz - 1, x, t[now].ch[1]);
        pushUp(now);
    }
    void Insert(int v) {
        long long x, y;
        split(rt, v - 1, x, y);
        y = merge(y, newNode(v));
        rt = merge(y, x);
    }
    void Update(int l, int r, long long v) {
        long long x, y, z;
        split(rt, r, x, y);
        split(y, l - 1, z, y);
        t[z].add += v, t[z].val += v, t[z].mx += v;
        y = merge(y, z);
        rt = merge(y, x);
    }
    void Reverse(int l, int r) {
        long long x, y, z;
        split(rt, r, x, y);
        split(y, l - 1, z, y);
        t[z].rev ^= 1;
        y = merge(y, z);
        rt = merge(y, x);
    }
    long long getMax(int l, int r) {
        long long x, y, z;
        split(rt, r, x, y);
        split(y, l - 1, z, y);
        long long res = t[z].mx;
        y = merge(y, z);
        rt = merge(y, x);
        return res;
    }
} F;

void solve() {
    n = read(), m = read();
    F.init();
    for (int i = 1; i <= n; i ++)
        F.Insert(i);
    while (m --) {
        long long op = read(), l, r, v;
        if (op == 1) {
            l = read(), r = read(), v = read();
            F.Update(l, r, v);
        } else if (op == 2) {
            l = read(), r = read();
            F.Reverse(l, r);
        } else {
            l = read(), r = read();
            cout << F.getMax(l, r) << '\n';
        }
    }
}

signed main() {
    int t = 1;
    while (t --) solve();
    return 0;
}

II Peaks

我們把詢問先離線下來,然後從下往上掃。

使用並查集維護連通塊關係,使用非旋 Treap 來進行第 \(k\) 大的維護。

// 咕。

相關文章