可持久化数据结构
简介
可持久化就是通过每次修改都创建新版本,来保留数据结构回滚与访问历史版本的能力。
可持久化分为两类:
-
部分可持久化:所有版本都可以访问,但是只有最新版本可以修改;
-
完全可持久化:所有版本都既可以访问又可以修改。
注意:一个数据结构可持久化当且仅当它的拓补结构在使用的过程中保持不变。
核心思想
要想访问数据结构的历史版本,暴力做法就是在每次修改操作后将整个数据结构拷贝一份存储起来,若共有
但是我们不妨思考一下,全部拷贝一遍是不是太浪费了?
比如更新一个游戏,只是下载新的内容,而不是把整个游戏重新下载一遍。
“可持久化”就提供了一个类似这样的思想,它并不是暴力地全部拷贝,而是在每次操作结束后,仅创建数据结构中发生改变的部分的副本,不拷贝其他内容。
这样一来,维护数据结构的时间复杂度没有增加,空间复杂度仅增长为与时间同级的规模。
下面都是一些可持久化数据结构的实例。
可持久化
和
依次维护
那么可持久化
先插入单词
再插入单词
接下来我们只需要把新的路径记录下来就可以了。
根据可持久化的核心思想,我们每次只用记录不一样的地方,相同的地方直接从上一个版本
所以先把根节点
接下来,由于在上一个版本中,由根到
同理,
插入完成后,这棵树就长成了这样:
绿框中的就是版本
该更新版本
但是
虽然这是一个新的点,但它其实是从原来的
但是
最后由于
完成插入后结构如图:
绿框中的即是版本
注意我们每次都只更新了一条路径,每次更新至于上一个版本比较,复制信息也从上一个版本复制。
按照上面的方法,更新
绿框中的即是版本
这就是可持久化
总之,只修改有关路径上的点。
例题:
P4735 最大异或和
类似于前缀和的思想,设
根据异或运算的性质:
对于每次添加操作,序列
对于每次询问操作,等价于:已知一个整数
若先不考虑
这种问题可以用
若只有
因为可持久化
那么再加上
其实这就是一个存在性问题,即询问在与
设
这样,从
整个算法的时间复杂度为
#include <iostream>
#include <cstring>
using namespace std;
const int N = 600010, M = N * 25;
int n, m;
int tr[M][2], max_id[M];
int root[N], idx;
int s[N];
void insert(int i, int k, int p, int q) {
if(k < 0) {
max_id[q] = i;
return ;
}
int v = s[i] >> k & 1;
if(p) tr[q][v ^ 1] = tr[p][v ^ 1];
tr[q][v] = ++idx;
insert(i, k - 1, tr[p][v], tr[q][v]);
max_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]);
}
int query(int root, int c, int l) {
int p = root;
for(int i = 23; i >= 0; i--) {
int v = c >> i & 1;
if(max_id[tr[p][v ^ 1]] >= l) p = tr[p][v ^ 1];
else p = tr[p][v];
}
return c ^ s[max_id[p]];
}
int main() {
scanf("%d%d", &n, &m);
max_id[0] = -1;
root[0] = ++idx;
insert(0, 23, 0, root[0]);
int x;
for(int i = 1; i <= n; i++) {
scanf("%d", &x);
s[i] = s[i - 1] ^ x;
root[i] = ++idx;
insert(i, 23, root[i - 1], root[i]);
}
char op[2];
int l, r;
while(m--) {
scanf("%s", op);
if(op[0] == 'A') {
scanf("%d", &x);
root[++n] = ++idx;
s[n] = s[n - 1] ^ x;
insert(n, 23, root[n - 1], root[n]);
}
else {
scanf("%d%d%d", &l, &r, &x);
printf("%d\n", query(root[r - 1], s[n] ^ x, l - 1));
}
}
return 0;
}
可持久化线段树
可持久化线段树又称主席树(据说是因为它的发明者的名字首字母是 hjt)。
基于可持久化
修改的时候与
同时,由于可持久化线段树不再是一棵完全二叉树,所以就不满足“左儿子编号是父节点的
所以不能再用次序编号,而是改为直接记录每个节点的左、右子节点编号,与
struct node{
int ls, rs;
}tr[N << 5];
因为每次只会修改
同时,我们不再记录每个节点代表的区间
可持久化线段树难以支持大部分区间修改操作。
因为当一个节点下传懒标记时,一旦我们创建它左右儿子
在一些特殊的题目中,可以使用“标记永久化”代替标记下传,从而完成区间修改操作。
但这种做法局限性太大,实用性也不广。
例题:
P3919 【模板】可持久化线段树 1(可持久化数组)
历史版本单点修改模板题,只需要注意每次查询也要新建版本就行了。
#include <iostream>
using namespace std;
const int N = 1000010;
int n, m;
int a[N];
struct node{
int ls, rs;
int val;
}tr[N << 5];
int root[N], idx;
int build(int l, int r) {
int p = ++idx;
if(l == r) {
tr[p].val = a[l];
return p;
}
int mid = l + r >> 1;
tr[p].ls = build(l, mid);
tr[p].rs = build(mid + 1, r);
return p;
}
int insert(int p, int x, int v, int l, int r) {
int q = ++idx;
tr[q] = tr[p];
if(l == r) {
tr[q].val = v;
return q;
}
int mid = l + r >> 1;
if(x <= mid) tr[q].ls = insert(tr[p].ls, x, v, l, mid);
else tr[q].rs = insert(tr[p].rs, x, v, mid + 1, r);
return q;
}
int query(int p, int x, int l, int r) {
if(l == r) return tr[p].val;
int mid = l + r >> 1;
if(x <= mid) return query(tr[p].ls, x, l, mid);
else return query(tr[p].rs, x, mid + 1, r);
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
root[0] = build(1, n);
int p, op, pos, v;
int tt = 0;
while(m--) {
scanf("%d%d%d", &p, &op, &pos);
if(op == 1) {
scanf("%d", &v);
root[++tt] = insert(root[p], pos, v, 1, n);
}
else {
printf("%d\n", query(root[p], pos, 1, n));
root[++tt] = root[p]; //新建版本
}
}
return 0;
}
P3834 【模板】可持久化线段树 2
静态区间求第
这道题解法多样,比如归并树、划分树、可持久化线段树、树套树。
这里讲的是可持久化线段树做法。
在线段树上维护”序列
只考虑限制
但是左范围不能像上一个题那样做,上个题由于是存在性问题,具有一定的特殊性。
不过,线段树有个很特殊的地方:它的结构是一模一样的,不像
也就是说我们可以通过减法,计算出
同时由于这道题的值域范围很大,所以还需要做离散化。
该算法的时间复杂度为
若要拓展到带修动态区间第
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 200010;
int n, m;
int a[N];
vector<int> nums;
struct node{
int ls, rs;
int cnt;
}tr[N * 25];
int root[N], idx;
int find(int x) { //求离散化之后的值
return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
}
int build(int l, int r) {
int p = ++idx;
if(l == r) return p;
int mid = l + r >> 1;
tr[p].ls = build(l, mid);
tr[p].rs = build(mid + 1, r);
return p;
}
int insert(int p, int l, int r, int x) {
int q = ++idx;
tr[q] = tr[p];
if(l == r) {
tr[q].cnt++;
return q;
}
int mid = l + r >> 1;
if(x <= mid) tr[q].ls = insert(tr[p].ls, l, mid, x);
else tr[q].rs = insert(tr[p].rs, mid + 1, r, x);
tr[q].cnt = tr[tr[q].ls].cnt + tr[tr[q].rs].cnt;
return q;
}
int query(int q, int p, int l, int r, int k) {
if(l == r) return l;
int mid = l + r >> 1;
int cnt = tr[tr[q].ls].cnt - tr[tr[p].ls].cnt; //有多少个数落在值域 [l, mid] 内
if(k <= cnt) return query(tr[q].ls, tr[p].ls, l, mid, k); //左边找第 k 大
else return query(tr[q].rs, tr[p].rs, mid + 1, r, k - cnt); //右边找第 k - cnt 大
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
nums.push_back(a[i]);
}
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end()); //离散化
root[0] = build(0, nums.size() - 1);
for(int i = 1; i <= n; i++) root[i] = insert(root[i - 1], 0, nums.size() - 1, find(a[i]));
int l, r, k;
while(m--) {
scanf("%d%d%d", &l, &r, &k);
printf("%d\n", nums[query(root[r], root[l - 1], 0, nums.size() - 1, k)]); //由于 query 返回的是离散化之后的值,所以要再套一层 nums 才是原数
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】