前言
可持久化线段树是动态开点的线段树,又被称为主席树。显然,听名字就知道,要想学习主席树,首先得学会线段树,如果还不会请戳这里,或者自己找一篇博客学习。
作为一种可持久化数据结构,主席树支持我们查询历史版本。具体的讲,就是如果你在原来的线段树上进行了一系列操作后,还要保存原有的以及每次操作后的线段树,这样才能查询历史版本。
正片
静态主席树
首先从我们的静态主席树开始入门。
下面以一个题为例,给你整明白主席树。
问题(点我进入)
给你
N
N
N个数
a
i
a_i
ai,
M
M
M次操作,现在有两种操作,①在某个历史版本上修改某一个位置上的值,②是查询某个历史版本上某个位置的值,其中
N
,
M
≤
1
0
6
N,M\leq10^6
N,M≤106.
此外,每进行一次操作(对于操作②,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)。
分析
线段树我们都会,如果要想维护 N N N个数的序列,那么他需要的空间为 4 N 4N 4N,如果我们还要可持久化,每次操作后都存一下,对于 M M M次操作,那么空间将会达到 4 M N 4MN 4MN,那么当 M M M很大的时候会非常耗内存,在算法竞赛中,这么大的空间一般是开不下的。那么有没有什么解决的办法呢?
当然是有的。首先我们得整明白,每一次操作之后,对于上一个状态的线段树而言,有多少个节点的状态发生了改变。这里由于我们是单点修改,所以其实只有一条链(从根节点开始一直到要修改的叶节点)
l
o
g
2
N
log_2N
log2N个点发生了改变,而剩下的
4
N
−
l
o
g
2
N
4N-log_2N
4N−log2N个点都没有改变,所以每次操作后,只需要记录改变的节点,其余没变的节点只需要原封不动继承就行。
下面一张图,是将长度为7的线段建成一棵线段树,并且改变
a
2
a_2
a2的值的图。
这里的 r t [ i ] rt[i] rt[i]表示第 i i i次操作后线段树的根节点,其中 r t [ 0 ] rt[0] rt[0]是初始状态。
空间复杂度分析
初始状态
N
N
N个节点的线段树,需要的空间为
4
N
4N
4N,之后我们每次操作都会新增
l
o
g
2
N
log_2N
log2N个节点,
因此总体空间复杂度为
O
(
4
N
+
M
l
o
g
2
N
)
O(4N+Mlog_2N)
O(4N+Mlog2N),此处
N
,
M
N,M
N,M都取一样的话就是
O
(
(
4
+
l
o
g
2
N
)
N
)
O((4+log_2N)N)
O((4+log2N)N),如果N有
1
0
6
10^6
106那开30倍的
N
N
N就够了。当然,如果你不放心可以再多开大些。
好了,讲了这么多,相信你已经理解主席树的思想——重用节点。那么要如何实现呢?因为他看起来好复杂。
实现
首先要解决记录问题:
(1)左/右子树节点
这里我们采用动态开点的方式,不能像原来的线段树(左/右子树节点为
2
u
,
2
u
+
1
2u,2u+1
2u,2u+1)那样直接确定左子树右子树节点。
(2)当前是哪个区间
这里我们可以在函数的里面记录
(3)叶节点的值
一个数组记就行
const int MAX = 1e6 + 10;//N, M
const int MAX_N = MAX * 30;//(logN + 4)倍, 可以再开大点
int rt[MAX], tot;//rt[i]为第i次操作后线段树的根节点, tot -> 动态开点
int lc[MAX_N], rc[MAX_N], val[MAX_N];//分别为左儿子, 右儿子,
接下来看操作:
在这里只需要三个操作:
①建树
②单点修改
③区间查询
建树
和线段树类似,只是方式变为动态开点。
void build(int &u, int l, int r) {//这里用引用的方式
u = ++tot;//给当前节点一个编号
if (l == r) val[u] = a[l];//到达叶节点
else {
int mid = (l + r) / 2;
build(lc[u], l, mid);//建左子树
build(rc[u], mid + 1, r);//建右子树
}
}
那么我们建树就只需要这样:
build(rt[0], 1, N);
单点修改
void update(int &u, int v, int l, int r, int p, int k) {//u为当前版本, v为过去的某个版本, p为修改的点, k为变成的值
u = ++tot;//同理
lc[u] = lc[v], rc[u] = rc[v];//首先, 当前版本要先继承过去版本的左右子树
//然后在这个基础之上, 修改其中的一条链(就是找一个子树修改)
if (l == r) val[u] = k;//到达叶节点修改即可
else {
int mid = (l + r) / 2;
if (p <= mid) update(lc[u], lc[v], l, mid, p, k);//这样就是修改左子树,
else update(rc[u], rc[v], mid + 1, r, p, k);//修改右子树
}
}
单点查询
int query(int u, int l, int r, int p) {//这个比较简单, 就不解释了
if (l == r) return val[u];
else {
int mid = (l + r) / 2;
if (p <= mid) return query(lc[u], l, mid, p);
else return query(rc[u], mid + 1, r, p);
}
}
完整代码
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e6 + 10;
const int MAX_N = MAX * 30;
int N, M;
int a[MAX];
int rt[MAX], tot;
int lc[MAX_N], rc[MAX_N], val[MAX_N];
void build(int &u, int l, int r) {
u = ++tot;
if (l == r) val[u] = a[l];
else {
int mid = (l + r) / 2;
build(lc[u], l, mid);
build(rc[u], mid + 1, r);
}
}
void update(int &u, int v, int l, int r, int p, int k) {
u = ++tot;
lc[u] = lc[v], rc[u] = rc[v];
if (l == r) val[u] = k;
else {
int mid = (l + r) / 2;
if (p <= mid) update(lc[u], lc[v], l, mid, p, k);
else update(rc[u], rc[v], mid + 1, r, p, k);
}
}
int query(int u, int l, int r, int p) {
if (l == r) return val[u];
else {
int mid = (l + r) / 2;
if (p <= mid) return query(lc[u], l, mid, p);
else return query(rc[u], mid + 1, r, p);
}
}
int main() {
scanf("%d%d", &N, &M);
for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
build(rt[0], 1, N);
for (int i = 1; i <= M; i++) {
int v, op, p; scanf("%d%d%d", &v, &op, &p);
if (op == 1) {
int k; scanf("%d", &k);
update(rt[i], rt[v], 1, N, p, k);
}
else {
printf("%d\n", query(rt[v], 1, N, p));
rt[i] = rt[v];//题目说了查询后,生成一个一模一样的版本,那我们就将根节点复制就可以
}
}
return 0;
}