主席树(可持久化线段树)

10 篇文章 0 订阅
9 篇文章 0 订阅
本文详细介绍了可持久化线段树(又称主席树)的概念和实现,通过实例展示了如何用C++实现可持久化线段树,并探讨了其在数组更新和值域划分问题上的应用。此外,文章还提及了可持久化线段树与平衡树结合的高级应用,但未展开讨论。文章适合对数据结构有一定了解的读者,有助于提升在算法竞赛和数据结构学习中的能力。
摘要由CSDN通过智能技术生成

前言

真不是有目的地学习主席树的…(实在是因为它太上头了

《关于我某天第二节晚修一直在看<进阶指南>可持久化数据结构这章然后学了主席树这件事》

所以新学一个数据结构只要一整节晚修。

主席树

也叫可持久化线段树、函数式线段树。其思想与可持久化 T r i e \mathtt{Trie} Trie 相似。

其实,就是在普通线段树的基础上,修改了一下 u p d a t e \mathtt{update} update 操作,使得它成为可持久化数据结构:

每次新建一个根节点,保存此次修改之后的状态。在遍历线段树的时候,对更改了的部分创建一个副本,然后直接将孩子指针指向上一个状态中没更改的部分。

下面这张图展现了对 [ 4 , 4 ] [4,4] [4,4] 修改后树的状态。

(注:图自此主席树博客

不过主席树难以支持大部分的区间修改。原因是标记难以下传(后面有若干依赖此子树的树)。在一些特殊题目中,可以使用标记永久化代替标记的下传,例如:SP11470 TTM - To the moon

对数组下标的划分

P3919 【模板】可持久化线段树 1(可持久化数组)

本质上就是用线段树实现可持久化数组。

#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;
}

对值域的划分

P3834 【模板】可持久化线段树 2

离散化,然后对于每个根节点 r o o t i root_i rooti,它维护的是数组从 a 1 a_1 a1 a i a_i ai 中每个数的出现次数。

该算法时间复杂度为 O ( ( N + M ) l o g N ) O((N+M)logN) O((N+M)logN),空间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)

#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", &lt, &rtm, &kt);
		int ans = query(rt[lt - 1], rt[rtm], 1, q, kt);
		printf("%d\n", b[ans]);
	}
	return 0;
}

E m m m . . . Emmm... Emmm... 更有意思的是,这道题也可以使用线段树套平衡树的树套树做法,因为平衡树具有添加和删除的功能,所以这个做法支持动态修改。具体地…没写

关于对值域的划分,P4587 [FJOI2016]神秘数(题解)是道综合性较强的例题。


得赶紧跑路了,再搞主席树我平衡树就烂地里了,逃

—— E n d \mathfrak{End} End——

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值