可持久化线段树
可持久化线段树算法详解
一、什么是可持久化数据结构
1.1 基本概念
可持久化(Persistent)是指在数据结构被修改后,仍然能够保留修改前的历史版本,并能够在这些历史版本上进行查询操作。
1.2 应用场景
- 区间第k大/小查询(主席树)
- 二维/三维偏序问题
- 版本控制系统
- 时间轴查询问题
二、可持久化线段树(主席树)
2.1 核心思想
- 每次修改只创建新路径上的节点,复用未修改的子树
- 通过根节点数组记录每个版本的入口
2.2 数据结构设计
struct Node {
int l, r; // 左右儿子编号
int val; // 节点存储的值
// 根据具体问题可添加更多字段
};
const int MAXN = 1e5 + 10;
const int LOG = 20; // log2(MAXN)
Node tree[MAXN * LOG * 4]; // 节点池
int roots[MAXN]; // 各个版本的根节点
int node_cnt = 0; // 节点计数器
基础操作实现
3.1 节点创建
// 创建新节点(可复用旧节点部分信息)
int new_node(int old = 0) {
node_cnt++;
if (old) tree[node_cnt] = tree[old];
else tree[node_cnt].l = tree[node_cnt].r = tree[node_cnt].val = 0;
return node_cnt;
}
3.2 建树(初始化版本0)
int build(int l, int r) {
int p = new_node();
if (l == r) {
// 初始化叶子节点
return p;
}
int mid = (l + r) >> 1;
tree[p].l = build(l, mid);
tree[p].r = build(mid + 1, r);
return p;
}
3.3 单点更新
// 在版本old_root基础上,将位置pos的值增加val
int update(int old_root, int l, int r, int pos, int val) {
int p = new_node(old_root);
if (l == r) {
tree[p].val += val;
return p;
}
int mid = (l + r) >> 1;
if (pos <= mid) {
tree[p].l = update(tree[old_root].l, l, mid, pos, val);
} else {
tree[p].r = update(tree[old_root].r, mid + 1, r, pos, val);
}
// 向上更新
tree[p].val = tree[tree[p].l].val + tree[tree[p].r].val;
return p;
}
经典应用:静态区间第k小
4.1 问题描述
给定长度为n的数组和m个查询,每个查询求区间[l, r]中第k小的数。
4.2 算法步骤
步骤1:离散化
vector<int> nums = original_array;
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
// 获取离散化后的值
int get_id(int x) {
return lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1;
}
步骤2:构建版本序列
roots[0] = build(1, nums.size());
for (int i = 1; i <= n; i++) {
int id = get_id(original_array[i-1]);
roots[i] = update(roots[i-1], 1, nums.size(), id, 1);
}
步骤3:区间查询
int query(int root_l, int root_r, int l, int r, int k) {
if (l == r) return l;
int mid = (l + r) >> 1;
int left_cnt = tree[tree[root_r].l].val - tree[tree[root_l].l].val;
if (k <= left_cnt) {
return query(tree[root_l].l, tree[root_r].l, l, mid, k);
} else {
return query(tree[root_l].r, tree[root_r].r, mid + 1, r, k - left_cnt);
}
}
// 查询区间[l, r]的第k小
int ans_id = query(roots[l-1], roots[r], 1, nums.size(), k);
int ans = nums[ans_id - 1]; // 还原原始值
4.3 时间复杂度分析
建树:O(n log n)
每次更新:O(log n)
每次查询:O(log n)
空间复杂度:O(n log n)
五、进阶应用
5.1 带修改的可持久化线段树(树状数组套线段树)
// 使用树状数组维护外层,每个节点是一棵线段树
void modify(int x, int pos, int val) {
for (; x <= n; x += x & -x) {
roots[x] = update(roots[x], 1, len, pos, val);
}
}
int query_prefix(int x, int l, int r, int k) {
// 查询前缀[1, x]的信息
}
5.2 可持久化权值线段树求区间不同数个数
// 记录每个值最后一次出现的位置
// 在位置i插入时,在版本i中:
// 1. 删除该值上次出现的位置
// 2. 在位置i插入该值
六、模板代码(完整实现)
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 10;
const int LOG = 20;
struct Node {
int l, r, sum;
} tree[MAXN * LOG];
int roots[MAXN], a[MAXN], nums[MAXN];
int node_cnt = 0, n, m;
int build(int l, int r) {
int p = ++node_cnt;
if (l == r) return p;
int mid = (l + r) >> 1;
tree[p].l = build(l, mid);
tree[p].r = build(mid + 1, r);
return p;
}
int update(int pre, int l, int r, int pos) {
int p = ++node_cnt;
tree[p] = tree[pre];
tree[p].sum++;
if (l == r) return p;
int mid = (l + r) >> 1;
if (pos <= mid) {
tree[p].l = update(tree[pre].l, l, mid, pos);
} else {
tree[p].r = update(tree[pre].r, mid + 1, r, pos);
}
return p;
}
int query(int u, int v, int l, int r, int k) {
if (l == r) return l;
int mid = (l + r) >> 1;
int left_sum = tree[tree[v].l].sum - tree[tree[u].l].sum;
if (k <= left_sum) {
return query(tree[u].l, tree[v].l, l, mid, k);
} else {
return query(tree[u].r, tree[v].r, mid + 1, r, k - left_sum);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
nums[i] = a[i];
}
// 离散化
sort(nums + 1, nums + n + 1);
int len = unique(nums + 1, nums + n + 1) - nums - 1;
// 建树
roots[0] = build(1, len);
// 构建版本
for (int i = 1; i <= n; i++) {
int id = lower_bound(nums + 1, nums + len + 1, a[i]) - nums;
roots[i] = update(roots[i-1], 1, len, id);
}
// 查询
while (m--) {
int l, r, k;
scanf("%d%d%d", &l, &r, &k);
int id = query(roots[l-1], roots[r], 1, len, k);
printf("%d\n", nums[id]);
}
return 0;
}
练习题推荐
基础题目
1. 静态区间第k小
POJ 2104 K-th Number
http://poj.org/problem?id=2104
SPOJ MKTHNUM
https://www.spoj.com/problems/MKTHNUM/
洛谷 P3834 【模板】可持久化线段树 2
https://www.luogu.com.cn/problem/P3834
2. 区间不同数个数
SPOJ DQUERY - D-query
https://www.spoj.com/problems/DQUERY/
HDU 3333 Turing Tree
http://acm.hdu.edu.cn/showproblem.php?pid=3333
进阶题目
1. 带修改的可持久化线段树
HDU 4348 To the moon
http://acm.hdu.edu.cn/showproblem.php?pid=4348
BZOJ 1901 Dynamic Rankings
https://hydro.ac/d/bzoj/p/1901
洛谷 P2617 Dynamic Rankings
https://www.luogu.com.cn/problem/P2617
2. 二维问题
Codeforces 813E Army Creation
https://codeforces.com/problemset/problem/813/E
HDU 5919 Sequence II
http://acm.hdu.edu.cn/showproblem.php?pid=5919
3. 树上问题
SPOJ COT - Count on a tree
https://www.spoj.com/problems/COT/
BZOJ 2588 Count on a tree
https://hydro.ac/d/bzoj/p/2588
洛谷 P2633 Count on a tree
https://www.luogu.com.cn/problem/P2633
4. 综合应用
Codeforces 840D Destiny
https://codeforces.com/problemset/problem/840/D
HDU 5799 This world need more Zhu
http://acm.hdu.edu.cn/showproblem.php?pid=5799
专项训练
1. 可持久化并查集
BZOJ 3673 可持久化并查集
https://hydro.ac/d/bzoj/p/3673
洛谷 P3402 可持久化并查集
https://www.luogu.com.cn/problem/P3402
2. 可持久化平衡树
洛谷 P3835 【模板】可持久化平衡树
https://www.luogu.com.cn/problem/P3835
BZOJ 3674 可持久化并查集加强版
https://hydro.ac/d/bzoj/p/3674
八、注意事项
1.空间管理
2.预估节点总数:操作次数 × log(n)
3.使用动态开点避免MLE
4.离散化处理
5.注意边界情况
6.保持值域连续
7.版本控制
8.明确每个版本根节点的含义
9.注意版本索引从0还是1开始
总结
可持久化线段树是一种功能强大的数据结构,通过"部分复制"的思想实现了版本管理。掌握其原理和实现,能够解决许多复杂的区间查询问题,是算法竞赛中的重要工具。
相关学习资源
1.算法竞赛进阶指南 - 李煜东
2.可持久化数据结构专题 - OI Wiki
https://oi-wiki.org/ds/persistent/
注:部分BZOJ链接使用的是Hydro镜像站,因为原BZOJ已关闭。
好水的主席树板子题
一:https://www.luogu.com.cn/problem/P4587
P4587 [FJOI2016] 神秘数
用可持久化线段树维护区间小于等于 \(x\) 的值之和。
可以证明每次计算答案是 \(\log\) 级别的。
#include<bits/stdc++.h>
#define wk(x) write(x),putchar(' ')
#define wh(x) write(x),putchar('\n')
#define int long long
#define INF 1e15
#define L (tree[p].l)
#define R (tree[p].r)
#define MID ((l+r)>>1)
#define N 500005
using namespace std;
int n,m,k,jk,ans,sum,num,cnt,tot;
int head[N],dis[N],vis[N],wis[N],f[N],Root[N];
void read(int &x)
{
//
return;
}
void write(int x)
{
//
return;
}
struct SUG{
struct T{
int l,r,ans;
}tree[(N<<2)*20];
void push_down(int p){
tree[p].ans=tree[L].ans+tree[R].ans;
}
void ADD(int &p,int l,int r,int x,int y){
tree[++cnt]=tree[p];p=cnt;
if(x<=l&&r<=y){
tree[p].ans+=y;return;
}
if(MID>=x) ADD(L,l,MID,x,y);
if(MID<y) ADD(R,MID+1,r,x,y);
push_down(p);
}
int query_add(int p,int l,int r,int x,int y){
if(x<=l&&r<=y){return tree[p].ans;}
int zz=0;
if(MID>=x) zz+=query_add(L,l,MID,x,y);
if(MID<y) zz+=query_add(R,MID+1,r,x,y);
push_down(p);return zz;
}
}T;
//struct H{
// int l,r,t,id;
//}q[N];
//
//bool cmp(const H &a,const H &b){
// return a.l/sz==b.l/sz?a.r/sz==b.r/sz?a.t<b.t:a.r<b.r:a.l<b.l;
//}
signed main()
{
read(n);
for(int i=1;i<=n;i++) read(dis[i]);Root[0]=++cnt;
for(int i=1;i<=n;i++) T.ADD(Root[i]=Root[i-1],1,INF,dis[i],dis[i]);
read(m);while(m--){
int l,r,ans=1;read(l),read(r);
while(1){
int z=T.query_add(Root[r],1,INF,1,ans)-T.query_add(Root[l-1],1,INF,1,ans);
if(z>=ans) ans=z+1;else break;
}wh(ans);
}
return 0;
}
二:https://www.luogu.com.cn/problem/P11807
P11807 [PA 2017] 抄作业
「你抄就抄吧,但是稍微改改,别和我的一模一样就行。」
容易想到用哈希比较,得出第一个不同的位置。
于是可以在线段树上二分。由于每次只修改一个点,故不难想到可持久化线段树,每次新开一条路。
#include<bits/stdc++.h>
#define wk(x) write(x),putchar(' ')
#define wh(x) write(x),putchar('\n')
#define int long long
#define L (tree[p].l)
#define R (tree[p].r)
#define MID ((l+r)>>1)
#define N 500005
using namespace std;
const int G=1331;
const int mod=998244353;
int n,m,k,jk,ans,sum,num,cnt,tot;
int head[N],dis[N],vis[N],wis[N],f[N],Root[N];
void read(int &x)
{
//
return;
}
void write(int x)
{
//
return;
}
struct SUG{
struct T{
int l,r;
unsigned long long ans;
}tree[(N<<2)*20];
void push_down(int p,int len){
tree[p].ans=tree[L].ans*wis[len]+tree[R].ans;
return;
}
void build(int &p,int l,int r){
tree[++cnt]=tree[p];p=cnt;
if(l==r){tree[p].ans=dis[l];return;}
build(L,l,MID);build(R,MID+1,r);
push_down(p,MID-l+1);return;
}
void ADD(int &p,int l,int r,int x,int z){
tree[++cnt]=tree[p];p=cnt;
if(l==r){tree[p].ans=z;return;}
if(MID>=x) ADD(L,l,MID,x,z);
else ADD(R,MID+1,r,x,z);
push_down(p,MID-l+1);return;
}
int query_add(int p,int p1,int l,int r){
if(l==r) return tree[p].ans<tree[p1].ans?1:-1;
if(tree[tree[p].l].ans!=tree[tree[p1].l].ans)
return query_add(tree[p].l,tree[p1].l,l,MID);
else if(tree[tree[p].r].ans!=tree[tree[p1].r].ans)
return query_add(tree[p].r,tree[p1].r,MID+1,r);
else return 0;
}
}T;
struct P{int id;}q[N];
bool cmp(P a,P b){
int Get=T.query_add(Root[a.id],Root[b.id],1,n);
return ((Get==0)?a.id<b.id:(Get==-1?0:1));
}
signed main()
{
read(n);read(m);wis[0]=1;
for(int i=1;i<=n;i++) read(dis[i]),wis[i]=wis[i-1]*G%mod;
T.build(Root[1]=++cnt,1,n);q[1].id=1;
for(int i=2,x,y;i<=m;i++){
read(x),read(y);q[i].id=i;
T.ADD(Root[i]=Root[i-1],1,n,x,y);
}
sort(1+q,1+q+m,cmp);
for(int i=1;i<=m;i++) wk(q[i].id);
return 0;
}
拓展
P4755 Beautiful Pair
https://www.luogu.com.cn/problem/P4755
题目描述
小 D 有个数列 \(\{a\}\),当一个数对 \((i,j)\)(\(i \le j\))满足 \(a_i\) 和 \(a_j\) 的积不大于 \(a_i, a_{i+1}, \ldots, a_j\) 中的最大值时,小 D 认为这个数对是美丽的。请你求出美丽的数对的数量。
输入格式
第一行输入一个整数 \(n\),表示元素个数。
第二行输入 \(n\) 个整数 \(a_1,a_2,a_3,\ldots,a_n\),为所给的数列。
输出格式
输出一个整数,为美丽的数字对数。
输入输出样例 #1
输入 #1
4
1 3 9 3
输出 #1
5
输入输出样例 #2
输入 #2
5
1 1 2 1 1
输出 #2
14
说明/提示
【样例解释 #1】
五种可行的数对为 \((1,1), (1,2), (1,3), (1,4), (2,4)\)。
【样例解释 #2】
只有数对 \((3,3)\) 不可行。
【数据范围】
对于 \(100 \%\) 的数据,\(1\le n\le{10}^5\),\(1\le a_i\le{10}^9\)。
整体二分,考虑对于一段区间的贡献,先用 ST表 找出区间最大值的位置后,考虑这个最大值的贡献。枚举点少的一边,这样就最多是 \(\log\) 级别的了。判断另一边小于等于 \(MAX/A_i\) 的个数就行了。
#include<bits/stdc++.h>
#define wk(x) write(x),putchar(' ')
#define wh(x) write(x),putchar('\n')
#define int long long
#define L (tree[p].l)
#define R (tree[p].r)
#define MID ((l+r)>>1)
#define N 600005
#define NO printf("No\n")
#define YES printf("Yes\n")
using namespace std;
int n,m,k,jk,ans,sum,num,cnt,tot;
int head[N],dis[N],vis[N],wis[N],f[N][30],Root[N];
void read(int &x)
{
//qwq
return;
}
void write(int x)
{
//qwq
return;
}
struct SUG{
struct T{
int l,r,ans,lazy;
}tree[(N<<2)*20];
void push_down(int p){
tree[p].ans=tree[L].ans+tree[R].ans;
}
void build(int &p,int l,int r){p=++cnt;
if(l==r){tree[p].ans=0;return;}
build(L,l,MID);build(R,MID+1,r);
push_down(p);
}
void ADD(int &p,int l,int r,int x){
tree[++cnt]=tree[p];p=cnt;
if(x<=l&&r<=x){
tree[p].ans++;return;
}
if(MID>=x) ADD(L,l,MID,x);
if(MID<x) ADD(R,MID+1,r,x);
push_down(p);
}
int query_add(int p,int l,int r,int x,int y){
if(y==0) return 0;
if(x<=l&&r<=y){return tree[p].ans;}
int zz=0;
if(MID>=x) zz+=query_add(L,l,MID,x,y);
if(MID<y) zz+=query_add(R,MID+1,r,x,y);
push_down(p);return zz;
}
}T;
int Get(int l,int r){
int len=__lg(r-l+1);
return (dis[f[l][len]]>dis[f[r-(1<<len)+1][len]])?f[l][len]:f[r-(1<<len)+1][len];
}
void solve(int l,int r){
if(l>r) return;
int M=Get(l,r);
if(M-l<=r-M){
for(int i=l;i<=M;i++){
int z=lower_bound(vis+1,vis+1+tot,dis[M]/dis[i])-vis;
while(z>=0&&vis[z]>dis[M]/dis[i]) z--;
int p=T.query_add(Root[r],1,tot,1,z);
p-=T.query_add(Root[M-1],1,tot,1,z);
// wk(-i),wh(p);
ans+=p;
}
}else{
for(int i=M;i<=r;i++){
int z=lower_bound(vis+1,vis+1+tot,dis[M]/dis[i])-vis;
while(z>=0&&vis[z]>dis[M]/dis[i]) z--;
int p=T.query_add(Root[M],1,tot,1,z);
// wk(p),wh(lower_bound(vis+1,vis+1+tot,dis[M]/dis[i])-vis);
p-=T.query_add(Root[l-1],1,tot,1,z);
// wk(-i),wh(p);
ans+=p;
}
}//wk(l),wk(r),wh(M);
//wh(ans);
solve(l,M-1),solve(M+1,r);
}
signed main()
{
read(n);
for(int i=1;i<=n;i++) read(dis[i]),f[i][0]=i,vis[i]=dis[i];
for(int j=1;j<=__lg(n);j++)
for (int i=1;i<=n-(1<<j)+1;i++)
f[i][j]=(dis[f[i][j-1]]>dis[f[i+(1<<(j-1))][j-1]])?f[i][j-1]:f[i+(1<<(j-1))][j-1];
sort(1+vis,1+vis+n);
tot=unique(1+vis,1+vis+n)-vis-1;
T.build(Root[0],1,tot);
for(int i=1;i<=n;i++) Root[i]=Root[i-1],T.ADD(Root[i],1,tot,(int)(lower_bound(vis+1,vis+1+tot,dis[i])-vis));
solve(1,n);wh(ans);
return 0;
}
本文来自博客园,作者:Red_river_hzh,转载请注明原文链接:https://www.cnblogs.com/Red-river-hzh/p/19491428

浙公网安备 33010602011771号