https://www.luogu.com.cn/problem/P3835
题目背景:
本题为题目普通平衡树的可持久化加强版。
题目描述:
您需要写一种数据结构(可参考题目标题),来维护一个可重整数集合,其中需要提供以下操作(对于各个以往的历史版本):
1、插入
x
x
x
2、删除
x
x
x(若有多个相同的数,应只删除一个,如果没有请忽略该操作)
3、查询
x
x
x的排名(排名定义为比当前数小的数的个数
+
1
+1
+1)
4、查询排名为
x
x
x的数
5、求
x
x
x的前驱(前驱定义为小于
x
x
x,且最大的数,如不存在输出
−
2
31
+
1
-2^{31}+1
−231+1)
6、求
x
x
x的后继(后继定义为大于
x
x
x,且最小的数,如不存在输出
2
31
−
1
2^{31}-1
231−1)
和原本平衡树不同的一点是,每一次的任何操作都是基于某一个历史版本,同时生成一个新的版本。(操作3, 4, 5, 6即保持原版本无变化)。每个版本的编号即为操作的序号(版本
0
0
0即为初始状态,空树)
输入格式:
第一行包含一个正整数
n
n
n ,表示操作的总数。
接下来
n
n
n行,每行包含三个整数,第
i
i
i行记为
v
i
,
opt
i
,
x
i
v_i, \text{opt}_i, x_i
vi,opti,xi。
v
i
v_i
vi表示基于的过去版本号,
opt
i
\text{opt}_i
opti表示操作的序号,
x
i
x_i
xi表示参与操作的数值
输出格式:
每行包含一个整数,依次为各个
3
,
4
,
5
,
6
3,4,5,6
3,4,5,6操作所对应的答案
数据范围:
对于
28
%
28\%
28%的数据,
1
≤
n
≤
10
1 \leq n \leq 10
1≤n≤10;
对于
44
%
44\%
44%的数据,
1
≤
n
≤
2
×
10
2
1 \leq n \leq 2\times {10}^2
1≤n≤2×102;
对于
60
%
60\%
60%的数据,
1
≤
n
≤
3
×
10
3
1 \leq n \leq 3\times {10}^3
1≤n≤3×103;
对于
84
%
84\%
84%的数据,
1
≤
n
≤
10
5
1 \leq n \leq {10}^5
1≤n≤105;
对于
92
%
92\%
92%的数据,
1
≤
n
≤
2
×
10
5
1 \leq n \leq 2\times {10}^5
1≤n≤2×105;
对于
100
%
100\%
100%的数据,
1
≤
n
≤
5
×
1
0
5
1 \leq n \leq 5 \times 10^5
1≤n≤5×105 ,
∣
x
i
∣
≤
10
9
|x_i| \leq {10}^9
∣xi∣≤109,
0
≤
v
i
<
i
0 \le v_i < i
0≤vi<i,
1
≤
opt
≤
6
1\le \text{opt} \le 6
1≤opt≤6。
需要用FHQ Treap。可持久化平衡树,一般要写成无旋Treap的形式,即FHQ Treap。FHQ Treap参考https://blog.csdn.net/qq_46105170/article/details/118997891。不需要持久化的话,效果如下:
考虑需要持久化的情形。由于需要持久化,我们需要维持当前版本的结构不变,以方便以后来查询。由于FHQ Treap是基于分裂合并来进行所有操作的,所以我们要考虑分裂和合并如何做持久化。分裂的时候,从树根向下走的那条进行分裂的路径,需要另外开新的点,剩余的点用当前版本的点。如下图:
同样是分裂为小于等于
3
3
3和大于
3
3
3的两棵子树,分裂的路径是
6
→
2
→
5
→
3
→
4
6\to 2\to 5\to 3\to 4
6→2→5→3→4,这个路径上小于等于
3
3
3的点和大于
3
3
3的点都copy一份,这样尽量利用了没有改变的部分,从而节省了维护版本需要的空间。而合并的时候,由于合并之前一定是进行了分裂,所以合并的时候合并的“拼接处”和分裂的时候那条链是一样的,所以并不需要新开节点。具体思路与不持久化的情形类似。代码如下:
#include
#include
using namespace std;
const int N = 5e5 + 10;
int n, root[N], idx;
int x, y, z;
struct Node {
int l, r;
int key, val;
int sz;
} tr[N * 50];
int newnode(int v) {
int p = ++idx;
tr[p].key = v;
tr[p].val = rand();
tr[p].sz = 1;
return p;
}
#define pushup(u) tr[u].sz = tr[tr[u].l].sz + tr[tr[u].r].sz + 1
void split(int p, int v, int &x, int &y) {
if (!p) {
x = y = 0;
return;
}
if (tr[p].key <= v) {
x = ++idx;
tr[x] = tr[p];
split(tr[x].r, v, tr[x].r, y);
pushup(x);
} else {
y = ++idx;
tr[y] = tr[p];
split(tr[y].l, v, x, tr[y].l);
pushup(y);
}
}
int merge(int x, int y) {
if (!x || !y) return x ^ y;
if (tr[x].val < tr[y].val) {
tr[x].r = merge(tr[x].r, y);
pushup(x);
return x;
} else {
tr[y].l = merge(x, tr[y].l);
pushup(y);
return y;
}
}
void insert(int &root, int v) {
split(root, v, x, y);
z = newnode(v);
root = merge(merge(x, z), y);
}
void remove(int &root, int v) {
split(root, v, x, z);
split(x, v - 1, x, y);
y = merge(tr[y].l, tr[y].r);
root = merge(merge(x, y), z);
}
int get_rank(int &root, int v) {
split(root, v - 1, x, y);
int res = tr[x].sz + 1;
root = merge(x, y);
return res;
}
int get_key(int root, int rk) {
if (rk <= tr[tr[root].l].sz) return get_key(tr[root].l, rk);
else if (rk > tr[tr[root].l].sz + 1)
return get_key(tr[root].r, rk - tr[tr[root].l].sz - 1);
else return tr[root].key;
}
int get_prev(int &root, int v) {
split(root, v - 1, x, y);
if (!x) return INT_MIN;
int res = get_key(x, tr[x].sz);
root = merge(x, y);
return res;
}
int get_next(int &root, int v) {
split(root, v, x, y);
if (!y) return INT_MAX;
int res = get_key(y, 1);
root = merge(x, y);
return res;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int vi, op, x;
scanf("%d%d%d", &vi, &op, &x);
root[i] = root[vi];
if (op == 1) insert(root[i], x);
else if (op == 2) remove(root[i], x);
else if (op == 3) printf("%d\n", get_rank(root[i], x));
else if (op == 4) printf("%d\n", get_key(root[i], x));
else if (op == 5) printf("%d\n", get_prev(root[i], x));
else printf("%d\n", get_next(root[i], x));
}
}
每次操作时间复杂度 O ( log n ) O(\log n) O(logn),空间 O ( n log n ) O(n\log n) O(nlogn)。