可持久化线段树(主席树)详解(c++ 图片演示+ 代码)

news2024/11/18 19:54:39

文章目录

  • 可持久化线段树(主席树)
    • 建树前准备!
    • 初始化建树!
    • 更新操作!
    • 区间查询!
  • AC code

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

可持久化线段树指的是可持久化权值线段树,关于权值线段树可以看我这篇博客:

权值线段树详解+模板

下面我直接用主席树这个名称来介绍写可持久化权值线段树


引入:

对于一颗权值线段树,我们要往里面添加n个数字,我们知道,这很容易实现,只需要一个for循环,然后调用n次update函数即可(update函数具有添加元素的功能,详见《权值线段树博文》)。

但是如果需要一个记忆化的过程,即我们每添加一个元素都需要记住这个权值线段树的状态,以便于我们对历史的操作,那么如果只使用权值线段树,则我们需要n个权值线段树同时保存状态,对于非常多的元素,我们就需要保存非常多的权值线段树,这样做空间一定会爆炸,有没有什么办法可以使我们有效的保存历史的状态呢?? 主席树提供了这一功能


先来看看每次修改或者插入一个元素往权值线段树,这个权值线段树的每个时刻的状态吧:

  • 我们以插入元素 [1,2,3,4,3] 为例吧

这是元素逐一插入的过程:

在这里插入图片描述
在这里插入图片描述

这样我们就完成了五个权值线段树的构建构成,但是我们发现,貌似每次的插入一个元素,修改的线段树的结构只与这个插入的元素有关

  1. 每次修改只会是添加的值到根节点的一条链上的值发生了变化(如红色链所示),而其它的节点和上次修改结束后的都是一样的。
  2. 我们貌似不用每次都新建一个权值线段树,直接新建一条表示值发生变化的链不就好了

只添加链,不适用新建权值线段树的方式创建的主席树:

  • 这种只添加链的方式构建的线段树就是主席树

在这里插入图片描述
恶心!!!!!!!!!!!!!!!!!!!!!!!!!

不要惊慌!!!!!!! 这就是只添加链的主席树。

其实还是很清晰的,每次找到要修改的值所会影响的节点链,然后单独添加一颗链就好了。


观察这颗主席树,我们可以发现几个性质:

  • 新添加的节点链条上:叶子节点不会连接原有节点,非叶子节点一定是一端连接新的链条节点,另一端连接原有节点
  • 每次添加的节点链条的长度(深度)是logN
  • 主席树有很多根,图中有5个,每一个根都可以单独形成一个新的权值线段树

因此,主席树就是一个可以保存历史的权值线段树,如何访问历史呢? 我们前面提到过,每一个根可以组成一个单独的线段树,而每一个根节点都可以用一个唯一的编号表示。用一个root数组存储所有历史中的根节点。

主席树只会对部分节点进行复制,并且每一次复制的节点个数是log(n)。我们每一次想询问一个版本的线段树,就可以在那个版本的根构成的线段树(使用root获取版本)中询问。


主席树对于处理任意区间第k大/小这种问题具有非常明显的优势,前面我们说过权值线段树也可以处理区间第k大,但是权值线段树必须是整个区间的第k大,对于任意区间无法得出答案

我们以例题为例,说明主席树的代码实现:区间第k小的元素


来看看代码实现:

建树前准备!

注意:

  • btree表示我们的主席树的每一个节点,其中 l 和 r 表示左右孩子节点的编号,而不是 l2 和 l2+1,val表示数字出现的次数 (请注意,这个地方非常重要)
  • 对于N个历史,假设N为 1e6 ,则最多会存在 N+ NlogN 个节点,则我们最好这个主席树弄成 N的32倍以上。
  • root存储每一个根节点编号,top表示顶层编号
int n, m;
const int N = 1e6 + 10;
int nums[N<<2], top, root[N<<2];
struct node
{
	int l, r, val;
	friend ostream& operator<<(ostream& os, node& p)
	{
		//dbg
		os << p.l << " " << p.r << " " << p.val << endl;
		return os;
	}
}btree[N*40]; //N+N(log2(N))=3kw ...

初始化建树!

注意我们的每个节点的区间 l,r需要进行赋值!!!,通过递归与回溯可以完成这一过程,其他过程与普通线段树一致

另外node表示当前节点的编号,随着递归与回溯起到给节点的 l,r赋值的作用。还可以是这样的写法:把node参数设置为引用,则build就是void了,直接在参数内修改,我比较喜欢按返回值的形式来写。

