可持久化线段树

可持久化线段树


前置知识

线段树,线段树上动态开点(不会可以参考我之前的博客或者oiwiki)


可持久化线段树(可持久化数组)——单点修改

[例题:洛谷——P3919 【模板】可持久化线段树 1(可持久化数组)](P3919 【模板】可持久化线段树 1(可持久化数组) - 洛谷)

题目大意:

对一个数组进行单点操作,每次查询与修改会产生一个历史版本,对某个指定的历史版本进行操作

解题思路:

朴素算法就是每次操作时新开一个数组存储新版本,时间复杂度与空间复杂度都为$ O(N ^ 2)$,显然,这是我们无法接受的,那么我们就要利用可持久化线段树来维护操作

算法讲解:

可持久化线段树,是基于线段树的一种算法,它通过对每一个操作进行增加一条链来维护操作

具体实现是对于每一个操作i,在线段树上新开一条链以root[i]为根节点。比若说我们要对一个数组a=[1,2,3,4]的第4项进行修改,就可以新开一个根节点,然后新建出一条链与原线段树相合并

抽象图示:

![](C:\Users\Lenovo\Pictures\Screenshots\屏幕截图 2025-07-22 220939.png)

这个样子,只要再查询时传入对应版本号就可以直接查询

代码示例:

#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;
}
posted @ 2025-07-27 16:11  Accepted_wyr  阅读(35)  评论(0)    收藏  举报