前言
之前我也会使用主席树,但是都是用别人的模板。我觉得网上的主席树的写法很奇怪已经不是一天两天了,今天我特意写了一种主席树的实现,保留了较多的线段树写法。准备水一篇博客顺便把代码贴上来备用。从现在开始我就有自己的主席树模板了。
可持久化权值线段树
这是主席树的大名,他是线段树的一种可持久化实现。它维护的是值域,且在对树中信息进行修改时可以保留一份历史版本,在算法竞赛中经常出现。
想要保存一个树的历史版本,最简单的办法就是每进行一次修改,就另外新开一棵线段树。但这样会造成时空大爆炸。所以要寻求优化。
众所周知,权值线段树的叶节点,维护的是一个单个值的出现次数。例如下图中的红色结点,维护的是整个数组中数字 3 的个数。
当我们想插入一个 3,受到影响的,实际上只有包含 3 的区间,其他区间是不受影响的,体现在树上就是,只有从根节点到
[
3
,
3
]
[3, 3]
[3,3]这个叶节点之间路径上的点被改变了。因此我们可以利用已有的信息,只对这个路径上的点另建新的结点,这样每次修改,只需要新建
log
n
\log{n}
logn 个结点,大大节省了空间。
下图中红色结点,为新建结点。新建节点是旧结点的副本,因此连接方式与旧结点相同,从图中可以看出,从新根节点向下,形成了一棵新的权值线段树。
(轻点骂我知道图丑,但意思到位了)。
实现代码
显然实现可持久化线段树需要动态开点,我们一般取内存池大小为 M A X N MAXN MAXN 的 40 倍,这个数字可以凭借经验变动,也因题而异。具体实现在代码里(详细注释)。
#include <cstdio>
#include <algorithm>
#include <map>
#define MAXN 200010
using namespace std;
struct t_Tree {
int L, R; //结点维护的区间范围
int lc, rc; //结点的左右子结点编号
int num; //注意和线段树的区别 由于动态开点 子结点编号不再是2n和2n+1了
};
t_Tree tree[40*MAXN]; //刚才说的40倍
int root[MAXN], cnt; //每个版本的根节点在内存池中的编号 cnt内存池计数器
int N, M, A, a, b, c;
void pushup(int newnode) //同线段树pushup
{
tree[newnode].num = tree[tree[newnode].lc].num+tree[tree[newnode].rc].num;
}
int Build(int node, int l, int r)
{
node = ++cnt; //申请新节点
tree[node].L = l;
tree[node].R = r;
if(l==r)
{
tree[node].num = 0;
return node;
}
int mid = (l+r)/2;
tree[node].lc = Build(tree[node].lc, l, mid); //注意和线段树的区别
tree[node].rc = Build(tree[node].rc, mid+1, r);
return node;
}
int update(int node, int x) //在主席树中插入新元素x 同时产生一个新版本
{
int newnode = ++cnt; //申请新节点 备用
tree[newnode].L = tree[node].L; //新节点的表示区间范围和旧结点一样
tree[newnode].R = tree[node].R;
if(tree[node].L==tree[node].R)
{
tree[newnode].num = tree[node].num+1;
return newnode;
}
int mid = (tree[node].L+tree[node].R)/2;
if(x<=mid) //向左子节点修改 那么左子节点需要一个副本 右子节点直接连上
{
tree[newnode].lc = update(tree[node].lc, x);
tree[newnode].rc = tree[node].rc;
}
else //向右子节点修改 那么右子节点需要一个副本 左子节点直接连上
{
tree[newnode].lc = tree[node].lc;
tree[newnode].rc = update(tree[node].rc, x);
}
pushup(newnode);
return newnode;
}
int query(int node, int k) //和线段树几乎一样
{
if(tree[node].L==tree[node].R)
return tree[node].num;
int mid = (tree[node].L+tree[node].R)/2;
if(k<=mid)
return query(tree[node].lc, k);
else
return query(tree[node].rc, k);
}
int main()
{
scanf("%d%d", &N, &M);
root[0] = Build(root[0], 1, N); //新建一棵空树 "版本0" 似乎其实可以省略?
for(int i=1;i<=N;i++)
{
scanf("%d", &A);
root[i] = update(root[i-1], A);
}
for(int i=1;i<=M;i++)
{
scanf("%d%d%d", &a, &b, &c);
printf("%d\n", query(root[b], c)-query(root[a-1], c));
} //版本b中c出现的次数 减去版本a中c出现的次数 就等于[a, b]这个区间 c出现的次数
return 0;
}
经典问题
可持久化线段树也是维护区间信息的,它解决的问题也和区间有关。
问题一:多次询问一个区间 [ L , R ] [L, R] [L,R]内,某个数 x x x 出现的次数。
对权值线段树熟悉的人瞬间就能看出来。因为权值线段树维护的信息本身就是数字出现的次数。但是经过可持久化之后,我们要考虑怎么得到我们想要的那棵线段树。答案就是利用前缀和的思想,用以 R R R 为根的线段树的值,减去用以 L − 1 L-1 L−1 为根的线段树的值,得到的就是维护 [ L , R ] [L, R] [L,R] 这个区间的信息。
代码已经在上面了。
问题二:静态区间第
K
K
K 小。
每次查询一个区间
[
L
,
R
]
[L, R]
[L,R] 内的第
k
k
k 小(大,本质一样)值。
还是前缀和思想,先找出表示 [ L , R ] [L, R] [L,R] 这段区间的树,从根节点向下找,每到达一个结点,先查询 [ L , m i d ] [L, mid] [L,mid] 中所有数字出现的次数,如果次数大于 k k k 说明第 k k k 大的数字一定在左子树里,反之则在右子树里。
int query(int rt1, int rt2, int k)
{
if(tree[rt1].L==tree[rt1].R)
return tree[rt1].L;
int temp = tree[tree[rt2].lc].num-tree[tree[rt1].lc].num;
if(k<=temp)
return query(tree[rt1].lc, tree[rt2].lc, k);
else
return query(tree[rt1].rc, tree[rt2].rc, k-temp);
}
例题
没啥用的题,就是让你感受一下可持久化这种思想:
P3919 【模板】可持久化线段树 1(可持久化数组).
静态区间第k小:
P3834 【模板】可持久化线段树 2(主席树).
一道正解并非主席树的主席树题:
2021年广东工业大学第十五届文远知行杯程序设计竞赛 E - 捡贝壳.
哈希+主席树:
西南科技大学2021届新生赛 A - 暗号I.