首页 > 其他分享 >#5. 可持久化线段树

#5. 可持久化线段树

时间:2024-11-14 17:32:26浏览次数:1  
标签:持久 rs int 线段 tr ls

请先学习线段树的相关内容喵。线段树博客待填

可持久化线段树

0x01. 简介

OI Wiki上的神秘定义:

函数式线段树 是指使用函数式编程思想的线段树。在函数式编程思想中,将计算机运算视为数学函数,并避免可改变的状态或变量。不难发现,函数式线段树是 完全可持久化 的。

可持久化线段树,顾名思义就是支持维护每次修改前历史版本,可以相同时间复杂度查询的线段树。

0x02. 如何变得‘持久’

一个朴素的想法是对于每次修改新建一棵线段树去存,那么就变成了维护线段树森林。

空间复杂度 \(O(qn\log\ n)\) ,炸飞了。


重新考虑实际中每次修改,从根节点递归到叶子节点实际上只修改了树上的一条链,如图:

那么要想维护历史版本上所有信息,每次就会增加 \(O(\log\ n)\) 个点。
而对于每次没有修改的节点,我们可以直接把修改过的节点直接'嫁接'其上,通过重复利用的方式最大限度节省空间,即:

可以发现,从每个根节点开始遍历形成的二叉树都与上文朴素想法中的二叉树完全一致,但是长得好毒瘤啊

0x03. 具体实现与代码

对于每个新版本,我们进行以下操作:

  1. 新建一个根节点,先继承上个版本根节点的所有信息
继承根节点实现
int clone(int u) {
    tr[++root] = tr[u];
    ls[root] = ls[u];
    rs[root] = rs[u];
    return root;
}
u = clone(u);
  1. 递归修改,修改的左、右儿子信息,记录到新开的节点上
递归修改实现
int mid = l + r >> 1;
if(loc <= mid) ls[u] = upd(ls[u], l, mid, loc, val);    
else rs[u] = upd(rs[u], mid + 1, r, loc, val); 

对于查询操作,直接在指定的版本递归向下找就完了。

注意注意注意:由于可持久化线段树的结构整体上不再是二叉树,而是一棵有很多很多根节点的奇形怪状的树,这意味着你不再能用形如 u<<1u<<1|1u*2u*2+1 来访问 u 的左儿子和右儿子。可以再开两个数组 lsrs 分别存左右儿子来解决。实际上,这种方式即为'动态开点'。

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

就是板子题,维护支持单点修改和历史单点查询的可持久化线段树。

AC代码:

#include<bits/stdc++.h>
using namespace std;

inline int read() {
	int f = 1, otto = 0;
	char a = getchar();
	while(!isdigit(a)) {
		if(a == '-') f = -1;
		a = getchar();
	}
	while(isdigit(a)) {
		otto = (otto << 1) + (otto <<3) + (a ^ 48);
		a = getchar(); 
	}
	return f * otto;
}

const int maxn = 1e6 + 10;
int a[maxn], rt[maxn], ls[maxn << 5], rs[maxn << 5], tr[maxn << 5];

int root;
int build(int u, int l, int r) {
	u = ++root;
	if(l == r) {
		tr[u] = a[l];
		return u;
	}
	int mid = l + r >> 1;
	ls[u] = build(ls[u], l, mid);
	rs[u] = build(rs[u], mid + 1, r);
	return u;
}

int clone(int u) {
	tr[++root] = tr[u];
	ls[root] = ls[u];
	rs[root] = rs[u];
	return root;
}

int upd(int u, int l, int r, int loc, int val) {
	u = clone(u);
	if(l == r) {
		tr[u] = val;
		return u;
	}
	int mid = l + r >> 1;
	if(loc <= mid) ls[u] = upd(ls[u], l, mid, loc, val);
	else rs[u] = upd(rs[u], mid + 1, r, loc, val); 
	return u;
}

int ask(int u, int l, int r, int loc) {
	if(l == r) return tr[u];
	int mid = l + r >> 1;
	if(loc <= mid) return ask(ls[u], l, mid, loc);
	else return ask(rs[u], mid + 1, r, loc);
}

