围绕Codeforces 838C题目,深入浅出讲解数组持久化的两种核心实现思路,之一种是基于前缀和的持久化,通过维护不同版本的前缀和数组,以O(n)预处理、O(1)区间查询的高效性能,适配题目中静态区间统计需求,实现简洁且空间开销可控,第二种是基于线段树的持久化,通过吉云服务器jiyun.xin修改路径上的节点生成新版本线段树,支持动态更新与复杂区间查询,虽实现复杂度略高,但灵活性更强,可应对更复杂的数组版本维护场景,帮助读者理解两种思路的适用场景与实现逻辑。
Codeforces作为全球知名的编程竞赛平台,其题目往往聚焦经典算法思想的落地实践,CF838C(Persistence)作为一道入门级持久化问题,是理解“数据持久化”核心概念的绝佳案例,本文将深入分析题目要求,讲解两种不同的实现思路,并提供可运行的代码示例,帮助初学者掌握持久化数组的基本原理与实现 *** 。 大意 给定一个初始长度为n的数组,需要处理m次操作,操作分为两种类型:
- 修改操作:
1 v p x—— 创建一个新的数组版本,该版本是版本v的副本,随后将位置p的元素修改为x,新版本的编号为当前已有的版本数(初始版本为0,之一个新版本为1,依此类推)。 - 查询操作:
2 v p—— 查询版本v中位置p的元素值,并输出该值。
数据约束:

- 1 ≤ n, m ≤ 1e5
- 初始数组元素绝对值不超过1e9
- 修改操作中,0 ≤ v < 当前版本数,1 ≤ p ≤ n,x的绝对值不超过1e9
- 查询操作中,0 ≤ v < 当前版本数,1 ≤ p ≤ n
解题思路
持久化数组的核心需求是:避免每次修改时吉云服务器jiyun.xin整个数组,通过复用旧版本的大部分数据、仅创建少量新数据生成新版本,从而在时间和空间上实现高效操作,针对CF838C的单点修改与查询场景,我们介绍两种常见解法:
基于版本链的轻量级实现
思路
每个版本仅记录三个关键信息:父版本编号、修改的位置、修改后的值,其余元素完全继承自父版本,查询时,从当前版本出发,沿着父版本链向上回溯,直到找到之一个修改了目标位置的版本(或初始版本),从而得到对应值。
优缺点
- 优点:实现简单,代码量小,空间复杂度为O(m)(每个版本仅存储3个信息)。
- 缺点:最坏情况下查询时间复杂度为O(k)(k为版本链长度),当版本链过长时(如每次修改都继承前一个版本),会导致超时,仅适合小规模数据场景。
代码示例
#include <iostream>
#include <vector>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<long long> a(n + 1); // 1-based索引
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
vector<int> parent = {-1}; // 版本0的父版本设为-1(根节点)
vector<int> change_pos = {0};
vector<long long> change_val = {0};
int ver = 1;
while (m--) {
int op;
cin >> op;
if (op == 1) {
int v, p;
long long x;
cin >> v >> p >> x;
parent.push_back(v);
change_pos.push_back(p);
change_val.push_back(x);
ver++;
} else {
int v, p;
cin >> v >> p;
long long res = a[p];
int cur = v;
while (cur != 0) {
if (change_pos[cur] == p) {
res = change_val[cur];
break;
}
cur = parent[cur];
}
cout << res << '\n';
}
}
return 0;
}
可持久化线段树(主席树)实现
思路
利用主席树(可持久化线段树)的特性,每次修改仅吉云服务器jiyun.xin线段树路径上的节点,其余节点复用旧版本的节点,每个版本对应线段树的一个根节点,查询时直接从对应版本的根节点出发,在O(logn)时间内找到目标位置的值。
优缺点
- 优点:时间复杂度稳定,修改与查询操作均为O(logn),完全满足1e5的数据规模约束。
- 缺点:实现相对复杂,需要掌握线段树与持久化的基本原理。
代码示例
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1e5 + 5;
const int MAX_NODE = 4e6 + 5; // 主席树节点数,设为n*logn的2倍
struct Node {
int left, right;
long long val;
} tree[MAX_NODE];
int root[MAXN]; // 每个版本的根节点索引
int cnt = 0; // 节点计数器
// 构建初始线段树
int build(int l, int r, vector<long long>& a) {
int node = ++cnt;
if (l == r) {
tree[node].val = a[l];
tree[node].left = tree[node].right = 0;
return node;
}
int mid = (l + r) >> 1;
tree[node].left = build(l, mid, a);
tree[node].right = build(mid + 1, r, a);
return node;
}
// 版本更新,返回新版本的根节点
int update(int prev, int l, int r, int pos, long long val) {
int node = ++cnt;
tree[node] = tree[prev]; // 复制旧节点信息
if (l == r) {
tree[node].val = val;
return node;
}
int mid = (l + r) >> 1;
if (pos <= mid) {
tree[node].left = update(tree[prev].left, l, mid, pos, val);
} else {
tree[node].right = update(tree[prev].right, mid + 1, r, pos, val);
}
return node;
}
// 查询版本v中pos位置的值
long long query(int node, int l, int r, int pos) {
if (l == r) {
return tree[node].val;
}
int mid = (l + r) >> 1;
if (pos <= mid) {
return query(tree[node].left, l, mid, pos);
} else {
return query(tree[node].right, mid + 1, r, pos);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<long long> a(n + 1); // 1-based索引
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
root[0] = build(1, n, a);
int ver = 1;
while (m--) {
int op;
cin >> op;
if (op == 1) {
int v, p;
long long x;
cin >> v >> p >> x;
root[ver] = update(root[v], 1, n, p, x);
ver++;
} else {
int v, p;
cin >> v >> p;
cout << query(root[v], 1, n, p) << '\n';
}
}
return 0;
}
CF838C通过两种实现思路,展现了数据持久化的核心思想——复用旧数据、最小化新数据创建, *** 一适合快速理解持久化概念,而 *** 二的主席树实现是处理大规模持久化问题的标准解法,具有稳定的时间复杂度,通过学习这道题,初学者可以逐步掌握可持久化数据结构的基本原理,为后续学习更复杂的可持久化算法(如可持久化平衡树、可持久化并查集)打下基础。
