可持久化线段树学习笔记

可持久化线段树学习笔记

可持久化线段树

假如有一颗线段树如下(黑色部分):

现在要修改权值为 1 1 1 的点(在线段树中对应编号为 8 8 8),那么从根到对应点的一条长为 log ⁡ 2 n \log_2n log2n 的链(红色部分)就会被插入到原来的线段树。

现在考虑怎么在历史版本上修改。

其实我们发现,历史版本对现在版本的印象仅限于两点的 LCA 到根的那一条链。

在上图中,绿色点是黑色点的修改版本,橙色点是绿色点的修改版本,但是修改的节点不在绿色链上

但实际上这其实无关紧要,因为根不一样,所以根的子树可以代表一个版本。实际上我们可以这样画图。

所以每次做链上的修改的时候,只需要把链上的点复制一份,再根据回溯过来的结果指定左右儿子即可。

总结一下,就是动态开点,但是根有版本之间的区分。

例题 P3919 【模板】可持久化线段树 1(可持久化数组)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6;
int n, m;
int rt[N + 5], a[N + 5];// 表示版本 i 的根节点编号 rt[i] 
int val[(N << 5) + 5], ls[(N << 5) + 5], rs[(N << 5) + 5];// 表示 i 号节点的权值 
int cnt;

int new_node(int num){
	cnt++;
	ls[cnt] = ls[num], rs[cnt] = rs[num], val[cnt] = val[num];
	return cnt;
}

int build(int num, int l, int r){
	num = ++cnt;
	if(l == r){
		val[num] = a[l];
		return num;
	}
	int mid = (l + r) >> 1;
	ls[num] = build(ls[num], l, mid);
	rs[num] = build(rs[num], mid + 1, r);
	return num;
}

int upd(int num, int l, int r, int x, int v){
	num = new_node(num);
	if(l == r)	val[num] = v;
	else{
		int mid = (l + r) >> 1;
		if(x <= mid)
			ls[num] = upd(ls[num], l, mid, x, v);
		else
			rs[num] = upd(rs[num], mid +1, r, x, v);
	}
	return num;
}

int qry(int num, int l, int r, int x){
	if(l == r)	return val[num];
	else{
		int mid = (l + r) >> 1;
		if(x <= mid)	return qry(ls[num], l, mid, x);
		else	return qry(rs[num], mid + 1, r, x);
	}
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>m;
	for(int i = 1;i <= n;i++){
		cin>>a[i]; 
	}
	rt[0] = build(0, 1, n);
	for(int i = 1;i <= m;i++){
		int rot, op, p;
		cin>>rot>>op>>p;
		if(op == 1){
			int c;	cin>>c;
			rt[i] = upd(rot[rt], 1, n, p, c);
		}
		else{
			cout<<qry(rt[rot], 1, n, p)<<'\n';
			rt[i] = rt[rot];
		}
	}
	return 0;
}

主席树

主席树就是可持久化权值线段树。

例题 P3834 【模板】可持久化线段树 2

给定 n n n 个整数构成的序列 a a a,查询 [ l , r ] [l, r] [l,r] 中第 k k k 小值。

维护一棵可持久化权值线段树,父节点 [ l , r ] [l, r] [l,r] 表示有多少个数 x ∈ [ l , r ] x \in [l, r] x[l,r]

假如从 a 1 a_1 a1 遍历到 a n a_n an,当遍历到 a i a_i ai 时,将线段树中 v a l [ a i ] val[a_i] val[ai] 增加 1 1 1 并备份到新的版本 i i i。那么这个版本 i i i 表示的是 1 ∼ i 1 \sim i 1i 的权值线段树。

假如我要找 [ 1 , i ] [1, i] [1,i] 中第 k k k 大的数,那么我可以在第 i i i 个版本进行线段树上二分,即,当节点 u u u左儿子大小(表示区间内数的数量)大于等于 k k k 时往左走,否则往右走。

现在我需要找到两个版本的权值线段树,能代表 [ l , r ] [l, r] [l,r]。容易发现,如果我们把 r r r 版本的权值线段树减去 l − 1 l-1 l1 版本的权值线段树(所有节点都相减,相当于统计了 [ l , r ] [l, r] [l,r] 的桶),那么差就能代表 [ l , r ] [l, r] [l,r] 的权值线段树了。

而可持久化权值线段树就是主席树。

#include <bits/stdc++.h>
#define pii pair<int, int>
#define pb push_back
using namespace std;
using ll = long long;
const int N = 2e5 + 5;
int n, m, len, cnt;
int sum[N << 5], ls[N << 5], rs[N << 5], rt[N]; 
int a[N], b[N];// Ô­Êý×éºÍÀëÉ¢Ö®ºóµÄÊý×é 

int new_node(int num){
	++cnt;
	sum[cnt] = sum[num] + 1, ls[cnt] = ls[num], rs[cnt] = rs[num];
	return cnt;
}

void build(int &num, int l, int r){
	num = ++cnt;
	if(l == r)	return;
	int mid = (l + r) >> 1;
	build(ls[num], l, mid);
	build(rs[num], mid + 1, r);
}

int upd(int num, int l, int r, int p){
	num = new_node(num);// ¸´Öƽڵã
	if(l == r)	return num;
	int mid = (l + r) >> 1;
	if(p <= mid)	ls[num] = upd(ls[num], l, mid, p);
	else	rs[num] = upd(rs[num], mid + 1, r, p);
	return num;
}