int main() {
	int n = read(), m = read();
	for(int i = 1; i <= n; i++) a[i] = read();
	rt[0] = build(1, 1, n);
	
	for(int i = 1; i <= m; i++) {
		int ver = read(), op = read(), loc = read();
		if(op == 1) {
			int val = read();
			rt[i] = upd(rt[ver], 1, n, loc, val);
		}
		if(op == 2) {
			printf("%d\n", ask(rt[ver], 1, n, loc));
			rt[i] = rt[ver];	//克隆指定版本 
		}
	}
} 

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

给定序列和 \(m\) 次询问,求静态区间第 \(k\) 小,也就是传说中的'主席树'。
简单来说:

  1. 维护一棵在值域上的可持久化线段树,每个节点维护区间值域内序列中的数的个数,序列中的数需要离散化。
  2. 直接查询前缀 \(r\) 减去 \(l - 1\) 的第 \(k\) 小即为答案。

AC代码:

#include<bits/stdc++.h>
using namespace std;

inline int read() {
	int f = 1, otto = 0;
	char a = getchar();
	while(!isdigit(a)) {
		if(a == '-') f = -1;
		a = getchar();
	}
	while(isdigit(a)) {
		otto = (otto << 1) + (otto <<3) + (a ^ 48);
		a = getchar(); 
	}
	return f * otto;
}

const int maxn = 2e5 + 10;
int n, m, num, ver;
int a[maxn], b[maxn], rk[maxn];
int rt[maxn], tr[maxn << 5], ls[maxn << 5], rs[maxn << 5];

void upd(int &u1, int u2, int l, int r, int loc) {
	u1 = ++ver, tr[u1] = tr[u2] + 1;
	if(l == r) return;
	int mid = l + r >> 1;
	if(loc <= mid) rs[u1] = rs[u2], upd(ls[u1], ls[u2], l, mid, loc);
	else ls[u1] = ls[u2], upd(rs[u1], rs[u2], mid + 1, r, loc);
}

int ask(int u1, int u2, int l, int r, int k) {
	if(l == r) return l;
	int mid = l + r >> 1;
	int res = tr[ls[u1]] - tr[ls[u2]];
	if(res >= k) return ask(ls[u1], ls[u2], l, mid, k);
	return ask(rs[u1], rs[u2], mid + 1, r, k - res);
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= n; i++) a[i] = read(), b[i] = a[i];
	sort(b + 1, b + n + 1);
	num = unique(b + 1, b + n + 1) - b - 1;
	for(int i = 1; i <= n; i++) rk[i] = lower_bound(b + 1, b + num + 1, a[i]) - b, upd(rt[i], rt[i - 1], 1, num, rk[i]);
	for(int i = 1; i <= m; i++) {
		int l = read(), r = read(), k = read();
		printf("%d\n", b[ask(rt[r], rt[l - 1], 1, num, k)]);
	}
	return 0;
}

???为什么这样是正确的
细想一下:

  1. Q:为什么要用可持久化线段树?
    A:相当于对序列上的每个前缀维护一棵值域线段树,每次 upd() 在上一个版本的基础上更新。这样一来维护的信息就是前缀和的形式,也就可以直接用 \(r\) 版本减去 \(l-1\) 版本来查询了。
  2. Q:查询时为什么直接用 int res = tr[ls[u1]] - tr[ls[u2]]; ,即左儿子值域区间内的数的个数之差来作为二分的依据?
    A:相当于二分时先判断 midl 的关系。求区间第 \(k\) 大,即要找到一个值域区间刚好有 \(k\) 个数的最右端。所以 res 大于等于 k 时,接着二分左儿子值域区间; res 小于 k 时,把 k 减去 res 再二分右儿子值域区间。这里同样是在前缀上进行的查询。

0x04. 小结

可持久化线段树作为线段树的升级版,的确具有很强的启发作用。像类似主席树的'前缀和'思想,和可持久化线段树本身重复利用线段树上同一部分的思想,也揭示了数据结构'无穷无尽'的可能性。