划重点!!!!!!!!!!!!!!!!!!!!
关于主席树的节点 l 和 r的值,有必要再说明以下:(假设的图,与题无关)

  • 红色: val值
  • 绿色:节点的 l 和 r值
  • 黑色数字: 节点编号
  1. 关于根节点的 l 和 r 分别是2编号和7编号,其中2为左孩子的编号,7为右孩子的编号
  2. 对于左孩子2也同理,3和6分别表示其左右孩子的编号
  3. 叶子节点没有 l 和 r 编号

在这里插入图片描述

这是怎么形成的呢???就是通过 node 的递归与回溯实现的

//初始化建树
int build(int node,int pl,int pr)
{
    node=++top;
    tree[node].val=0;
    if (pl==pr)
    {
        return node;    //返回当前节点编号,传递给父节点
    }
    int mid=(pl+pr)>>1;
    tree[node].l=build(node,pl,mid);    //递归左子树,完成当前节点l的赋值
    tree[node].r=build(node,mid+1,pr);  //递归右子树,完成当前节点r的赋值
    return node;    //返回当前当前编号,传递给父节点
} 

更新操作!

  • 注意到clone函数,我们必须新建一条节点链,而不是整个权值线段树,因此我们必须复制每一个原有的节点,还需要注意复制的过程是递归中进行的
  • 另外关于clone,你是否觉得它放在第一行会不会造成整颗线段树全都复制了??? 这其实是不可能造成的,请看下面的递归部分,当修改的loc位置在某一个区间中,则会递归到这个节点,只要是递归到了这个区间,则这个区间一定是会被修改的,所以说也一定是需要新建节点的(请认真理解这句话),所以clone函数完成了对每一个需要修改的节点形成了一条新的节点链条,而不是整棵树。
inline int clone(int pre)
{
    /*从原始节点复制一个节点数据*/
    //pre为原有节点的编号
    ++top;
    tree[top].l=tree[pre].l;
    tree[top].r=tree[pre].r;
    tree[top].val=tree[pre].val+1;		//这个元素出现的次数+1
    return top; //返回新建的节点的编号
}
  • loc:表示修改的节点位置
  • clone后新建了一个节点,则返回这个新的节点编号,此时要完成对这个新的节点 l,r的更新操作,即有的l不需要更新(未修改的),有的r需要更新为新的区间(修改的)
//更新
int update(int pre,int pl,int pr,int loc)
{
    int cur=clone(pre); //新建一个节点,同时返回这个新的节点编号
    if (pl==pr)
    {
        return cur; //返回当前节点编号
    }
    int mid=(pl+pr)>>1;
    if (loc<=mid)
    {
        //完成当前节点的l的update
        tree[cur].l=update(tree[cur].l,pl,mid,loc);
    } 
    else
    {
        //完成当前节点r的update
        tree[cur].r=update(tree[cur].r,mid+1,pr,loc);
    } 
    return cur; //返回当前节点编号,实现对子根节点的lr赋值
}

更新操作示意图:

在这里插入图片描述


区间查询!

发明者的原话:“对于原序列的每一个前缀[1···i]建立出一棵线段树维护值域上每个数出现的次数,则其树是可减的”

可以加减的理由:主席树的每个节点保存的是一颗线段树,维护的区间信息,结构相同,因此具有可加减性

详细查询过程: 原始区间: [1,2,3] 查询 1到3 里的第二小的值(应该为2)

  • 我们查询 l=1 r=3 k=2 因此这样查询:query(sum[r]) - query(sum[l-1])
  1. 首先进入query函数,当前为12编号根节点,前一个为1编号根节点。对于编号12的根节点的左孩子编号为10,编号为1的根节点左孩子是2,让sum[10]-sum[2],sum记录的是此节点的val,相减得2,因为k=2,所以进入左子树递归。
  2. 当前为10编号根节点,前一个为2编号根节点。对于编号10的根节点的左孩子编号为8,编号为2的根节点左孩子是3,让d=(sum[8]-sum[3]),sum记录的是此节点的val,相减得1,因为k=2,所以进入右子树递递归,同时 k - d
  3. 当前为11编号根节点,前一个为4编号根节点。此时已经到达了叶子节点,返回 pl或者pr,得结果为 2(此时pl== pr==2,达到编号为4的叶子节点)

在这里插入图片描述

