[数据结构]可持久化线段树
#1.0 何为可持久化线段树
也有人叫它 “主席树”,参见 知乎讨论。
至于为什么,别问,问就是枪毙加急许可。
(以上纯属笔者乱搞,下面正文开始)
都知道线段树可以进行单点、区间修改等操作,但是如果我们已经做了 \(114514\) 次操作,突然想要查询在第 \(2333\) 次修改后的信息呢?那我们就需要保存每一次修改的信息了,这也就是 “可持久化” 的意义。
嘛,总之就是这样,似乎没有多么麻烦的样子,下面来看怎么实现。
#2.0 如何实现
为了下面更方便叙述,这里用一个 简单的题目 作为例题。
如题,你需要维护这样的一个长度为 \(N\) 的数组,支持如下几种操作
- 在某个历史版本上修改某一个位置上的值
- 访问某个历史版本上的某一位置的值
此外,每进行一次操作(对于操作 \(2\),即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从 \(1\) 开始编号,版本 \(0\) 表示初始状态数组)
上面这个问题抛去“历史版本”不看,可以线段树单点修改维护,不多说,还是来看如何可持久化。
#2.1 朴素想法
最最最最最朴素的想法就是直接将整个修改过的线段树,复制一遍,存下来(话说笔者最初真没想到这个,直接跳过这一步了...),但是很显然这么做空间、时间都不允许。
#2.2 最终做法
没想到吧,就是这么快...
小编也很惊讶,但事实就是这样。
我们注意到上面的操作存下来了许多没有用的信息,明明有很多点没有被修改,却也被复制了一遍,就好比明明你的学长在机房颓废,但你没有,教练却把所有人都训了一顿一样无理。
那怎么办?很简单,只存修改了的节点不就好了!
显然根节点一定会改变,那么递归时,如果左右儿子之一没改变,就直接连上就好了,只去递归被修改的子节点,对这个子节点复制一份,在这个副本上进行修改,并连接到这个副本。
当然,对于上题中的操作 \(2\),直接将该操作对应的根节点设置为要查询的那个历史版本就好了。
以上就是所有的额外操作了,非常简单对吧!
为了保证空间,所以建议采用 动态开点 的形式。
#2.3 完整代码
好久没写过单点查询单点修改的线段树了...突然有点忘了咋写...
const int N = 4000100;
const int INF = 0x3fffffff;
struct Node {
int l, r;
int ls, rs;
int sum;
};
Node p[N << 2];
int n, a[N], cnt, rt[N], m;
void build(int k, int l, int r) {
p[k].l = l, p[k].r = r;
if (l == r) {p[k].sum = a[l]; return;}
int mid = (l + r) >> 1;
p[k].ls = ++ cnt;
build(p[k].ls, l, mid);
p[k].rs = ++ cnt;
build(p[k].rs, mid + 1, r);
}
void modify(int t, int &k, int x, int c) {
if (!k) k = ++ cnt;
p[k].l = p[t].l, p[k].r = p[t].r;
if (p[t].l == p[t].r) {p[k].sum = c; return;}
int mid = (p[k].l + p[k].r) >> 1;
if (x <= mid) {
p[k].rs = p[t].rs;
modify(p[t].ls, p[k].ls, x, c);
} else {
p[k].ls = p[t].ls;
modify(p[t].rs, p[k].rs, x, c);
}
}
int query(int k, int x) {
if (p[k].l == p[k].r) return p[k].sum;
int mid = (p[k].l + p[k].r) >> 1;
if (x <= mid) return query(p[k].ls, x);
else return query(p[k].rs, x);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++)
scanf("%d", &a[i]);
build(rt[0] = ++ cnt, 1, n);
int v, op, pos, val;
for (int i = 1; i <= m; i ++) {
scanf("%d%d%d", &v, &op, &pos);
if (op == 1) {
scanf("%d", &val);
modify(rt[v], rt[i], pos, val);
} else {
rt[i] = rt[v];
printf("%d\n", query(rt[v], pos));
}
}
return 0;
}
#3.0 主席树
在文章的开头,我们就提到了“主席树”这个称呼,有一点是比较确定的,“主席树”指的是可持久化权值线段树。
#3.1 经典问题
来看一个主席树的经典问题吧:静态区间第 \(k\) 小。
给定 \(n\) 个整数构成的序列 \(a\),将对于指定的闭区间 \([l,r]\) 查询其区间内的第 \(k\) 小值。
这是一个主席树的经典应用,我们考虑将序列中的数一个一个依次插入主席树,那么,我们不难发现,在插入第 \(r\) 个数后的主席树减去插入第 \(l-1\) 个数后的主席树形成的线段树便是只包含了区间 \([l,r]\) 中的数,那么可以用权值线段树查询区间第 \(k\) 小这一基本操作来解决这题。
#3.2 代码实现
首先是单点修改,维护区间和,这个操作与可持久化线段树的操作的基本构架相同。
void modify(int lt, int &k, int l, int r, int x) {
if (!k) k = ++ cnt;
p[k].sum = p[lt].sum + 1;
/*因为是维护区间数的个数
所以可以直接更改,不必 pushup*/
if (l == r) return;
int mid = (l + r) >> 1;
if (x <= mid) {
p[k].rs = p[lt].rs;
modify(p[lt].ls, p[k].ls, l, mid, x);
} else {
p[k].ls = p[lt].ls;
modify(p[lt].rs, p[k].rs, mid + 1, r, x);
}
}
然后是查询操作。这里上文提到的“减去”操作就是减法操作,我们可以得到分离出的线段树的左子树上数的个数 sum_l
,当前寻找第 k
小,如果 sum_l>=k
,那么就意味着,第 k
小在左子树,应当递归左子树,k
不变;否则第 k
小在右子树,应当递归右子树,并令 k=k-sum_l
;重复以上操作,直到递归到叶节点。
int query(int lt, int k, int l, int r, int x) {
if (l == r) return l;
int mid = (l + r) >> 1;
int sum_l = p[p[k].ls].sum - p[p[lt].ls].sum;
if (sum_l >= x) return query(p[lt].ls, p[k].ls, l, mid, x);
else return query(p[lt].rs, p[k].rs, mid + 1, r, x - sum_l);
}
最主要的两个操作也就是这些,因为 \(a\) 中的数的大小可能很大,需要进行离散化处理,这里附上完整程序。
对于每一个询问,我们有以下两种处理方式:
- 将询问离线,按右端点排序,在插入每个数后检查是否有右端点为此数的,如果有,进行查询操作。
- 将所有数一并插入,依次处理每个询问。
这里采用第一种处理方法。
const int N = 1000100;
const int INF = 0x3fffffff;
struct Node {
int ls, rs;
int sum;
};
Node p[N << 2];
struct Ques {
int l, r, k, idx;
bool operator < (const Ques &b) const {
return r < b.r;
}
};
Ques q[N];
int n, m, a[N], cnt, t[N], tot, rt[N], ans[N];
inline void pushup(int k) {
p[k].sum = p[p[k].ls].sum + p[p[k].rs].sum;
}
void build(int &k, int l, int r) {
if (!k) k = ++ cnt;
if (l == r) return;
int mid = (l + r) >> 1;
build(p[k].ls, l, mid);
build(p[k].rs, mid + 1, r);
}
void modify(int lt, int &k, int l, int r, int x) {
if (!k) k = ++ cnt;
p[k].sum = p[lt].sum + 1;
if (l == r) return;
int mid = (l + r) >> 1;
if (x <= mid) {
p[k].rs = p[lt].rs;
modify(p[lt].ls, p[k].ls, l, mid, x);
} else {
p[k].ls = p[lt].ls;
modify(p[lt].rs, p[k].rs, mid + 1, r, x);
}
}
int query(int lt, int k, int l, int r, int x) {
if (l == r) return l;
int mid = (l + r) >> 1;
int sum_l = p[p[k].ls].sum - p[p[lt].ls].sum;
if (sum_l >= x) return query(p[lt].ls, p[k].ls, l, mid, x);
else return query(p[lt].rs, p[k].rs, mid + 1, r, x - sum_l);
}
inline int mpg(int x) { //离散化后进行映射
return lower_bound(t + 1, t + tot + 1, x) - t;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) {
scanf("%d", &a[i]);
t[i] = a[i];
}
for (int i = 1; i <= m; i++) {
scanf("%d%d%d", &q[i].l, &q[i].r, &q[i].k);
q[i].idx = i;
}
/*对询问排序及对 a 进行离散化*/
sort(q + 1, q + m + 1);
sort(t + 1, t + n + 1);
tot = unique(t + 1, t + n + 1) - t - 1;
build(rt[0], 1, tot); //建空树
for (int i = 1, j = 1; i <= n; i ++) {
modify(rt[i - 1], rt[i], 1, tot, mpg(a[i]));
while (q[j].r == i)
ans[q[j].idx] = query(rt[q[j].l - 1], rt[i], 1, tot, q[j].k), j ++;
}
for (int i = 1; i <= m; i ++)
printf("%d\n", t[ans[i]]);
return 0;
}