题解:P5217 贫穷

一、题前话

题目传送门

借此写一写对 FHQ-Treap 的感悟

二、FHQ-Treap

Treap

什么是 Treap?Treap 是一个二叉搜索树,基于随机化。其满足 BST 和 Heap 两个的性质。即对于每一个节点都有一个 \(\text{key}\)(数值,用来满足 BST)和 \(\text{val}\)(优先级,用来满足 Heap)。对于任意一个节点,都有 \(\text{key}_{lson} < \text{key}_u < \text{key}_{rson}\), \(\text{val}_u > \text{val}_{lson}\), \(\text{val}_u > \text{val}_{rson}\)

首先根据 Treap 的定义则一个集合(假设所有元素的 \(\text{key}\)\(\text{val}\) 已知)建成的 Treap 应该是唯一的。我们可以这样去想:我们先把 \(\text{val}\) 最大的 \(\text u\) 拿出来做根节点,然后把 \(\text{key}\) < \(\text{key}_u\) 的集合拿出来做左子树,剩余的做右子树。递归下去树的结构就是确定的。

那么如果这样我们就可以证明树高的期望是 \(O(\log n)\) 的,很好的完成了保持树平衡的任务,使得 BST 平均时间复杂度降至 \(O(\log n)\)

FHQ-Treap

普通 Treap 是基于旋转的,那么为什么要有 FHQ-Treap 呢?因为他简单好写好理解,能解决很多区间问题且可以可持久化。

什么是 FHQ-Treap 呢?首先他只基于 \(\text {merge}\)\(\text {split}\) 这两个操作,可以参考一下这里。笔者主要写一下对区间操作的认识。

首先我们先放上 P3369 的代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define LL __int128

// problem : P3369
// coded by talent_wei
// time : 2025 / 3 / 18
// algorithm : FHQ - Treap

const int N = 1e5 + 5;
int n, val[N], key[N], size[N], num[N];
int ls[N], rs[N], root, cnt;

mt19937 g(time(0));
uniform_int_distribution < int > myrand(0, 1000000000);

int newnode(int v) {
	key[++ cnt] = v;
	val[cnt] = myrand(g);
	size[cnt] = num[cnt] = 1;
	return cnt;
}

void pushup(int o) {
	size[o] = size[ls[o]] + size[rs[o]] + num[o];
}

void split(int o, int &x, int &y, int v) {
	if(!o) {
		x = y = 0;
		return;
	}
	if(key[o] <= v) {
		x = o;
		split(rs[o], rs[o], y, v);
	} else {
		y = o;
		split(ls[o], x, ls[o], v);
	}
	pushup(x), pushup(y);
}

void merge(int &o, int x, int y) {
	if(!x || !y) {
		o = x + y;
		return;
	}
	if(val[x] >= val[y]) {
		o = x;
		merge(rs[o], rs[x], y);
	} else {
		o = y;
		merge(ls[o], x, ls[y]);
	}
	pushup(o);
}

void insert(int v) {
	int l = 0, mid = 0, r = 0;
	split(root, l, r, v);
	split(l, l, mid, v - 1);
	if(mid) num[mid] ++, size[mid] ++;
	else mid = newnode(v);
	merge(l, l, mid);
	merge(root, l, r);
}

void remove(int v) {
	int l = 0, mid = 0, r = 0;
	split(root, l, r, v);
	split(l, l, mid, v - 1);
	num[mid] --, size[mid] --;
	if(num[mid]) merge(l, l, mid);
	merge(root, l, r);
}

int getrank(int v) {
	int l = 0, r = 0, ans = 0;
	split(root, l, r, v - 1);
	ans = size[l] + 1;
	merge(root, l, r);
	return ans;
}

int getkth(int k) {
	int o = root;
	while(true) {
		if(k <= size[ls[o]]) {
			o = ls[o];
			continue;
		}
		k -= size[ls[o]] + num[o];
		if(k <= 0) return key[o];
		o = rs[o];
	}
}

int getprev(int v) {
	int l = 0, r = 0;
	split(root, l, r, v - 1);
	int o = l;
	while(rs[o]) o = rs[o];
	merge(root, l, r);
	return key[o];
}

int getnext(int v) {
	int l = 0, r = 0;
	split(root, l, r, v);
	int o = r;
	while(ls[o]) o = ls[o];
	merge(root, l, r);
	return key[o];
}

int main() {
	ios::sync_with_stdio(false);
	cin >> n;
	while(n --) {
		int op, x;
		cin >> op >> x;
		if(op == 1) insert(x);
		if(op == 2) remove(x);
		if(op == 3) cout << getrank(x) << "\n";
		if(op == 4) cout << getkth(x) << "\n";
		if(op == 5) cout << getprev(x) << "\n";
		if(op == 6) cout << getnext(x) << "\n";
	}
	return 0;
}

这是基于按值分裂的。我们可以瞅一下按排名分裂的(因为一般序列操作都是在每一位置进行操作)

