不持續更新。
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\) 大的維護。
// 咕。