【学习笔记】两种非旋转平衡树在OI中的应用
前言
N O I P NOIP NOIP挂完之后打算把数据结构啃完(不包括 L C C , E T T , A V L , z y f LCC,ETT,AVL,zyf LCC,ETT,AVL,zyf等神仙数据结构),于是就开始学习非旋转的平衡树了。然后发现自己被旋转平衡树坑了好久。。非旋转的代码各种吊打旋转平衡树。。。。废话不多说,今天主要讨论的两种非旋转平衡树分别是fhqTreap和替罪羊树。
fhqTreap
非旋转
T
r
e
a
p
Treap
Treap,范浩强
T
r
e
a
p
Treap
Treap。
众所周知,
T
r
e
a
p
Treap
Treap相当于是
H
e
a
p
+
B
s
t
Heap+Bst
Heap+Bst
而
T
r
e
a
p
Treap
Treap的复杂度保证基于期望意义下树的深度不超过
O
(
l
o
g
n
)
O(logn)
O(logn),而Treap采用旋转是为了满足堆的性质。
也就是说,
T
r
e
a
p
Treap
Treap的旋转操作和复杂度并不直接挂钩,我们抛弃以前的旋转做法,考虑如何插入一个节点
n
w
nw
nw。
一种神奇的思路是,把一棵树劈开,分成两棵树
l
e
f
t
,
r
i
g
h
t
left,right
left,right,这两棵树中
v
l
e
f
t
≤
v
n
w
<
v
r
i
g
h
t
v_{left}\le v_{nw}<v_{right}
vleft≤vnw<vright。把一个节点看成一棵树,把这三棵树合并起来。
这就是FHQ著名的两个操作
S
p
l
i
t
Split
Split和
M
e
r
g
e
Merge
Merge
Split
假设当前节点为 p p p,插入节点的权值是 v v v,分两种情况。
- v [ p ] ≤ v a l v[p]\le val v[p]≤val,此时当前节点以及整个左子树都要放 l e f t left left中,之后如果再有节点放入左子树,一定在当前节点的右边。
- v [ p ] > v a l v[p]> val v[p]>val,此时当前节点以及整个右子树都要放 r i g h t right right中,之后如果再有节点放入右子树,一定在当前节点的左边。
基于这个思路,采用引用的方式可以得到如下代码。
void Split(int u, int &a, int &b, int x) {
if(!u) return void(a = b = 0);
if(val[u] <= x)
a = u, Split(rs[u], rs[a], b, x);
else
b = u, Split(ls[u], a, ls[b], x);
Up(u);
}
核心操作1结束了。
Merge
类似左偏树的合并,不过采用启发式合并,因为 T r e a p Treap Treap是一个堆,而 S p l i t Split Split的过程保证了 M e r g e Merge Merge的顺序,所以合并的时候比较一下键值大小即可。
int Merge(int u, int v) {
if(!u || !v) return u | v;
if(key[u] < key[v]) return rs[u] = Merge(rs[u], v), Up(u), u;
return ls[v] = Merge(u, ls[v]), Up(v), v;
}
剩下的
插入的时候劈成两棵树合并即可。
void Ins(int x) {
int a = 0, b = 0; Get(x);
Split(rt, a, b, x);
a = Merge(a, nw);
rt = Merge(a, b);
}
删除的时候劈成三棵树删除即可。
void Del(int x) {
int a = 0, b = 0, c = 0;
Split(rt, a, b, x);
Split(a, a, c, x - 1);
st[++tp] = c;
c = Merge(ls[c], rs[c]);
a = Merge(a, c);
rt = Merge(a, b);
}
查找第 k k k大就是在平衡树上二分。
int Kth(int p, int k) {
for(;p;) {
if(sz[ls[p]] + 1 == k) return val[p];
sz[ls[p]] < k ? k -= sz[ls[p]] + 1, p = rs[p] : p = ls[p];
}
}
查找排名,前驱和后继就是劈开之后找第 1 1 1大或者第 n n n大
void Rk(int x) {
int a = 0, b = 0;
Split(rt, a, b, x - 1);
printf("%d\n", sz[a] + 1);
rt = Merge(a, b);
}
void Pre(int x) {
int a = 0, b = 0;
Split(rt, a, b, x - 1);
printf("%d\n", Kth(a, sz[a]));
rt = Merge(a, b);
}
void Nxt(int x) {
int a = 0, b = 0;
Split(rt, a, b, x);
printf("%d\n", Kth(b, 1));
rt = Merge(a, b);
}
都是平衡树的正常操作,非旋转 T r e a p Treap Treap直观容易理解,掌握了 M e r g e Merge Merge和 S p l i t Split Split之后可以快速提取出需要的树,然后就是按照题意模拟即可。
总代码
#include<bits/stdc++.h>
const int N = 1e5 + 10;
int ri() {
char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int sz[N], ls[N], rs[N], val[N], key[N], st[N], tp, tot, rt = 1, nw, se = 233;
int Rand() {return se = 1LL * se * 998244353 & 0x7fffffff;}
void Get(int k) {
nw = tp ? st[tp--] : ++tot;
sz[nw] = 1; val[nw] = k; key[nw] = Rand();
ls[nw] = rs[nw] = 0;
}
void Up(int u) {sz[u] = sz[ls[u]] + sz[rs[u]] + 1;}
void Split(int u, int &a, int &b, int x) {
if(!u) return void(a = b = 0);
if(val[u] <= x)
a = u, Split(rs[u], rs[a], b, x);
else
b = u, Split(ls[u], a, ls[b], x);
Up(u);
}
int Merge(int u, int v) {
if(!u || !v) return u | v;
if(key[u] < key[v]) return rs[u] = Merge(rs[u], v), Up(u), u;
return ls[v] = Merge(u, ls[v]), Up(v), v;
}
int Kth(int p, int k) {
for(;p;) {
if(sz[ls[p]] + 1 == k) return val[p];
sz[ls[p]] < k ? k -= sz[ls[p]] + 1, p = rs[p] : p = ls[p];
}
}
void Rk(int x) {
int a = 0, b = 0;
Split(rt, a, b, x - 1);
printf("%d\n", sz[a] + 1);
rt = Merge(a, b);
}
void Pre(int x) {
int a = 0, b = 0;
Split(rt, a, b, x - 1);
printf("%d\n", Kth(a, sz[a]));
rt = Merge(a, b);
}
void Nxt(int x) {
int a = 0, b = 0;
Split(rt, a, b, x);
printf("%d\n", Kth(b, 1));
rt = Merge(a, b);
}
void Ins(int x) {
int a = 0, b = 0; Get(x);
Split(rt, a, b, x);
a = Merge(a, nw);
rt = Merge(a, b);
}
void Del(int x) {
int a = 0, b = 0, c = 0;
Split(rt, a, b, x);
Split(a, a, c, x - 1);
st[++tp] = c;
c = Merge(ls[c], rs[c]);
a = Merge(a, c);
rt = Merge(a, b);
}
int main() {
int n = ri(); Get(0x7fffffff); sz[1] = 0;
for(;n--;) {
int op = ri(), x = ri();
if(op == 1) Ins(x);
else if(op == 2) Del(x);
else if(op == 3) Rk(x);
else if(op == 4) printf("%d\n", Kth(rt, x));
else if(op == 5) Pre(x);
else Nxt(x);
}
return 0;
}
效率和旋转的基本相同。
应用
非旋转
T
r
e
a
p
Treap
Treap最牛逼的地方在于,它能够做到可持久化。
可持久化最重要的是函数式编程的思想。
考虑
S
p
l
i
t
Split
Split和
M
e
r
g
e
Merge
Merge的时候仅仅会接触到
O
(
d
)
O(d)
O(d)个节点,所以每次切割的操作都新建这
O
(
d
)
O(d)
O(d)个节点,把原来的信息保留,把劈开或合并之后的信息接上去就可以做到
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)得到所有版本的平衡树。
可持久化平衡树
#include<bits/stdc++.h>
const int N = 5e5 + 10;
int ri() {
char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int se = 703;
int Rand() {return se = (se * 998244353LL & 0x7fffffff);}
struct Node {
int ls, rs, val, key, sz;
void Init(int x) {
ls = rs = 0; sz = 1;
key = rand(); val = x;
}
}t[N * 50];
int Rt[N * 50], cnt, tp, st[N * 50];
int Nw() {return tp ? st[tp--] : ++cnt;}
void Up(int p) {t[p].sz = t[t[p].ls].sz + t[t[p].rs].sz + 1;}
void Split(int p, int k, int <, int &rt) {
if(!p) return void(lt = rt = 0);
if(t[p].val <= k)
t[lt = Nw()] = t[p], Split(t[p].rs, k, t[lt].rs, rt), Up(lt);
else
t[rt = Nw()] = t[p], Split(t[p].ls, k, lt, t[rt].ls), Up(rt);
}
int Merge(int u, int v) {
if(!u || !v) return u | v;
int np = Nw();
if(t[u].key > t[v].key)
t[np] = t[u], t[np].rs = Merge(t[u].rs, v);
else
t[np] = t[v], t[np].ls = Merge(u, t[v].ls);
return Up(np), np;
}
int Get(int val) {
int np = Nw();
return t[np].Init(val), np;
}
void Ins(int &p, int val) {
int a = 0, b = 0;
Split(p, val, a, b);
p = Merge(Merge(a, Get(val)), b);
}
void Del(int &p, int val) {
int a = 0, b = 0, c = 0;
Split(p, val, a, c);
Split(a, val - 1, a, b);
if(b) st[++tp] = b, b = Merge(t[b].ls, t[b].rs);
p = Merge(Merge(a, b), c);
}
void Kth(int p, int k) {
for(;p && t[t[p].ls].sz + 1 != k;)
t[t[p].ls].sz < k ? k -= t[t[p].ls].sz + 1, p = t[p].rs : p = t[p].ls;
printf("%d\n", t[p].val);
}
void Rk(int &p, int val) {
int a = 0, b = 0;
Split(p, val - 1, a, b);
printf("%d\n", t[a].sz + 1);
p = Merge(a, b);
}
void Pre(int &p, int val) {
int a = 0, b = 0;
Split(p, val - 1, a, b);
a ? Kth(a, t[a].sz), 0 : puts("-2147483647");
p = Merge(a, b);
}
void Nxt(int &p, int val) {
int a = 0, b = 0;
Split(p, val, a, b);
b ? Kth(b, 1), 0 : puts("2147483647");
p = Merge(a, b);
}
int main() {
srand(time(NULL));
int n = ri();
for(int i = 1;i <= n; ++i) {
int v = ri(), op = ri(), x = ri();
Rt[i] = Rt[v];
if(op == 1) Ins(Rt[i], x);
else if(op == 2) Del(Rt[i], x);
else if(op == 3) Rk(Rt[i], x);
else if(op == 4) Kth(Rt[i], x);
else if(op == 5) Pre(Rt[i], x);
else Nxt(Rt[i], x);
}
return 0;
}
替罪羊树
暴力是最优美的!
替罪羊树就是基于暴力重构的一种数据结构。
定义一个平衡因子
α
∈
[
0.6
,
0.8
]
\alpha \in [0.6, 0.8]
α∈[0.6,0.8],如果
m
a
x
(
s
z
[
l
]
,
s
z
[
r
]
)
≥
s
z
[
u
]
max (sz[l],sz[r]) \ge sz[u]
max(sz[l],sz[r])≥sz[u],就暴力重构以
u
u
u为根的子树。
这样子的均摊复杂度是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)的
重构
类似 K D − T r e e KD-Tree KD−Tree
void Build(int &p, int L, int R) {
if(L > R) return void(p = 0);
int m = L + R >> 1; p = st[m];
Build(ls, L, m - 1); Build(rs, m + 1, R);
Up(p);
}
void Dfs(int p) {
if(ls) Dfs(ls);
if(!t[p].del) st[++tp] = p;
if(rs) Dfs(rs);
}
void Build(int &p) {tp = 0; Dfs(p); Build(p, 1, tp);}
插入
注意一下,插入一个节点可能会导致从某个点到根路径上一连串的节点不满足平衡,我们只需要修改深度最低的节点即可,也就是 r b rb rb节点的作用。
void Ins(int &p, int val) {
if(!p) return t[p = ++cnt].Init(val);
++t[p].sz; ++t[p].cnt;
val < t[p].val ? Ins(ls, val) : Ins(rs, val);
if(t[p].cnt * A + 20 > std::max(t[ls].cnt, t[rs].cnt)) {
if(rb) Build(ls == rb ? ls : rs), rb = 0;
}
else rb = p;
}
int main() {
...
if(op == 1) Ins(rt, x), rb ? Build(rt), rb = 0 : 0;
...
}
删除
注意是惰性删除,额外开一个
d
e
l
del
del标记当前节点是否被删除。
注意及时这个节点被惰性删除,这棵树计算平衡因子仍然要用真正子树大小,所以要额外开一个
c
n
t
cnt
cnt来记录。
void Era(int p, int k) {
for(int x;t[p].del || t[ls].sz + 1 != k;) {
--t[p].sz;
(x = t[ls].sz + !t[p].del) < k ? k -= x, p = rs : p = ls;
}
--t[p].sz; t[p].del = true;
}
其余操作和其他平衡树类似,不写了咕咕咕此处不再赘述。
总代码
来自luogu3369
什么?你问我平衡因子设多少?当然是三分数据啦
平衡因子一般设
0.75
0.75
0.75
#include<bits/stdc++.h>
#define ls t[p].lc
#define rs t[p].rc
const int N = 2e5 + 10;
const double A = 0.75;
int ri() {
char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
struct Node {
int lc, rc, cnt, sz, val; bool del;
void Init(int _val) {cnt = sz = 1; lc = rc = 0; del = false; val = _val;}
}t[N];
int st[N], tp, cnt, rt, rb;
void Up(int p) {t[p].sz = t[ls].sz + t[rs].sz + !t[p].del;}
void Build(int &p, int L, int R) {
if(L > R) return void(p = 0);
int m = L + R >> 1; p = st[m];
Build(ls, L, m - 1); Build(rs, m + 1, R);
Up(p);
}
void Dfs(int p) {
if(ls) Dfs(ls);
if(!t[p].del) st[++tp] = p;
if(rs) Dfs(rs);
}
void Build(int &p) {tp = 0; Dfs(p); Build(p, 1, tp);}
void Ins(int &p, int val) {
if(!p) return t[p = ++cnt].Init(val);
++t[p].sz; ++t[p].cnt;
val < t[p].val ? Ins(ls, val) : Ins(rs, val);
if(t[p].cnt * A + 20 > std::max(t[ls].cnt, t[rs].cnt)) {
if(rb) Build(ls == rb ? ls : rs), rb = 0;
}
else rb = p;
}
int Rk(int p, int val) {
int r = 1;
for(;p;)
t[p].val < val ? r += t[ls].sz + !t[p].del, p = rs : p = ls;
return r;
}
int Kth(int p, int k) {
for(int x;t[p].del || t[ls].sz + 1 != k;)
(x = t[ls].sz + !t[p].del) < k ? k -= x, p = rs : p = ls;
return t[p].val;
}
void Era(int p, int k) {
for(int x;t[p].del || t[ls].sz + 1 != k;) {
--t[p].sz;
(x = t[ls].sz + !t[p].del) < k ? k -= x, p = rs : p = ls;
}
--t[p].sz; t[p].del = true;
}
int main() {
int n = ri();
for(;n--;) {
int op = ri(), x = ri();
if(op == 1) Ins(rt, x), rb ? Build(rt), rb = 0 : 0;
else if(op == 2) Era(rt, Rk(rt, x));
else if(op == 3) printf("%d\n", Rk(rt, x));
else if(op == 4) printf("%d\n", Kth(rt, x));
else if(op == 5) printf("%d\n", Kth(rt, Rk(rt, x) - 1));
else printf("%d\n", Kth(rt, Rk(rt, x + 1)));
}
return 0;
}
应用
这玩意儿显然不能可持久化对吧。
没有旋转常数又大是吧。
但是人家可以套套套啊!
你说其他的树嘞?
旋转了你想套个啥?
f
h
q
fhq
fhq的
M
e
r
g
e
Merge
Merge不保证复杂度。
只有替罪羊的重构优秀啊。
什么时候要外层平衡树呢?
带插入区间第
k
k
k大啊。
外层用替罪羊树,内层用权值线段树。
这样子暴力重构是
O
(
n
l
o
g
3
)
O(nlog^3)
O(nlog3)的。
不过有一个黑科技是可以采用函数树+线段树合并做到
O
(
n
l
o
g
2
)
O(nlog^2)
O(nlog2)
第
k
k
k大把树抓出来在线段树上二分。
代码常数爆炸。
具体看代码吧!能鸽善鹉的我
替罪羊树套权值线段树
luogu真心过不去啊。
bzoj3065
人傻自带大常数。
#include<bits/stdc++.h>
int ri() {
char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
const int N = 1e7 + 10, M = 1e5 + 10, Mx = 70000;
const double A = 0.75;
int a[M], rt[M], v[M], st[M], tp, st2[M], tp2, n, Rt, rb;
struct Segment {
int ls[N], rs[N], sum[N], st[N], tp, cnt;
int Nw() {return tp ? st[tp--] : ++cnt;}
void Up(int p) {sum[p] = sum[ls[p]] + sum[rs[p]];}
int Merge(int a, int b, int L, int R) {
if(!a || !b) return a | b;
int np = Nw();
if(L == R) return sum[np] = sum[a] + sum[b], np;
int m = L + R >> 1;
ls[np] = Merge(ls[a], ls[b], L, m);
rs[np] = Merge(rs[a], rs[b], m + 1, R);
return Up(np), np;
}
void Del(int &p) {
if(!p) return ;
st[++tp] = p; Del(ls[p]); Del(rs[p]);
sum[p] = ls[p] = rs[p] = 0; p = 0;
}
void Ins(int &p, int L, int R, int x, int f) {
if(!p) p = Nw();
if(L == R) return void(sum[p] += f); int m = L + R >> 1;
if(x <= m) Ins(ls[p], L, m, x, f);
else Ins(rs[p], m + 1, R, x, f);
Up(p); if(!sum[p]) Del(p);
}
}seg;
struct ScapeGoat {
int ls[M], rs[M];
void Build(int &p, int L, int R) {
if(L > R) return void(p = 0);
int m = L + R >> 1; p = a[m];
Build(ls[p], L, m - 1); Build(rs[p], m + 1, R);
for(int i = L;i <= R; ++i) seg.Ins(rt[p], 0, Mx, v[a[i]], 1);
}
void Del(int &p) {
if(!p) return ; seg.Del(rt[p]);
Del(ls[p]); st[++tp] = p; Del(rs[p]);
p = 0;
}
void Build(int &p) {
tp = 0; Del(p);
for(int i = 1;i <= tp; ++i) a[i] = st[i];
Build(p, 1, tp);
}
void Ins(int &p, int k, int val) {
if(!p) {
v[p = ++n] = val;
seg.Ins(rt[p], 0, Mx, val, 1);
return ;
}
seg.Ins(rt[p], 0, Mx, val, 1);
int lsz = seg.sum[rt[ls[p]]];
lsz >= k ? Ins(ls[p], k, val) : Ins(rs[p], k - lsz - 1, val);
if(seg.sum[rt[p]] * A + 20 > std::max(seg.sum[rt[ls[p]]], seg.sum[rt[rs[p]]])) {
if(rb) {
if(ls[p] == rb) Build(ls[p]);
else Build(rs[p]);
rb = 0;
}
}
else rb = p;
}
int Mod(int p, int k, int val) {
seg.Ins(rt[p], 0, Mx, val, 1);
int lsz = seg.sum[rt[ls[p]]], pr;
if(lsz + 1 == k) pr = v[p], v[p] = val;
else pr = (lsz >= k ? Mod(ls[p], k, val) : Mod(rs[p], k - lsz - 1, val));
seg.Ins(rt[p], 0, Mx, pr, -1);
return pr;
}
void Get(int p, int st, int ed) {
int L = seg.sum[rt[ls[p]]], R = seg.sum[rt[p]];
if(st == 1 && ed == R) return void(::st[++tp] = rt[p]);
if(st <= L + 1 && ed >= L + 1) st2[++tp2] = v[p];
if(ed <= L) Get(ls[p], st, ed);
else if(st > L + 1) Get(rs[p], st - L - 1, ed - L - 1);
else {
if(st <= L) Get(ls[p], st, L);
if(ed > L + 1) Get(rs[p], 1, ed - L - 1);
}
}
int Solve(int L, int R, int k) {
tp = tp2 = 0; Get(Rt, L, R); --k;
int l = 0, r = Mx;
for(;l != r;) {
int m = l + r >> 1, lsm = 0;
for(int i = 1;i <= tp; ++i) lsm += seg.sum[seg.ls[st[i]]];
for(int i = 1;i <= tp2; ++i)
if(st2[i] >= l && st2[i] <= m)
++lsm;
if(k < lsm) {
for(int i = 1;i <= tp; ++i) st[i] = seg.ls[st[i]];
r = m;
}
else {
for(int i = 1;i <= tp; ++i) st[i] = seg.rs[st[i]];
l = m + 1; k -= lsm;
}
}
return l;
}
}bst;
int main() {
n = ri(); int La = 0;
for(int i = 1;i <= n; ++i) v[i] = ri(), a[i] = i;
bst.Build(Rt, 1, n);
for(int m = ri();m--;) {
char op = getchar(); for(;op != 'Q' && op != 'M' && op != 'I';) op = getchar();
int u = ri() ^ La, v = ri() ^ La, k;
if(op == 'Q') k = ri() ^ La, printf("%d\n", La = bst.Solve(u, v, k));
else if(op == 'M') bst.Mod(Rt, u, v);
else rb = 0, bst.Ins(Rt, u - 1, v), rb ? bst.Build(Rt), rb = 0 : 0;
}
return 0;
}
总结
不管什么平衡树,重点果然都是深度要期望/均摊
O
(
l
o
g
)
O(log)
O(log)啊。
而
T
r
e
a
p
Treap
Treap的随机其实已经保证了这一点。
替罪羊的均摊基于优美的暴力重构。
两种树又分别有不同的用途。
一种指向可持久化,一种可以套套套。
但是都常数巨大的说。
接下来就是代码的世界了,靠底子抄了啊。
完结送命。