void split(int o, int &x, int &y, int k) {
	if(!o) {
		x = y = 0;
		return;
	}
	if(size[l(o)] + 1 <= k) {
		x = o;
		split(r(o), r(x), y, k - size[l(o)] - 1);
	} else {
		y = o;
		split(l(o), x, l(y), k);
	}
	pushup(x), pushup(y);
}

先放一道题:
我做的第一道题

题目要维护一个 stack,其实把他搞平了就是维护一个数组。

先看第一个要求:置顶。

置顶其实就是放到第一个位置。

那我们很明显可以先按排名分裂,找到这本书。然后把这本书的 \(\text{key}\) 值调到最小,然后 \(\text {merge}\)。这时的 \(\text{key}\) 可以理解成当前位置的相对大小。

但是我们发现一个很好玩的事情:我们在整个代码的其他部分(包括 \(\text {merge}\))都不会用到这个 \(\text{key}\),因为 \(\text {merge}\) 是有序的。

那我们就很好了,直接把不要 \(\text {key}\) 了,把他 \(\text {split}\) 出来后直接 \(\text {merge}\) 就行了。

其他操作同理。

最后注意一下要把 \(0\) 去除,因为如果我们 \(\text{pushup(0)}\)\(0\) 节点的内容就会有改变,但空节点也是 \(0\),容易出现错误。

代码:

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define LL __int128

// problem : P2596 
// coded by talent_wei
// time : 2025 / 3 / 19
// algorithm : FHQ - Treap

const int N = 1e5 + 5;
int n, m, a[N], val[N], id[N], cnt;
int root, fa[N], size[N], ls[N], rs[N];

mt19937 g(time(0));
uniform_int_distribution < int > myrand(0, 1000000000);

int newnode(int v) {
	val[++ cnt] = myrand(g);
	size[cnt] = 1;
	return cnt;
}

void pushup(int o) {
	if(!o) return;
	size[o] = size[ls[o]] + size[rs[o]] + 1;
	if(ls[o]) fa[ls[o]] = o;
	if(rs[o]) fa[rs[o]] = o;
}

void split(int o, int &x, int &y, int k) {
	if(!o) {
		x = y = 0;
		return;
	}
	if(size[ls[o]] + 1 <= k) {
		x = o;
		split(rs[o], rs[x], y, k - size[ls[o]] - 1);
	} else {
		y = o;
		split(ls[o], x, ls[y], k);
	}
	pushup(x), pushup(y);
}

void merge(int &o, int x, int y) {
	if(!x || !y) {
		o = x + y;
		return;
	}
	if(val[x] >= val[y]) {
		o = x;
		merge(rs[o], rs[x], y);
	} else {
		o = y;
		merge(ls[o], x, ls[y]);
	}
	pushup(o);
}
 
int getrank(int o) {
	int ans = size[ls[o]] + 1;
	while(o != root) {
		if(rs[fa[o]] == o) ans += size[ls[fa[o]]] + 1;
		o = fa[o];
	}
	return ans;
}

void debug(int o) {
	printf("size[%d] = %d, fa[%d] = %d\n\n", a[o], size[o], a[o], a[fa[o]]);
	if(ls[o]) printf("%d --l--> %d\n\n", a[o], a[ls[o]]), debug(ls[o]);
	if(rs[o]) printf("%d --r--> %d\n\n", a[o], a[rs[o]]), debug(rs[o]);
}

int main() {
	cin >> n >> m;
	for(int i = 1; i <= n; i++) {
		cin >> a[i];
		id[a[i]] = newnode(a[i]);
		merge(root, root, id[a[i]]);
	}
	while(m --) {
		char op[10]; 
		int x, l = 0, r = 0, mid = 0;
		scanf("%s%d", op + 1, &x);
		if(op[1] == 'T') {
			int k = getrank(id[x]);
			split(root, l, r, k);
			split(l, l, mid, k - 1);
			merge(l, mid, l);
			merge(root, l, r);
		} else if(op[1] == 'B') {
			int k = getrank(id[x]);
			split(root, l, r, k);
			split(l, l, mid, k - 1);
			merge(r, r, mid);
			merge(root, l, r);
		} else if(op[1] == 'I') {
			int t; scanf("%d", &t); 
			if(!t) continue;
			x = getrank(id[x]);
			int bmid = 0;
			if(t > 0) {
				split(root, l, r, x + 1);
				split(l, l, mid, x);
				split(l, l, bmid, x - 1);
				merge(l, l, mid);
				merge(l, l, bmid);
				merge(root, l, r);
			} else {
				split(root, l, r, x);
				split(l, l, mid, x - 1);
				split(l, l, bmid, x - 2);
				merge(l, l, mid);
				merge(l, l, bmid);
				merge(root, l, r);
			}
		} else if(op[1] == 'Q') {
			split(root, l, r, x);
			split(l, l, mid, x - 1);
			printf("%d\n", a[mid]);
			merge(l, l, mid);
			merge(root, l, r);
		} else printf("%d\n", getrank(id[x]) - 1);
	}
	return 0;
}