\(THE\ \ \ END\)

标签:持久,rs,int,线段,tr,ls
From: https://www.cnblogs.com/Ydoc770/p/18543224

相关文章

  • 线段树
    线段树题目:https://www.acwing.com/problem/content/1277//*题目:https://www.acwing.com/problem/content/1277/给定一个正整数数列a1,a2,…,an,每一个数都在0∼p−1之间。可以对这列数进行两种操作:添加操作:向序列后添加一个数,序列长度变成n+1;询问......
  • P8868 [NOIP2022] 比赛(线段树维护区间历史和)
    题意给定排列\(a,b\),\(q\)次询问\(l,r\),你需要求出\(\sum_{l\lel'\ler'\ler}(\max_{i=l'}^{r'}a_i)(\max_{i=l'}^{r'}b_i)\)对\(2^{64}\)取模的值。\(n,q\le2.5\times10^5\)分析根据经典套路,按\(r\)扫描线,维护两个单调栈,那么加入一个数就相当于进行若干段区......
  • 用一个项目把控制层、业务层、持久层将明白了,每一句话都讲的很清楚
    实现一个数据库和前端的交互三层结构持久层开发:依据前端页面的设置规划相关的sql语句,以及进行配置业务层开发:核心功能控制、业务操作以及异常的处理控制层开发:前后端连接,接受请求,处理响应完整的数据响应流程如下:前端发起请求:前端通过浏览器或其他客户端发起HTT......
  • 可持久化线段树(主席树)
    主席树作为最常用的可持久化数据结构,广泛运用与各种区间、树上问题的在线求解已经对DP的优化上。这里主要讨论其单纯作为数据结构的应用。P1972[SDOI2009]HH的项链这是一道极其经典的题——静态区间种类数,其变体非常多,树上的,待修的,强制在线的等等。这题做法也很多样,离线后......
  • 【一步步开发AI运动小程序】二十一、如果将AI运动项目配置持久化到后端?
    说明:本文所涉及的AI运动识别、计时、计数能力,都是基于云智「Ai运动识别引擎」实现。云智「Ai运动识别」插件识别引擎,可以为您的小程序或UniAPP赋于原生、本地、广覆盖、高性能的人体识别、姿态识别、10余种常见的运动计时、计数识别及自定义扩展运动识别能力。完善的文档、Demo......
  • 【模板】可持久化线段树 2(洛谷P3834)
    #include<bits/stdc++.h>#defineendl'\n'usingll=longlong;typedefunsignedlonglongull;usingnamespacestd;voidGordenGhost();constexprintN=2e5+7;intn,m,a[N],b[N];introot[N],tot;//根节点所有节点个数intls[N*40],rs[N*40],sum......
  • 安卓手机剪贴板数据持久化(MacroDroid教程)
    本教程将使用MacroDroidApp实现对安卓手机上复制过数据进行持久化保存,并能快速查看已保存的内容。MacroDroid软件介绍MacroDroid是一款功能强大的自动化应用程序,可帮助用户通过创建宏(macros)来自动化他们的Android设备上的各种任务和操作。用户可以使用MacroDroid应用......
  • 可持久化线段树
    少写了一点,可持久化的好处就是可以用较低的代价去得到可以变换版本这一功能。可持久化线段树(主席树)带注释的代码/*注意,可持久化线段树很难支持区间修改,一般涉及区间修改的时候不用单点修改是可以的一样,直接选这题不大好,看下面的通用模版,具有通......
  • 六、MyBatis-Plus高级用法(1):最优化持久层开发
    一、MyBatis-Plus快速入门1.1简介课程版本:3.5.3.1MyBatis-Plus......
  • Redis中的持久化
    什么是Redis持久化?        Redis是一个内存数据库,也就是说它主要把数据存储在内存中,这样可以实现非常高的读写速度。通常,内存数据库是非常快速且高效的,但它也有一个很大的问题:数据丢失的风险。因为当Redis服务关闭或系统崩溃时,所有存储在内存中的数据都将丢失。......