题解: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;
}

浙公网安备 33010602011771号