平衡树维护序列信息小记
感觉还是挺有意思的。
接下来的讨论均基于 \(\text{FHQ Treap}\)。别的平衡树是怎么样的呢,我不到啊。
让我们先来观察 \(\text{FHQ}\) 满足的性质!
\(\text{FHQ}\) 的 \(\text{merge(x, y)}\) 操作,会保证在操作结束后,中序遍历后 \(x\) 的节点都在 \(y\) 的节点之前。
然后众所周知 \(\text{FHQ}\) 的 split 操作既可以按照权值分裂又可以按照大小分裂,而按大小分裂出来的就是中序遍历的前 \(k\) 个。所以我们维护 \(\text{FHQ}\) 的中序遍历为原序列的顺序即可。
本质在于保留键值的堆结构以保证复杂度,而舍弃权值的二叉搜索树结构(转换为下标形成二叉搜索树)。
P3391 【模板】文艺平衡树
考虑引入懒标记的思想,对每个节点打一个 \(\text{tag}\) 表示是否翻转这个节点所维护的区间,注意递归调用子树信息时,要先 push_down 一下。
例:
正确的 split 写法:
点击查看代码
void split(int p, int k, int &x, int &y) {
push_down(p);
if (!p) {
x = y = 0;
return;
} else if (a[a[p].l].sz + 1 <= k) {
x = p; split(a[p].r, k - a[a[p].l].sz - 1, a[p].r, y);
} else {
y = p; split(a[p].l, k, x, a[p].l);
}
push_up(p);
}
错误的 split 写法:
点击查看代码
void split(int p, int k, int &x, int &y) {
if (!p) {
x = y = 0;
return;
} else if (a[a[p].l].sz + 1 <= k) {
push_down(p);
x = p; split(a[p].r, k - a[a[p].l].sz - 1, a[p].r, y);
} else {
push_down(p);
y = p; split(a[p].l, k, x, a[p].l);
}
push_up(p);
}
这份代码错误的原因是判断了子树大小再 push_down,所以程序判断的子树信息是错误的,因此会走进错误的子树。
有意思的是,因为平衡树键值随机的性质,每个点所在的子树大小是随机的,因此错误的子树信息也是随机的,所以这份代码会随机说话(
最后再考虑一个问题:交换两个子树会不会破坏平衡树本身的性质?
答案是不会的:首先中序遍历和每个点的权值是我们维护的东西,肯定不会出问题。而对于键值,一个很魔怔的事情是 \(\text{FHQ}\) 的键值满足的是堆性质,而堆只限制了父子节点的键值大小关系,却没有限制左右儿子的键值大小关系,所以不管怎么交换堆的性质都还是满足的。
完整代码如下(真要丑死了):
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define mid (l + r >> 1)
#define lowbit(x) (x & -x)
using namespace std;
constexpr int N = 1e5 + 5, INF = 1e9;
mt19937 rnd(time(0));
struct FHQ_Treap {
int cnt, rt;
struct Node {
bool tag;
int l, r, sz, val;
unsigned int key;
} a[N];
void push_up(int p) {
a[p].sz = a[a[p].l].sz + a[a[p].r].sz + 1;
}
void push_down(int p) {
if (!a[p].tag) return;
swap(a[p].l, a[p].r);
a[a[p].l].tag ^= 1;
a[a[p].r].tag ^= 1;
a[p].tag = 0;
}
int new_node(int x) {
a[++cnt].val = x;
a[cnt].tag = false;
a[cnt].l = a[cnt].r = 0;
a[cnt].sz = 1; a[cnt].key = rnd();
return cnt;
}
void split(int p, int k, int &x, int &y) {
push_down(p);
if (!p) {
x = y = 0;
return;
} else if (a[a[p].l].sz + 1 <= k) {
x = p; split(a[p].r, k - a[a[p].l].sz - 1, a[p].r, y);
} else {
y = p; split(a[p].l, k, x, a[p].l);
}
push_up(p);
}
int merge(int x, int y) {
if (!x || !y) {
return x | y;
} else if (a[x].key > a[y].key) {
push_down(x);
a[x].r = merge(a[x].r, y); push_up(x);
return x;
} else {
push_down(y);
a[y].l = merge(x, a[y].l); push_up(y);
return y;
}
}
void init(int num[], int n) {
cnt = rt = 0;
int x = 0, y = 0;
for (int i = 1; i <= n; ++i) {
rt = merge(rt, new_node(num[i]));
}
}
void rev(int l, int r) {
int x = 0, y = 0, z = 0;
split(rt, l - 1, x, y);
split(y, r - l + 1, y, z);
a[y].tag ^= 1;
rt = merge(merge(x, y), z);
}
vector<int> get_ans() {
vector<int> v;
function <void(int)> dfs = [&](int p) {
if (!p) return;
push_down(p);
dfs(a[p].l);
v.push_back(a[p].val);
dfs(a[p].r);
};
dfs(rt);
return v;
}
} FHQ;
int n, m;
int a[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
a[i] = i;
}
FHQ.init(a, n);
while (m--) {
int l, r;
cin >> l >> r;
FHQ.rev(l, r);
}
vector<int> ans = FHQ.get_ans();
assert(ans.size() == n);
for (int i = 0; i < ans.size(); ++i) {
cout << ans[i] << " \n"[i == n];
}
return 0;
}
平衡树维护区间平移
乱做一下就行了。先把要平移的区间列出来,然后把要覆盖的位置都删掉,如果要把另一些位置补上的话再把这些位置补上就行了。
似乎每次只能平移常数个下标。
并且好像最好要垃圾回收一下(但我不会阿)
放几道题:
- CF2121H 其实拿平衡树做这道题好像很愚蠢,具体地思路是:考虑维护一个长度为 \(n\) 的数组 \(x\),\(x_i\) 表示长度为 \(i\) 的 \(\text{LIS}\) 的最后一个元素的最小值。这个数组非空的部分显然是连续的,并且元素值必定单调不降,于是直接拿平衡树维护 \(x\) 就行了。
P3372 【模板】线段树 1
和「文艺平衡树」一样打懒标记即可,在哪里打标记等细节都一样。
U31865 文艺平衡树(变态版)
基本上是所有平衡树上维护懒标记的问题的总集,其实思路还是挺典的。

浙公网安备 33010602011771号