[数据结构]--------可持久化线段树(主席树)

前言

之前我也会使用主席树,但是都是用别人的模板。我觉得网上的主席树的写法很奇怪已经不是一天两天了,今天我特意写了一种主席树的实现,保留了较多的线段树写法。准备水一篇博客顺便把代码贴上来备用。从现在开始我就有自己的主席树模板了。

可持久化权值线段树

这是主席树的大名,他是线段树的一种可持久化实现。它维护的是值域,且在对树中信息进行修改时可以保留一份历史版本,在算法竞赛中经常出现。

想要保存一个树的历史版本,最简单的办法就是每进行一次修改,就另外新开一棵线段树。但这样会造成时空大爆炸。所以要寻求优化。

众所周知,权值线段树的叶节点,维护的是一个单个值的出现次数。例如下图中的红色结点,维护的是整个数组中数字 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 L1 为根的线段树的值,得到的就是维护 [ 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(主席树).

HDU 2665 Kth number.

一道正解并非主席树的主席树题:
2021年广东工业大学第十五届文远知行杯程序设计竞赛 E - 捡贝壳.

哈希+主席树:
西南科技大学2021届新生赛 A - 暗号I.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值