主席树(可持久化线段树)
前言
真不是有目的地学主席树的...(实在是因为它太上头了)
《关于我某天第二节晚修一直在看<进阶指南>可持久化数据结构这章然后学了主席树这件事》
update
- 2022-05-23:添加一些题型的整理。
主席树
也叫可持久化线段树、函数式线段树。其思想与可持久化
其实,就是在普通线段树的基础上,修改了一下
每次新建一个根节点,保存此次修改之后的状态。在遍历线段树的时候,对更改了的部分创建一个副本,然后直接将孩子指针指向上一个状态中没更改的部分。
下面这张图展现了对
(注:图自此主席树博客)
不过主席树难以支持大部分的区间修改。原因是标记难以下传(后面有若干依赖此子树的树)。在一些特殊题目中,可以使用标记永久化代替标记的下传,例如:SP11470 TTM - To the moon。
对数组下标的划分
本质上就是用线段树实现可持久化数组。
#include<bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for(int i = a; i <= b; ++i)
const int maxn = 1e6 + 5;
int n, m;
int a[maxn], rt[maxn];
struct node{
int l, r;
int val;
}t[maxn << 5];
int tot;
inline int build(int nw, int l, int r)
{
nw = ++tot;
if(l == r)
{
t[nw].val = a[l];
return nw;
}
int mid = (l + r) >> 1;
t[nw].l = build(t[nw].l, l, mid);
t[nw].r = build(t[nw].r, mid + 1, r);
return nw;
}
inline int cpy(int x)
{
int nw = ++tot;
t[nw] = t[x];
return nw;
}
inline int update(int lst, int l, int r, int k, int d)
{
int nw = cpy(lst), mid = (l + r) >> 1;
if(l == r)
t[nw].val = d;
else
{
if(k <= mid)
t[nw].l = update(t[nw].l, l, mid, k, d);
else t[nw].r = update(t[nw].r, mid + 1, r, k, d);
}
return nw;
}
inline int query(int nw, int l, int r, int k)
{
if(l == r)
return t[nw].val;
else
{
int mid = (l + r) >> 1;
if(k <= mid)
return query(t[nw].l, l, mid, k);
else return query(t[nw].r, mid + 1, r, k);
}
}
int main()
{
scanf("%d%d", &n, &m);
rep(i, 1, n) scanf("%d", &a[i]);
rt[0] = build(531, 1, n);
rep(i, 1, m)
{
int v, opt, loc, d;
scanf("%d%d%d", &v, &opt, &loc);
if(opt == 1)
{
scanf("%d", &d);
rt[i] = update(rt[v], 1, n, loc, d);
}
else
printf("%d\n", query(rt[v], 1, n, loc)),
rt[i] = rt[v];
}
return 0;
}
对值域的划分
离散化,然后对于每个根节点
该算法时间复杂度为
#include<bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for(int i = a; i <= b; ++i)
const int maxn = 2e5 + 5;
int n, m;
struct node{
int ls, rs;
int sum;
}t[maxn << 5];
int a[maxn], b[maxn];
int q;
int dt, tot;
int rt[maxn];
inline void build(int &nw, int l, int r)
{
nw = ++tot;
if(l == r) return;
int mid = (l + r) >> 1;
build(t[nw].ls, l, mid), build(t[nw].rs, mid + 1, r);
}
inline int addt(int lst, int l, int r)
{
int nw = ++tot;
t[nw] = t[lst], t[nw].sum += 1;
if(l == r) return nw;
int mid = (l + r) >> 1;
if(dt <= mid) t[nw].ls = addt(t[nw].ls, l, mid);
else t[nw].rs = addt(t[nw].rs, mid + 1, r);
return nw;
}
inline int query(int pl, int pr, int l, int r, int k)
{
if(l == r) return l;
int mid = (l + r) >> 1, lcnt = t[t[pr].ls].sum - t[t[pl].ls].sum;
if(k <= lcnt)
return query(t[pl].ls, t[pr].ls, l, mid, k);
else return query(t[pl].rs, t[pr].rs, mid + 1, r, k - lcnt);
}
int main()
{
scanf("%d%d", &n, &m);
rep(i, 1, n)
scanf("%d", &a[i]), b[i] = a[i];
sort(b + 1, b + n + 1);
q = unique(b + 1, b + n + 1) - b - 1;
build(rt[0], 1, q);
rep(i, 1, n)
{
dt = lower_bound(b + 1, b + q + 1, a[i]) - b;
rt[i] = addt(rt[i - 1], 1, q);
}
rep(i, 1, m)
{
int lt, rtm, kt;
scanf("%d%d%d", <, &rtm, &kt);
int ans = query(rt[lt - 1], rt[rtm], 1, q, kt);
printf("%d\n", b[ans]);
}
return 0;
}
没写
关于对值域的划分,P4587 [FJOI2016]神秘数(题解)是道综合性较强的例题。
其他有趣的题型
1. 树状数组套主席树
此题先是运用了纯树状数组求逆序对的方法求出不带删去的逆序对个数(此方法《Tricks 整理》中有收录)。
其他细节见此篇题解。
每一次删除了一个数之后,我们要减去它和其它数组成的逆序对个数,加回和已经删除的数构成的逆序对的个数(前面统计过了)。
所以每次在加回的过程中,就变成在已删除的元素中,下表在
乍一看好像套一个值域上的主席树就可以了,但是在具体实现的过程中发现对于当前新添加的一个数,它的树根是
再加上我们要访问一个连续区间,单单用主席树去维护似乎变得不可行了。
所以此时我们要在主席树上套一个树状数组,用前缀和的思想去解决。
具体实现就是每次
但是!!这样我们会发现对于每一个
2. 按照 dfs 序建立主席树
对于每次询问,分两类情况讨论,要么
后者显然容易处理,
前者相对困难。发现
考虑如何求出合法的
依靠这个,我们可以建立一个主席树:
对于每一个节点,有
那“a 的子树”这个限制如何转化?打破定式思维,我们在 dfs 的过程中给每个点一个 dfs 序,然后按照这个 dfs 序去建树,所以,此时我们询问的范围就变成了
此时也不难想到对于主席树内的节点的权值就设为其对应点的
3. 主席树在值域上维护求中位数
这个相对于前两种就简单多了,不过此题的重点在与求解不定区间的中位数。
在主席树这方面特别要注意的是,对于一棵根为
需要这个标记的前提是主席树内的一棵树
其他细节见【LG-P2839 [国家集训队]】middle 题解
4. 可持久化并查集
辛酸血泪史, 说白了就是 可持久化数组 + 并查集。
注意此处的并查集不需要路径压缩,因为主席上进行更改的复杂度是
此外还要注意的就是对于一个节点
具体见代码。
5. 标记永久化
回过头去做标记永久化,发现比想象中要简单。
标记永久化,顾名思义,标记不需要下传,主要用于主席树上的区间修改。
对被修改区间覆盖的节点打上标记,其左右子节点继承上个版本的左右子节点。
(图自此博客)
这样可以保证修改不影响公共节点了。具体看代码。
这里放一些重点:
- 区间修改:
inline void update(int &x, int lst, int l, int r, int L, int R, int d){
if(!x) x = ++tot;
t[x] = t[lst], t[x].sum += d * (min(R, r) - max(l, L) + 1);
if(l >= L and r <= R){
t[x].tag += d;
return;
}
int mid = l + r >> 1;
if(L <= mid) ls = 0, update(ls, t[lst].l, l, mid, L, R, d);
if(R > mid) rs = 0, update(rs, t[lst].r, mid + 1, r, L, R, d);
}
- 区间查询:
inline int query(int x, int ln, int rn, int L, int R){
if(ln >= L and rn <= R)
return t[x].sum;
int mid = ln + rn >> 1, s = 0;
if(L <= mid) s += query(ls, ln, mid, L, R);
if(R > mid) s += query(rs, mid + 1, rn, L, R);
return s + t[x].tag * (min(R, rn) - max(ln, L) + 1);
}
——
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)