看一下懒标记:

首先可以发现这个东西和线段树有点像,都是从下面合并,但不同的是这个还要加上此节点自己的影响。

但懒标记是一样的啊,可以就把他理解成线段树的懒标记,但是 \(\text{pushdown}\) 是在 \(\text{key}\)\(\text {split}\) 里面做。

所以就非常的简单。

对于此题:

我们对于每一个节点都维护一个 \(\text{rev}\),代表是不是要反转。然后维护一个 \(\text{s[26]}\),代表在这个区间内某字母是否出现过。

code:

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define LL __int128

// problem : P5217 
// coded by talent_wei
// time : 2025 / 3 / 21
// algorithm : FHQ - Treap
// 1.split注意是 split(l, l, mid, x - 1) 而不是 split(root, l, mid, x - 1)
// 2.所有pushup / pushdown 把 0 都跳过 
// 3.调试时可以用极为简单的样例 

const int N = 2e5 + 5;
int n, m, root, cnt, val[N], size[N], ls[N], rs[N];
int rev[N], s[N][26], sum[N], del[N], fa[N];
char ch[N];

mt19937 g(time(0));
uniform_int_distribution < int > myrand(0, 1000000000);

int newnode(char v) {
	val[++ cnt] = myrand(g);
	size[cnt] = sum[cnt] = 1;
	s[cnt][v - 'a'] = 1;
	ch[cnt] = v;
	return cnt;
} 

void adds(int x, int y) {
	sum[x] = 0;
	for(int i = 0; i < 26; i++) {
		s[x][i] = s[x][i] || s[y][i];
		sum[x] += s[x][i];
	}
}

#define l(o) ls[o]
#define r(o) rs[o]

void pushup(int o) {
	if(!o) return;
	size[o] = 1; sum[o] = 1;
	memset(s[o], 0, sizeof s[o]);
	s[o][ch[o] - 'a'] = 1;
	if(l(o)) fa[l(o)] = o, size[o] += size[l(o)], adds(o, l(o));
	if(r(o)) fa[r(o)] = o, size[o] += size[r(o)], adds(o, r(o));
}

void pushdown(int o) {
	if(!rev[o]) return;
	swap(l(o), r(o));
	if(l(o)) rev[l(o)] ^= 1;
	if(r(o)) rev[r(o)] ^= 1;
	rev[o] = 0;
}

void split(int o, int &x, int &y, int k) {
	if(!o) {
		x = y = 0;
		return;
	}
	pushdown(o);
	if(size[l(o)] + 1 <= k) {
		x = o;
		split(r(o), r(x), y, k - size[l(o)] - 1);
	} else {
		y = o;
		split(l(o), x, l(y), k);
	}
	pushup(x), pushup(y);
}

void merge(int &o, int x, int y) {
	if(!x || !y) {
		o = x + y;
		return;
	}
	pushdown(x), pushdown(y);
	if(val[x] >= val[y]) {
		o = x;
		merge(r(o), r(x], y);
	} else {
		o = y;
		merge(l(o), x, l(y));
	}
	pushup(o);
}

void cleartag(int o) {
	if(o != root) cleartag(fa[o]);
	pushdown(o);
}

int getrank(int o) {
	cleartag(o);
	int res = size[l(o)] + 1;
	while(o != root) {
		if(r(fa[o]) == o) res += size[l(fa[o])] + 1;
		o = fa[o];
	}
	return res;
}

int main() {
	ios::sync_with_stdio(false);
	cin >> n >> m >> ch + 1;
	for(int i = 1; i <= n; i++) 
		merge(root, root, newnode(ch[i]));
	while(m --) {
		char op, c; 
		int x, y, l = 0, r = 0, mid = 0;
		cin >> op >> x;
		if(op == 'I') {
			cin >> c;
			split(root, l, r, x);
			merge(l, l, newnode(c));
			merge(root, l, r);
		} else if(op == 'D') {
			split(root, l, r, x);
			split(l, l, mid, x - 1);
			if(mid <= n) del[mid] = 1;
			merge(root, l, r);
		} else if(op == 'R') {
			cin >> y;
			split(root, l, r, y);
			split(l, l, mid, x - 1);
			rev[mid] ^= 1;
			merge(l, l, mid);
			merge(root, l, r);
		} else if(op == 'P') {
			if(del[x]) cout << 0 << "\n";
			else cout << getrank(x) << "\n";
		} else if(op == 'T') {
			split(root, l, r, x);
			split(l, l, mid, x - 1);
			cout << ch[mid] << "\n";
			merge(l, l, mid);
			merge(root, l, r);
		} else if(op == 'Q') {
			cin >> y;
			split(root, l, r, y);
			split(l, l, mid, x - 1);
			cout << sum[mid] << "\n";
			merge(l, l, mid);
			merge(root, l, r);
		}
	}
	return 0;
}

END

posted @ 2025-07-14 16:09  talent_wei  阅读(13)  评论(0)    收藏  举报