//查询
int query(int pre,int cur,int pl,int pr,int k)
{
    int ans=0;
    //sum[lc[cur]]-sum[lc[pre]]
    int L1=tree[cur].l;    //当前节点的l位置
    int L2=tree[pre].l;    //当前节点的pre时刻的l位置
    //通过两个时刻同一位置的val的相减,得到这个节点所包含的元素个数
    //即表示了在[pl,pr]查询区间内,有多少个元素位于这个节点的子区间内
    int num=tree[L1].val-tree[L2].val; 
    if (pl==pr)
    {
        return pl;  //到达叶子节点,叶子节点是我们查询的节点,直接返回pl或者pr都行
    }
    int mid=(pl+pr)>>1;
    if (num>=k)
    {
        //递归查询左子树
        return query(tree[pre].l,tree[cur].l,pl,mid,k);
    } 
    else
    {
        //递归查询右子树
        return query(tree[pre].r,tree[cur].r,mid+1,pr,k-num);
    } 
}

AC code

#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using DB = double;
using PI = pair<int, int>;
using PL = pair<LL, LL>;
template<typename T> using v = vector<T>;
constexpr auto INF = 0X3F3F3F3F;
template<typename T1, typename T2> using umap = unordered_map<T1, T2>;
#define ic std::ios::sync_with_stdio(false);std::cin.tie(nullptr)
template <typename ConTainermap> void dbgumap(ConTainermap c);	//output umap
template <typename _Ty> void dbg(_Ty nums[], int n);
#if 1
#define int LL
#endif
inline int read();			//fast input
inline void write(int x);	//fast output