int qry(int fir, int sec, int l, int r, int k){
//	cout<<l<<" "<<r<<'\n';
//	cout<<sum[ls[sec]]<<" ls[sec], ls[fir] "<<sum[ls[fir]]<<'\n';
	if(l == r)	return l;
	int mid = (l + r) >> 1, x = sum[ls[sec]] - sum[ls[fir]];
	if(x >= k)	return qry(ls[fir], ls[sec], l, mid, k);// Íù×ó¶ù×Ó×ß 
	else	return qry(rs[fir], rs[sec], mid + 1, r, k - x);// ÍùÓÒ¶ù×Ó×ß 
}

signed 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);
	len = unique(b + 1, b + n + 1) - b - 1;
	build(rt[0], 1, len);// Ö÷ÒªÊÇ°Ñ ls, rs ´¦ÀíºÃ£¬ÀíÂÛÉÏÀ´½²¿ÉÒÔ O(n) ½â¾ö 
	for(int i = 1;i <= n;i++){
		int p = lower_bound(b + 1, b + len + 1, a[i]) - b;// ÀëÉ¢»¯ 
		rt[i] = upd(rt[i-1], 1, len, p); 
	}
	while(m--){
		int l, r, k;	cin>>l>>r>>k;
		cout<<b[qry(rt[l-1], rt[r], 1, len, k)]<<'\n';
	}
	return 0;
}
可持久化线段树是一种支持历史版本查询的数据结构,其核心思想是在每次修改操作时保留完整的旧版本信息。这使得它在某些应用场景中非常有用,例如版本控制系统或需要回溯操作的算法问题。 ### 空间复杂度分析 可持久化线段树的空间复杂度与普通线段树相比有所增加。普通线段树的空间复杂度为 $O(n)$,其中 $n$ 是数据规模。而可持久化线段树由于需要保留历史版本,每次更新操作都会生成新的节点,因此其空间复杂度为 $O(n \log n)$。具体来说,每次更新操作最多会生成 $O(\log n)$ 个新节点,因为线段树的高度为 $O(\log n)$,每个节点最多分裂一次[^1]。 ### 实现原理 可持久化线段树的核心实现原理是**节点复用**和**路径复制**。当对线段树进行更新时,只有从根节点到目标节点的路径上的节点会被复制,其余节点保持不变。这种方式避免了对整个线段树的完全复制,从而节省了内存[^1]。 具体实现中,每个版本的线段树通过一个根节点指针来标识。当进行更新操作时,新版本的根节点指向一个新的节点,而未修改的子树则继续指向旧版本的节点。这种设计使得不同版本之间可以共享未修改的部分,从而减少内存开销。 以下是一个简单的可持久化线段树的实现示例,用于单点更新和区间查询: ```cpp #include <iostream> #include <vector> using namespace std; struct Node { int val; // 节点值,例如区间和 Node* left; Node* right; Node(int v) : val(v), left(nullptr), right(nullptr) {} }; class PersistentSegmentTree { private: vector<int> data; Node* build(Node* node, int l, int r) { if (l == r) { node->val = data[l]; return node; } int mid = (l + r) / 2; node->left = new Node(0); node->right = new Node(0); build(node->left, l, mid); build(node->right, mid + 1, r); node->val = node->left->val + node->right->val; return node; } Node* update(Node* node, int l, int r, int idx, int value) { if (l == r) { Node* new_node = new Node(value); return new_node; } int mid = (l + r) / 2; Node* new_node = new Node(0); if (idx <= mid) { new_node->left = update(node->left, l, mid, idx, value); new_node->right = node->right; } else { new_node->left = node->left; new_node->right = update(node->right, mid + 1, r, idx, value); } new_node->val = new_node->left->val + new_node->right->val; return new_node; } int query(Node* node, int l, int r, int ql, int qr) { if (qr < l || ql > r) return 0; if (ql <= l && r <= qr) return node->val; int mid = (l + r) / 2; return query(node->left, l, mid, ql, qr) + query(node->right, mid + 1, r, ql, qr); } public: vector<Node*> roots; // 存储每个版本的根节点 PersistentSegmentTree(vector<int>& arr) { data = arr; roots.push_back(new Node(0)); build(roots[0], 0, data.size() - 1); } void update(int version, int idx, int value) { Node* new_root = update(roots[version], 0, data.size() - 1, idx, value); roots.push_back(new_root); } int query(int version, int ql, int qr) { return query(roots[version], 0, data.size() - 1, ql, qr); } }; ``` ### 内存占用分析 可持久化线段树的内存占用主要由以下几个部分构成: 1. **节点存储**:每个节点需要存储值、左右子节点指针。通常每个节点的大小为常数级别(例如包含一个整数值和两个指针)。 2. **版本管理**:每个版本通过一个根节点指针进行管理,根节点指针的存储开销为 $O(1)$。 3. **路径复制**:每次更新操作会生成新的节点,这些新节点的总数为 $O(\log n)$,因此总内存占用为 $O(n \log n)$。 在实际应用中,内存占用还可能受到编程语言的内存管理机制影响。例如,在 C++ 中手动管理内存可能导致较高的内存碎片,而在 Java 或 Python 等具有垃圾回收机制的语言中,内存占用可能相对较低,但具体表现取决于实现细节。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值