平衡树维护序列信息小记

感觉还是挺有意思的。


接下来的讨论均基于 \(\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 文艺平衡树(变态版)

基本上是所有平衡树上维护懒标记的问题的总集,其实思路还是挺典的。

posted @ 2025-06-22 19:18  zyb_txdy  阅读(49)  评论(0)    收藏  举报