首页 > 其他分享 >线段树复习笔记——可持久化线段树(主席树)

线段树复习笔记——可持久化线段树(主席树)

时间:2022-12-22 21:23:17浏览次数:62  
标签:cnt return 复习 int 线段 tr 笔记 leq 节点

可持久化线段树——主席树

引入(P3834 【模板】可持久化线段树 2 - 洛谷

看看题面:

题目描述

如题,给定 \(n\) 个整数构成的序列 \(a\),将对于指定的闭区间 \([l, r]\) 查询其区间内的第 \(k\) 小值。

数据规模与约定

  • 对于 \(100\%\) 的数据,满足 \(1 \leq n,m \leq 2\times 10^5\),\(|a_i| \leq 10^9\),\(1 \leq l \leq r \leq n\),\(1 \leq k \leq r - l + 1\)。

看着像线段树,可怎么做呢?

对于一个普通的线段树,如果要维护它的历史版本(即每一次修改后版本都要存下来),这种情况下是要开 \(M\)(操作次数)个线段树吗?

不可能的(不然我不会在这里说)

因为在每次修改时,只会影响到修改节点所在的一条链单点修改),即 \(\log n\) 个节点,那么……

img

我们就可以在每次修改时将改变的节点给“提”出来,如上图。

这就是主席树

这样空间复杂度就只有 \(\mathcal{O}((N + M)\log N)\) 了。

而此题就是经典的区间静态第 \(k\) 小问题。

  • 这就需要维护值域了,即主席树的每个节点维护的不是位置,而是一个范围,而节点的值为这个范围中的数的个数
  • 而此题 \(|a_i| \leq 10^9\) ,很明显需要离散化。
  • 并且主席树也不能用节点编号 \(\times \ 2\) 和 \(\times \ 2 + 1\) 来表示了,那么节点中的 \(l\) 和 \(r\) 就用来表示左右子节点的编号。
  • 还要单独开一个 \(root\) 数组来存 \(m\) 个根节点。

那么接下来就看看此题的细节吧!

分段讲解

  • 节点需定义的信息

struct Node{
	int l, r, cnt;
	// l, r 为左右子节点编号,cnt 即为此范围中数的个数
	// 范围其实是不用存的,二分时就可以算出来
}tr[N << 5]; // 计算得出,一定要尽量开大点
  • 离散化

用 \(vector\) 来离散化。

定义一个 \(\rm{find}\) 函数来查找数值在离散化数组中的位置。

vector<int> nums;
// 此处省略...
inline int find(int x){
	return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
	// 一个 lower_bound 即可解决
}
// 省略...
int main(){
	for(int i = 1; i <= n; ++i){
		scanf("%d", &a[i]);
		nums.push_back(a[i]);
	}
	
	sort(nums.begin(), nums.end());
	nums.erase(unique(nums.begin(), nums.end()), nums.end());
	// 离散化基本过程,排序,删除重复元素
}
  • \(\rm{build}\)

int build(int l, int r){
	int u = ++idx;
	// 节点编号也直接像建图一样依次加
	if(l == r) return u;
	// 这里是有返回值的,为了在后面子节点能直接上传节点编号
	int mid = l + r >> 1;
	tr[u].l = build(l, mid);
	tr[u].r = build(mid + 1, r);
	return u;
}
  • \(\rm{insert}\)

用来在主席树中插入一个新的值。

int insert(int p, int l, int r, int x){
	/*
	p表示原来的根节点(就是需要“提”出来的那个节点)
	l, r是节点的左右边界,即维护的值域
	x是插入的值
	*/
	int q = ++idx;
	// 新建一个节点,“提”出来
	tr[q] = tr[p];
	// 首先复制原节点的信息
	if(l == r){
		++tr[q].cnt;
		// 到子节点时,x就是个确定的值了,直接cnt++
		return q;
		// 记得返回编号
	}
	
	int mid = l + r >> 1;
	if(x <= mid) tr[q].l = insert(tr[p].l, l, mid, x);
	// x在左半边,到左子节点修改,原来的根节点也往下找左子节点,最后记得顺便记录左子节点编号
	else tr[q].r = insert(tr[p].r, mid + 1, r, x);
	// x在右半边,同上
	tr[q].cnt = tr[tr[q].l].cnt + tr[tr[q].r].cnt;
	// 更新cnt ,相当于普通的线段树中的pushup
	return q;
}
  • \(\rm{query}\)

要找到 \([l,r]\) 中的第 \(k\) 小值,就可以在查询的时候同时找到以 \(root_{l - 1}\) 和 \(root_r\) 为根节点的子树(即插入了前 \(l - 1\) 个数的区间\([1, l - 1]\) 和插入了前 \(r\) 个数的区间 \([1, r]\) 的历史版本),同时向下搜两个子树的节点,那么 \([l,r]\) 中每个节点 (区间) 中的 \(cnt\) 就可以直接用 \([1,r]\) 中的节点的 \(cnt\) 减去 \([1, l - 1]\) 中节点的 \(cnt\) 。

int query(int q, int p, int l, int r, int k){
	/*
	q是[1, r]中的节点,p是[1, l - 1]中的节点
	l, r还是节点维护的值域
	k是要找以此节点为根节点的子树中的第k小值
	*/
	if(l == r) return l;	// 到叶节点直接返回值
    
	int cnt = tr[tr[q].l].cnt - tr[tr[p].l].cnt;
	// 上面讲的,[l, r]的cnt = [1, r]的cnt - [1, l - 1]的cnt ,这里直接看左半边的,后面方便比较
	// 此处说的 l, r 是查询 [l, r]的第k小值 中的,不要和结点维护的值域搞混了
	int mid = l + r >> 1;
	if(k <= cnt) return query(tr[q].l, tr[p].l, l, mid, k);
	// 因为左半边的每个数都比右半边的小,所以如果要查找的排名比左半边的总个数小,那么第k小的数肯定在左半边
	else return query(tr[q].r, tr[p].r, mid + 1, r, k - cnt);
	// 否则同上,到右子节点去找
}

