可持久化线段树
可持久化线段树
前置知识
线段树,线段树上动态开点(不会可以参考我之前的博客或者oiwiki)
可持久化线段树(可持久化数组)——单点修改
[例题:洛谷——P3919 【模板】可持久化线段树 1(可持久化数组)](P3919 【模板】可持久化线段树 1(可持久化数组) - 洛谷)
题目大意:
对一个数组进行单点操作,每次查询与修改会产生一个历史版本,对某个指定的历史版本进行操作
解题思路:
朴素算法就是每次操作时新开一个数组存储新版本,时间复杂度与空间复杂度都为$ O(N ^ 2)$,显然,这是我们无法接受的,那么我们就要利用可持久化线段树来维护操作
算法讲解:
可持久化线段树,是基于线段树的一种算法,它通过对每一个操作进行增加一条链来维护操作
具体实现是对于每一个操作i,在线段树上新开一条链以root[i]为根节点。比若说我们要对一个数组a=[1,2,3,4]的第4项进行修改,就可以新开一个根节点,然后新建出一条链与原线段树相合并
抽象图示:

这个样子,只要再查询时传入对应版本号就可以直接查询
代码示例:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6+10;
const int M = 3e7+10;
ll n, cnt, m;
ll a[N],root[N];
struct Tree {
ll num, l, r;
} tree[M];
struct SegmentTree {
ll build(ll l, ll r) {
ll p = ++cnt;
if (l == r) {
tree[p].num = a[l];
return p;
}
ll mid = (l + r) >> 1;
tree[p].l = build(l, mid);
tree[p].r = build(mid + 1, r);
return p;
}
ll add(ll p, ll l, ll r, ll x, ll k) {
ll newp = ++cnt;
tree[newp] = tree[p];
if (l == r) {
tree[newp].num = k;
return newp;
}
ll mid = (l + r) >> 1;
if (x <= mid)tree[newp].l = add(tree[p].l, l, mid, x, k);
else tree[newp].r = add(tree[p].r, mid + 1, r, x, k);
return newp;
}
ll search(ll p, ll l, ll r, ll x) {
if (l == r) {
return tree[p].num;
}
ll mid = (l + r) >> 1;
if (x <= mid)return search(tree[p].l, l, mid, x);
else return search(tree[p].r, mid + 1, r, x);
}
}st;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
root[0]=st.build(1,n);
for(int i=1;i<=m;i++){
ll p,op,x,y;
cin>>p>>op>>x;
if(op==1){
cin>>y;
root[i]=st.add(root[p],1,n,x,y);
}
if(op==2){
cout<<st.search(root[p],1,n,x)<<'\n';
root[i]=root[p];
}
}
return 0;
}
一些常见的问题:
1、插入的只是一条链,是如何支持查询其它数据的
新插入的链是基于原线段树的,并不是但开一个线段树,在查询其它数据时会回到原线段树中
2、空间复杂度是多少,会不会很容易炸
主席树很吃空间,如果数据不大,推荐开 n << 5
主席树
[例题:P3834 【模板】可持久化线段树 2](P3834 【模板】可持久化线段树 2 - 洛谷)
题目大意:
对一个序列进行单点修改,区间查询第k小
解题思路:
很显然,这道题轻易就可以想出一种暴力做法\(O(N*N \log_2 N)\) 或者\(O(N^2)\) ,但很明显,不能接受,不过这是显然不可接受的,此时,我们就可以借助上一道题的思路,直接手搓一个主席树。
当然,如果你不会区间查询第k小也可以继续看(qwq)
算法讲解:
实际上上一道题我们利用的就是主席树的思想,对于每个操作多开一条链来维护操作,但是如何查询第k小呢
因为主席树是在权值线段树上修改的,对于每一个tree[i]所存储的是对于其对应区间内所包含的数字的个数,为什么呢,首先讲述主席树的建树过程,主席树在建树时是对于每一个数单开一个链,给予它一个独立的根节点编号,并且每一条链都是在上一条链的基础上维护的,所以对于每一个tree[root[i]],所存储的其实是区间 [1,i] 内的数字的个数,那么然后接简单了,查询root[l-1]到root[r]之间的第k小个数即可。
示例代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;
const int M = 3e7 + 10;
ll n, m, cnt;
ll root[N], a[N], b[N];
struct Tree {
ll l, r, cnt;
} tree[M];
struct SegmentTree {
ll update(ll p, ll l, ll r, ll x) {
ll newp = ++cnt;
tree[newp] = tree[p];
tree[newp].cnt++;
if (l == r) {
return newp;
}
ll mid = (l + r) >> 1;
if (x <= mid) tree[newp].l = update(tree[p].l, l, mid, x);
else tree[newp].r = update(tree[p].r, mid + 1, r, x);
return newp;
}
ll search(ll l, ll r, ll L, ll R, ll k) {
if (l == r) return l;
ll mid = (l + r) >> 1;
ll cnt = tree[tree[R].l].cnt - tree[tree[L].l].cnt;
if (k <= cnt) return search(l, mid, tree[L].l, tree[R].l, k);
else return search(mid + 1, r, tree[L].r, tree[R].r, k - cnt);
}
} st;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
b[i] = a[i];
}
sort(b + 1, b + n + 1);
ll len = unique(b + 1, b + n + 1) - (b + 1);
for (int i = 1; i <= n; i++) {
ll x = lower_bound(b + 1, b + len + 1, a[i]) - b;
root[i] = st.update(root[i - 1], 1, len, x);
}
while (m--) {
ll l, r, k;
cin >> l >> r >> k;
ll ans = st.search(1, len, root[l - 1], root[r], k);
cout << b[ans] << '\n';
}
return 0;
}

浙公网安备 33010602011771号