//TODO: Write code here
int n, m;
const int N = 2e5 + 10;
int nums[N << 2], top, temp[N << 2], root[N << 2];
struct node
{
	int l, r, val;
}tree[N << 5];
inline int clone(int pre)
{
	/*从原始节点复制一个节点数据*/
	//pre为原有节点的编号
	++top;
	tree[top].l = tree[pre].l;
	tree[top].r = tree[pre].r;
	tree[top].val = tree[pre].val + 1;
	return top; //返回新建的节点的编号
}
//初始化建树
int build(int node, int pl, int pr)
{
	node = ++top;
	tree[node].val = 0;
	if (pl == pr)
	{
		return node;    //返回当前节点编号,传递给父节点
	}
	int mid = (pl + pr) >> 1;
	tree[node].l = build(node, pl, mid);    //递归左子树,完成当前节点l的赋值
	tree[node].r = build(node, mid + 1, pr);  //递归右子树,完成当前节点r的赋值
	return node;    //返回当前当前编号,传递给父节点
}
//更新
int update(int pre, int pl, int pr, int loc)
{
	int cur = clone(pre); //新建一个节点,同时返回这个新的节点编号
	if (pl == pr)
	{
		return cur; //返回当前节点编号
	}
	int mid = (pl + pr) >> 1;
	if (loc <= mid)
	{
		//完成当前节点的l的update
		tree[cur].l = update(tree[cur].l, pl, mid, loc);
	}
	else
	{
		//完成当前节点r的update
		tree[cur].r = update(tree[cur].r, mid + 1, pr, loc);
	}
	return cur; //返回当前节点编号,实现对子根节点的lr赋值
}
//查询
int query(int pre, int cur, int pl, int pr, int k)
{
	int ans = 0;
	//sum[lc[cur]]-sum[lc[pre]]
	int L1 = tree[cur].l;    //当前节点的l位置
	int L2 = tree[pre].l;    //当前节点的pre时刻的l位置
	//通过两个时刻同一位置的val的相减,得到这个节点所包含的元素个数
	//即表示了在[pl,pr]查询区间内,有多少个元素位于这个节点的子区间内
	int num = tree[L1].val - tree[L2].val;
	if (pl == pr)
	{
		return pl;  //到达叶子节点,叶子节点是我们查询的节点,直接返回pl或者pr都行
	}
	int mid = (pl + pr) >> 1;
	if (num >= k)
	{
		//递归查询左子树
		return query(tree[pre].l, tree[cur].l, pl, mid, k);
	}
	else
	{
		//递归查询右子树
		return query(tree[pre].r, tree[cur].r, mid + 1, pr, k - num);
	}
}
signed main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		scanf("%lld", &nums[i]);
		temp[i] = nums[i];
	}
	sort(temp + 1, temp + 1 + n);
	int len = unique(temp + 1, temp + 1 + n) - temp - 1;
	root[0] = build(0, 1, len);   //构建空树,root[0]记录
	
	// 1 2 3 4 5
	// l r [3,5] 2
	for (int i = 1; i <= n; i++)
	{
	    /// @brief 离散化的时候!!!!!!!!!!!!!!!
	    /// @return lower_bound查找的是nuums[i]在temp中的位置,而temp的取值范围为[1,len+1],其中len为去重后的长度,不是原始的长度n!!!!!!!!!!!!!!!!!!
	    int loc = lower_bound(temp + 1, temp + 1 + len, nums[i]) - temp;
	    //root[i]记录每次更新后的新的树,其中每一棵树都是一个《权值线段树》
	    root[i] = update(root[i - 1], 1, len, loc);
	}
	int ans = 0;
	while (m--)
	{
	    int l, r, k;
	    scanf("%lld%lld%lld", &l, &r, &k);
	    //查询[l,r]区间,即完成root[r]-root[l-1]的计算
	    ans = query(root[l - 1], root[r], 1, len, k);
	    printf("%lld\n", temp[ans]);
	}
	return 0;
}
template <typename _Ty>
void dbg(_Ty nums[], int n)
{
	for (int i = 1; i <= n; i++)
	{
		cout << nums[i] << " ";
	}
	cout << endl;
}
template <typename ConTainermap>
void dbgumap(ConTainermap c)
{
	for (auto& x : c)
	{
		cout << "key:" << x.first << "  val:" << x.second << endl;
	}
}
inline int read()
{
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9')
	{
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
	{
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}
inline void output(int x)
{
	static int sta[35];
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	while (top) putchar(sta[--top] + 48);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/183903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mininal BASH-like line editing is supported.

翻译&#xff1a;支持类似Mininal bash的行编辑。对于第一个单词&#xff0c;TAB列出了可能的命令补全。“其他任何地方”标签列出可能的设备或文件补全。原因分析&#xff1a;出现这个问题的原因是windows启动的时候&#xff0c;没有找到启动文件。&#xff08;我的电脑之所以…

每天一道大厂SQL题【Day01】

每天一道大厂SQL题【Day01】 大家好&#xff0c;我是Maynor。相信大家和我一样&#xff0c;都有一个大厂梦&#xff0c;作为一名资深大数据选手&#xff0c;深知SQL重要性&#xff0c;接下来我准备用100天时间&#xff0c;基于大数据岗面试中的经典题&#xff0c;以每日1题的形…

5.6 奇异值

对单位圆的乘法 首先我们在单位圆上遍历所有的点&#xff0c;作为二维向量&#xff0c;来研究某个矩阵乘以这些向量得到的结果&#xff0c;我们选三种矩阵&#xff0c;秩为0的矩阵&#xff0c;秩为1的矩阵和秩为2的矩阵。   秩为0的矩阵就一个&#xff0c;也就是0矩阵&#x…

【页面设计】02.CSS

CSS&#xff1a;Cascading Style Sheets 层叠样式表1.不是编程语言。2.告诉浏览器如何指定样式、布局等一、基本格式效果&#xff1a;二、三种方式添加CSS1.外部样式表&#xff08;1&#xff09;CSS保存在.css文件中&#xff08;2&#xff09;在HTML的<head>中使用<li…

棕黑色L-CQDs/TiO2 离子液体修饰/500nm粒径氮硫掺杂碳量子点N,S-CQDs/ZnO的制备过程

棕黑色L-CQDs/TiO2 离子液体修饰/500nm粒径氮硫掺杂碳量子点N,S-CQDs/ZnO的制备过程 今天小编分享L-CQDs/TiO2 离子液体修饰碳量子点的制备过程&#xff0c;一起看看吧: L-CQDs/TiO2 离子液体修饰碳量子点的制备过程&#xff1a; 通过水热法制备L-CQDs/TiO2复合催化剂。将50 …

Harbor安装对接Containerd

使用docker-compose安装harbor 先决条件&#xff1a; 安装docker安装docker-compose 安装参考 下载并解压 wget -c https://github.com/goharbor/harbor/releases/download/v2.3.5/harbor-offline-installer-v2.3.5.tgztar -zxvf harbor-offline-installer-v2.3.5.tgz cd harbo…

Aurora、Chip2chip、Ethernet(一)

摘要&#xff1a;之前的文章对aurora、chip2chip以及Ethernet这三个IP都进行介绍、仿真和使用说明。但是在实际使用中一定没有那么简单&#xff0c;在复杂联合使用的情况下&#xff0c;肯定会碰到各种各样的问题。此系列文章主要说明如何解决联合使用情况下碰到的一系列问题。 …

操作系统(day02)

“指令”就是处理器&#xff08;CPU&#xff09;能识别、执行的最基本的命令也可以叫做机器指令 两种指令、两种处理器状态、两种程序 两种指令 特权指令 如内存清零指令&#xff0c;不允许用户程序使用非特权指令 如普通的运算指令 既然有两种指令&#xff0c;且特权指令不…

IB学习者培养目标-知识渊博

“We explore concepts, ideas and issues that have local and global significance. In so doing, we acquire in-depth knowledge and develop understanding across a broad and balanced range of disciplines.” -IB definition of the attribute Knowledgeable“Being …

C++ —— 容器适配器和仿函数

目录 1.什么是容器适配器 2.stack的模拟实现 3.queue的模拟实现 4.deque概述 5.priority_queue的模拟实现 5.1仿函数 5.2模拟实现 6.反向迭代器 1.什么是容器适配器 在已有的容器(vector、list)的基础上适配出其他的容器。就类似于手机、笔记本电脑的电源适配器&…

一些lc周赛

6285. 执行 K 次操作后的最大分数(327 贪心 优先队列模拟) Math.ceil(val) 向上取整函数 public long maxKelements(int[] nums, int k) {PriorityQueue<Integer> queuenew PriorityQueue<>((a,b)->(b-a));for(int n:nums){queue.add(n);}long sum0;for(int i0…

JavaScript高级 ES6新特性

ES6~ES13新特性1. ECMA新描述概念1. 概念区别回顾2. 词法环境3. 环境记录4. 内存图的表示2. let、const的使用1. 基础的使用2. 作用域提升3. 暂时性死区 (TDZ)4. window 添加属性的区别5. 块级作用域的使用6. var、let、const的选择3. 模板字符串的详解4. ES6函数的增强用法1. …

VSCODE 系列(七)格式化工具clang-format

文章目录一、VS Code中使用生成.clang-format文件VS Code设置参考一、VS Code中使用 VS Code 中自带clang-format.exe 生成.clang-format文件 使用命令 .\clang-format.exe -stylellvm -dump-config > .clang-format或者新建.clang-format文件&#xff0c;将自己的配置…

vscode firefox xdebug 安装及配置

一、安装 vscode扩展中招xdebug直接安装。 firefox扩展中找xdebug直接安装。 xdebug下载&#xff0c;以window为例。 根据配置选下载内容。 设置成和ide相同的 。 二、配置 文档地址&#xff1a;Xdebug: Documentation 所有配置说明&#xff1a;Xdebug: Documentation All …

.mp4 文件转化成 .bag 文件并在 rviz 中显示

文章目录一、Python实现.mp4和.bag相互转化1、.mp4转.bag验证是否转换成功&#xff1a;使用 rosplay2、.bag转.mp4二、rviz 读取 *.bag 数据包并显示1、查看bag数据包的基本信息2、rviz 显示信息一、Python实现.mp4和.bag相互转化 1、.mp4转.bag # -*- coding: utf-8 -*- ##i…

Webpack 的 Chunk,想怎么分就怎么分

想必大家都用过 webpack&#xff0c;也或多或少了解它的原理&#xff0c;但是不知道大家有没有写过 Webpack 的插件呢&#xff1f; 今天我们就一起来写一个划分 Chunk 的 webpack 插件吧&#xff0c;写完后你会发现想怎么分 Chunk 都可以&#xff01; 首先我们简单了解下 web…

图像配准:基于 OpenCV 的高效实现

在这篇文章中&#xff0c;我将对图像配准进行一个简单概述&#xff0c;展示一个最小的 OpenCV 实现&#xff0c;并展示一个可以使配准过程更加高效的简单技巧。什么是图像配准图像配准被定义为将不同成像设备或传感器在不同时间和角度拍摄的两幅或多幅图像&#xff0c;或来自同…

什么牌子的护眼灯最好推荐?盘点口碑好的护眼灯品牌

护眼灯是目前大部分家庭都在使用的灯具之一&#xff0c;利用光源起到保护视力的效果&#xff0c;预防近视&#xff0c;可谓是现代生活中伟大的发明&#xff0c;今天由小编来列出优秀的护眼灯品牌&#xff0c;并详细的介绍&#xff0c;告诉大家哪个护眼灯品牌好。① 南卡护眼台…

【微信小程序-原生开发】实用教程07 - Grid 宫格导航,详情页,侧边导航(含自定义页面顶部导航文字)

开始前&#xff0c;请先完成成员页的开发&#xff0c;详见 【微信小程序-原生开发】实用教程 06-轮播图、分类页签 tab 、成员列表&#xff08;含Tdesign升级&#xff0c;切换调试基础库&#xff0c;设置全局样式&#xff0c;配置组件按需注入&#xff0c;添加图片素材&#x…

Canal快速入门

Canal 一、Canal 入门 1.1、什么是 Canal ​ 阿里巴巴 B2B 公司&#xff0c;因为业务的特性&#xff0c;卖家主要集中在国内&#xff0c;买家主要集中在国外&#xff0c;所以衍生出了同步杭州和美国异地机房的需求&#xff0c;从 2010 年开始&#xff0c;阿里系公司开始逐步…