最后,完整 \(\text{Code}\)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 100010;

int n, m;
int a[N];
int root[N], idx;
vector<int> nums;

struct Node{
	int l, r, cnt;
}tr[21 * N];

inline int find(int x){
	return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
}

int build(int l, int r){
	int u = ++idx;
	if(l == r) return u;
	
	int mid = l + r >> 1;
	tr[u].l = build(l, mid);
	tr[u].r = build(mid + 1, r);
	return u;
}

int insert(int p, int l, int r, int x){
	int q = ++idx;
	tr[q] = tr[p];
	if(l == r){
		++tr[q].cnt;
		return q;
	}
	
	int mid = l + r >> 1;
	if(x <= mid) tr[q].l = insert(tr[p].l, l, mid, x);
	else tr[q].r = insert(tr[p].r, mid + 1, r, x);
	tr[q].cnt = tr[tr[q].l].cnt + tr[tr[q].r].cnt;
	return q;
}

int query(int q, int p, int l, int r, int k){
	if(l == r) return r;
    
	int cnt = tr[tr[q].l].cnt - tr[tr[p].l].cnt;
	int mid = l + r >> 1;
	if(k <= cnt) return query(tr[q].l, tr[p].l, l, mid, k);
	else return query(tr[q].r, tr[p].r, mid + 1, r, k - cnt);
}

int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; ++i){
		scanf("%d", &a[i]);
		nums.push_back(a[i]);
	}
	
	sort(nums.begin(), nums.end());
	nums.erase(unique(nums.begin(), nums.end()), nums.end());
	
	root[0] = build(0, nums.size() - 1);
	// root[0]先赋值为 build 返回的根节点编号
	for(int i = 1; i <= n; ++i)
		root[i] = insert(root[i - 1], 0, nums.size() - 1, find(a[i]));
		// 插入n个数,上一个根节点为 root[i - 1],左右边界如上,注意插入的值要变成 find(a[i])
	
	int l, r, k;
	while(m--){
		scanf("%d%d%d", &l, &r, &k);
		printf("%d\n", nums[query(root[r], root[l - 1], 0, nums.size() - 1, k)]);
		// 注意query返回的是离散数组中的位置,同时搜索以 root[r] 和 root[l - 1] 为根节点的子树
	}
	
	return 0;
}

\(\text{Updeted on 2022.12.22}\)

\(\mathcal{To \ Be \ Continued}\) ~ ~

标签:cnt,return,复习,int,线段,tr,笔记,leq,节点
From: https://www.cnblogs.com/ZZM-248/p/16999590.html

相关文章

  • HDU4553 线段树维护最长连续区间
    //题意:(略了)//思路:这里很明显是要维护区间最大连续子段,按照以下优先级查找//A1.左边区间的连续子段是否满足//A2.左右两个区间中间合并起来的子段是否满足......
  • vue笔记
    Vue笔记前言使用Vue需要node.js环境,所以这里需要利用nvm安装node.js。node和npm相关的名词很多,比较容易混淆。下面对这些名词做个统一梳理node:一个基于ChromeV8......
  • FreeSWITCH学习笔记:Lua脚本
    本文更新于2022-06-03,使用FreeSWITCH1.10.7。目录argvfreeswitch.APIAPI:executefreeswitch.bridgefreeswitch.consoleLogfreeswitch.DbhDBH:affected_rowsDBH:connected......
  • Node.js(笔记04) - 时钟示例 - fs 和 path 模块结合应用
    案例需求将素材目录​./files​ 中的index.html 页面,拆分成三个文件,分别是:index.cssindex.jsindex.html并且将拆分出来的3个文件,存放到​./files/clock​ 目录中;这个i......
  • Spring IOC官方文档学习笔记(三)之依赖项
    1.依赖注入(1)依赖注入(DI)的概念:某个bean的依赖项,由容器来负责注入维护,而非我们自己手动去维护,以此来达到bean之间解耦的目的,如下//情况一:不使用依赖注入publicclass......
  • leetcode笔记——单调栈
    在leetcode中,使用单调栈的题大多是寻找下一个更大的数类似,我感觉其他变形问题还是挺难搞的实际处理时候可能还是先暴力,再想着怎么通过单调栈去优化吧列一下今天遇到的三......
  • Node.js(笔记03) - path 路径模块
    path 路径模块path 模块是Node.js 官方提供的、用来处理路径的模块。它提供了一系列方法和属性,来满足用户对路径的处理需求。 官方文档:​​https://nodejs.org/dist/la......
  • (笔记)伺服电机三环(电流环、速度环、位置环)控制原理及参数调节
     运动伺服一般都是三环控制系统,从内到外依次是电流环、速度环、位置环。  一、理解三环1、电流环电流环的输入是速度环PID调节后的输出,我们称为“电流环给定”吧......
  • #yyds干货盘点# react笔记之学习之完成添加功能
    前言我是歌谣我有个兄弟巅峰的时候排名c站总榜19叫前端小歌谣曾经我花了三年的时间创作了他现在我要用五年的时间超越他今天又是接近兄弟的一天人生难免坎坷大不了从......
  • #yyds干货盘点# react笔记之学习之完成删除功能
    前言我是歌谣我有个兄弟巅峰的时候排名c站总榜19叫前端小歌谣曾经我花了三年的时间创作了他现在我要用五年的时间超越他今天又是接近兄弟的一天人生难免坎坷大不了从......