洛谷题单指南-线段树的进阶用法-P3834 【模板】可持久化线段树 2

news/2024/12/28 0:24:55/文章来源:https://www.cnblogs.com/jcwy/p/18630532

原题链接:https://www.luogu.com.cn/problem/P3834

题意解读:静态区间第k小问题,可持久化线段树(也称为主席树)模版题。

解题思路:

一、朴素想法:如何求完整区间[1,n]第k小

1、权值线段树

设n个数构成序列a,b数组代表a中元素出现的次数,即b数组的构建方式为对每一个a[i]做b[a[i]]++。

针对b数组区间构建的线段树,称为权值线段树,权值线段树的节点维护的信息是节点范围内所有元素出现的次数。

因此,可以说,普通线段树节点表示的区间是下标,权值线段树节点表示的区间是值域

2、权值线段树主要操作

节点定义:

struct Node
{int l, r; //l,r对应原数组a的值int cnt; //[l, r]的数出现的个数
} tr[N * 4];

单点修改:

//将数值x的个数增加v,也就是将节点x维护的cnt加v
void update(int u, int x, int v)
{if(tr[u].l == tr[u].r) tr[u].cnt += v;else {int mid = tr[u].l + tr[u].r >> 1;if(x <= mid) update(u << 1, x, v);else update(u << 1 | 1, x, v);pushup(u);}
}

区间查询:

//查询[l,r]范围所有值出现的次数
int query(int u, int l, int r)
{if(tr[u].l >= l && tr[u].r <= r) return tr[u].cnt;else if(tr[u].l > r || tr[u].r < l) return 0;else return query(u << 1, l, r) + query(u << 1 | 1, l, r);
}

为了演示权值线段树的基本操作,这里给出求逆序对问题(P1908)的权值线段树解法:

a、对序列a[]每一个元素进行离散化处理

b、依次遍历每一个元素a[i],在区间[a[i] + 1, n]中查询所有元素个数,累加到答案

c、对a[i]个数进行加1,update(1, a[i], 1)

注意:[a[i] + 1, n]的元素个数意味着排在a[i]前面且值比a[i]大的元素个数,也就是逆序对!

下面是完整代码:

P1908-权值线段树做法
#include <bits/stdc++.h>
using namespace std;const int N = 500005;struct Node
{int l, r; //l,r对应原数组a的值int cnt; //[l, r]的数出现的个数
} tr[N * 4];
int a[N];
vector<int> b;
int n;
long long ans;//查询离散化后的值
int find(int x)
{return lower_bound(b.begin(), b.end(), x) - b.begin() + 1;
}void pushup(int u)
{tr[u].cnt = tr[u << 1].cnt + tr[u << 1 | 1].cnt;
}void build(int u, int l, int r)
{tr[u] = {l, r};if(l == r) tr[u].cnt = 0;else {int mid = l + r >> 1;build(u << 1, l, mid);build(u << 1 | 1, mid + 1, r);pushup(u);}
}//将数值x的个数增加v,也就是将节点x维护的cnt加v
void update(int u, int x, int v)
{if(tr[u].l == tr[u].r) tr[u].cnt += v;else {int mid = tr[u].l + tr[u].r >> 1;if(x <= mid) update(u << 1, x, v);else update(u << 1 | 1, x, v);pushup(u);}
}//查询[l,r]范围所有值出现的次数
int query(int u, int l, int r)
{if(tr[u].l >= l && tr[u].r <= r) return tr[u].cnt;else if(tr[u].l > r || tr[u].r < l) return 0;else return query(u << 1, l, r) + query(u << 1 | 1, l, r);
}int main()
{cin >> n;for(int i = 1; i <= n; i++){cin >> a[i];b.push_back(a[i]);}sort(b.begin(), b.end()); //排序b.erase(unique(b.begin(), b.end()), b.end()); //去重build(1, 1, b.size()); //建立线段树for(int i = 1; i <= n; i++){int x = find(a[i]); //离散化ans += query(1, x + 1, b.size()); //查询在a[i]前面出现且比a[i]大的元素个数update(1, x, 1); //将a[i]出现的次数加1}cout << ans;return 0;
}

3、查询第k小

除了查询元素个数,权值线段树也可以查询第k小,

由于权值线段树叶节点表示的就是值的个数,而且叶节点从分布上从左到右是递增的,

如序列5 4 2 6 3 1生成的权值线段树为左图所示:

 

要查询第4小元素,查询路径如右图所示,主要逻辑如下:

从根节点[1,6]开始,先看左子树元素个数为3, 4 > 3,因此要找的元素在右子树,转化成在右子树查第4 - 3 = 1个元素;

再看[4,6]节点,其左子树元素个数为2, 2 > 1,因此要找的元素在左子树;

再看[4,5]节点,其左子树元素个数为1, 1 >= 1,因此要找的元素在左子树;

再看[4,4],已经到叶子结点,所以要找的第4小元素就是4。

以上过程用代码描述为

//查询第k小的元素
int find_kth(int u, int k)
{if(tr[u].l == tr[u].r) return tr[u].l; //到了叶子节点说明找到了第k小元素else{int leftcnt = tr[u << 1].cnt; //左子树所有元素个数if(leftcnt >= k) return find_kth(u << 1, k); //如果左子树元素个数>k,说明第k小在左子树else return find_kth(u << 1 | 1, k - leftcnt); //否则在右子树去找第k - leftcnt小}
}

如此,权值线段树就可以解决查询完整区间第k小问题。

二、进阶思考:如何求指定区间[l,r]第k小

1、权值线段树的可加/减性

针对序列5 4 2 6 3 1,我们采用逐步建立线段树的方式:

建立初始线段树:

对序列5建立线段树:

对序列5 4建立线段树:

对序列5 4 2建立线段树:

对序列5 4 2 6建立线段树:

对序列5 4 2 6 3建立线段树:

对序列5 4 2 6 3 1建立线段树:

不难发现,序列每增加一个值,线段的变化都是在前一个线段树基础上将值到根节点一条路径上的cnt都加1,因此说权值线段树具有可加性。

2、前缀和思想

根据以上可加性的分析,可以知道对于值区间a[1] ~ a[i]建立的线段树可以维护a[1] ~ a[i]的元素个数

那么如何才能知道一个区间a[l] ~ a[r]各个元素的个数呢?这里可以借助前缀和的思想,也就是用a[1] ~ a[r]的线段树减去a[1] ~ a[l-1]的对应各个节点的cnt值,就可以得到l ~ r区间范围各个元素值的个数。

例如,用序列5 4 2 6 3 1的线段树 减去 序列5 4的线段树对应节点值,就得到了序列2 6 3 1的线段树:

这样一来,我们可以考虑针对所有的a[1] ~ a[i] ( 1<=i<=n) 都建立一棵线段树,算上初始空线段树,一共是n + 1棵线段树,

要查询区间[l, r]第k小,可以用第r棵线段树减去第l-1棵线段树对应节点的cnt,然后利用上述介绍的查询第k小的算法即可得到区间k小值。

3、动态开点

如果要完整的建立n + 1棵线段树,空间复杂度将达到 O(n2*4)级别,不可接受,就需要借助于“动态开点”技术来优化。

以从序列5 4 2的线段树到序列5 4 2 6的线段树,更新值6的数量操作为例:

不难发现,要将节点6的cnt加1,并不需要完整新建一棵线段树,只会影响从节点[6,6]到根节点一条路径,因此只需要将节点[6,6],[4,6],[1,6]复制出来,将其cnt加1即可,这样每次操作只会新增logn的节点,空间复杂度缩减至O(n * 4 + n*logn)。

要实现上述动态开点的操作,线段树节点的定义就与之前线段树不太一样了,我们将节点定义为:

struct Node
{int L, R; //L:左子树编号 R:右子树编号int cnt; //节点所表示的值域区间的元素个数
} tr[N * 24];

再用一个int数组来保存每一棵复制出来的新线段树的根节点:

int root[N]; //维护n + 1棵线段树的根节点

节点的编号,在创建和复制的时候进行递增来生成

int idx; //线段树节点编号,新增节点时递增idx++

例如初始空线段树根节点为root[0],到序列5 4 2时线段树的根节点为root[3],序列5 4 2 6时线段树的根节点为root[4]

4、核心操作

a、build:初始化线段树

建立线段树,cnt初始都是0,将根节点赋值给root[0]。

代码实现如下:

//建立初始线段树,区间范围为l~r
int build(int l, int r)
{int u = idx++;if(l == r) return u;else{int mid = l + r >> 1;tr[u].L = build(l, mid); //递归建立左子树tr[u].R = build(mid + 1, r); //递归建立右子树}return u;
}

b、update:在线段树中将a[i]加1

依次处理序列,对于每个值a[i],要将前一棵线段树root[i-1]的值是a[i]的叶子节点到根节点都进行复制,并将所有节点的cnt加1,生成新的根节点编号赋值给root[i]。

代码实现如下:

//在根节点为pre的权值线段树中将权值x的个数加1,返回新生成的根节点,l、r表示节点所在区间
int update(int pre, int l, int r, int x, int v)
{int u = idx++;//复制pre节点到u,左右子树都复制,cnt加1tr[u].L = tr[pre].L;tr[u].R = tr[pre].R;tr[u].cnt = tr[pre].cnt + 1;if(l == r) return u; //到叶子节点则返回int mid = l + r >> 1;if(x <= mid) tr[u].L = update(tr[u].L, l, mid, x, v); //在左子树递归找x,左子结点应该复制else tr[u].R = update(tr[u].R, mid + 1, r, x, v); //在右子树递归找x,右子节点应该复制return u;
}

c、find_kth:在[l,r]值域范围查询第k大

先找到root[r]和root[l-1]为根的两棵线段树,[l,r]范围内在左子树的元素个数为:leftcnt = root[r]的左子树的元素个数 - root[l-1]的左子树的元素个数;

再将k与leftcnt比较,如果k <= leftcnt,则应该往root[r]和root[l-1]的左子树去查询;

否则,应该往root[r]和root[l-1]的右子树去查询。

代码实现如下:

//在根节点是left,right的线段树中查询第k小,l、r表示节点所在区间
int find_kth(int l, int r, int left, int right, int k)
{//计算查询区间范围内所有左子树的元素个数int leftcnt = tr[tr[right].L].cnt - tr[tr[left].L].cnt;if(l == r) return l; //到叶子节点,说明找到第k小int mid = l + r >> 1;if(k <= leftcnt) return find_kth(l, mid, tr[left].L, tr[right].L, k);else return find_kth(mid + 1, r, tr[left].R, tr[right].R, k - leftcnt);
}

5、离散化

需要注意,值的范围是0ai10^9,要进行离散化处理。

整体流程请参考代码注释。

100分代码:

#include <bits/stdc++.h>
using namespace std;const int N = 200005;struct Node
{int L, R; //L:左子树编号 R:右子树编号int cnt; //节点所表示的值域区间的元素个数
} tr[N * 24];int root[N]; //维护n + 1棵线段树的根节点
int idx; //线段树节点编号,新增节点时递增idx++
int a[N], n, m; //a是n个整数的序列
vector<int> b; //用于对a排序去重离散化//获取x离散化之后的值
int lsh(int x)
{return lower_bound(b.begin(), b.end(), x) - b.begin() + 1;
}//建立初始线段树,区间范围为l~r
int build(int l, int r)
{int u = idx++;if(l == r) return u;else{int mid = l + r >> 1;tr[u].L = build(l, mid); //递归建立左子树tr[u].R = build(mid + 1, r); //递归建立右子树}return u;
}//在根节点为pre的权值线段树中将权值x的个数加1,返回新生成的根节点,l、r表示节点所在区间
int update(int pre, int l, int r, int x, int v)
{int u = idx++;//复制pre节点到u,左右子树都复制,cnt加1tr[u].L = tr[pre].L;tr[u].R = tr[pre].R;tr[u].cnt = tr[pre].cnt + 1;if(l == r) return u; //到叶子节点则返回int mid = l + r >> 1;if(x <= mid) tr[u].L = update(tr[u].L, l, mid, x, v); //在左子树递归找x,左子结点应该复制else tr[u].R = update(tr[u].R, mid + 1, r, x, v); //在右子树递归找x,右子节点应该复制return u;
}//在根节点是left,right的线段树中查询第k小,l、r表示节点所在区间
int find_kth(int l, int r, int left, int right, int k)
{//计算查询区间范围内所有左子树的元素个数int leftcnt = tr[tr[right].L].cnt - tr[tr[left].L].cnt;if(l == r) return l; //到叶子节点,说明找到第k小int mid = l + r >> 1;if(k <= leftcnt) return find_kth(l, mid, tr[left].L, tr[right].L, k);else return find_kth(mid + 1, r, tr[left].R, tr[right].R, k - leftcnt);
}int main()
{cin >> n >> m;for(int i = 1; i <= n; i++){cin >> a[i];b.push_back(a[i]);}//排序、去重sort(b.begin(), b.end());b.erase(unique(b.begin(), b.end()), b.end());//建立初始线段树,根节点赋值root[0]root[0] = build(1, b.size());for(int i = 1; i <= n; i++){//在以root[i-1]为根的线段树中将a[i]离散化后的    个数加1,生成新的线段树根节点赋值给root[i]root[i] = update(root[i - 1], 1, b.size(), lsh(a[i]), 1);}int l, r, k;while(m--){cin >> l >> r >> k;int res = find_kth(1, b.size(), root[l - 1], root[r], k);cout << b[res - 1] << endl; //恢复离散化之前的值}return 0;
}

 

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

FTP一键安装脚本(linux版)

简述:linux版权限可能会不容易理解,可参考windwos做为基础。一、FTP(linux版) 1. ftp详解 简介: vsftpd 是“very secure FTP daemon”的缩写,安全性是它的一个最大的特点。 vsftpd 是一个 UNIX 类操作系统上运行的服务器的名字,它可以运行在诸如 Linux、BSD、Solaris、…

React—01—基本学习,如何在html中直接使用react;

一、react的特点:<script>标签这里要加一个“text/babel”,babel才知道这个<script>标签里要解析js代码,否则babel不会启动。 React 组件是返回标签的 JavaScript 函数:哪个组件是通过改变 state 实现可响应的,或者哪个组件拥有 这个 state。 然后我们需要确定…

记录python+pyside+qml+qtcharts 使用,防踩坑

使用QML-qtquick 进行开发时,有个使用chart图表的需求,看了一大圈,网上都是qmake或是cmake来构建QTchart,用python开发也只搜到QtWidgets模块进行图表绘制。然而我对qtwidgets不是很了解,想要的是QML开发,在使用ChartView{}时一直闪退,没有效果。经历了苦苦搜寻,终于在 h…

制作了一款 pdf 转换图片的工具( csharp 版 )

在 Windsurf 的帮助下,制作了一款 windows 下的 pdf 转换图片(png/jpg/tif)工具。支持递归查找 pdf,一些配置给写死了,适合简单使用。 PDF 批量转图片工具 这是一个功能强大且易用的 PDF 转图片工具,专为批量处理 PDF 文件设计。它能够将 PDF 文件的每一页转换为高质量的…

C# WPF PrintDialog 打印(1)

参考“WPF 打印实例”的文章:https://www.cnblogs.com/gnielee/archive/2010/07/02/wpf-print-sample.html 测试程序: 首先打印Canvas效果:看起来似乎没问题,但是调整窗体尺寸遮挡部分元素:再打印Canvas效果:可以发现PrintVisual方法只打印了可见部分的元素,测试打印Dat…

CentOS8安装

安装方法跟Centos7无差异。 一、下载安装包镜像地址:https://mirrors.aliyun.com/centos-vault/centos/8.2.2004/isos/x86_64/?spm=a2c6h.25603864.0.0.1f647af6cvFFgO 二、安装操作系统 安装界面如下, 三、后续工作a、配置ip地址 [root@localhost ~]# cat /etc/sysconfi…

某视频tv app 签名算法分析

一、基本信息 分析工具:objection、frida 二、加解密分析 通过抓包发现请求体里有签名校验 sign ,并且响应体里返回的数据是密文,所以本次的目标就是破解签名 sign 以及响应包加密算法。2.1 请求签名 sign 从 sign 的数值长度上判断可能是 MD5,先使用 objection hook java.…

Metal RT压缩

iOS设备的屏幕分辨率越来越高,渲染时需要的RT的分辨率和内存的开销都更高了。苹果官方在2021年发布的A15 Bonic芯片中支持了RT的lossy压缩,可以减少50%的内存footprint。 MetalRT压缩是什么? Apple在2018年就已经提供了RT的lossless压缩方案,可以节省带宽,从而降低功耗。在…

百度地图轨迹绘制以及轨迹回放以及鼠标滑过自定义样式

用百度地图的绘制轨迹以及相关的轨迹回放,鼠标滑过展示与否的自定义弹窗的整理 图片展示组件代码:<!--* @Author: menxiaojin* @Date: 2023-07-12 14:03:03* @LastEditors: menxiaojin* @LastEditTime: 2023-07-20 19:41:02 --> <!--首页地图组件--> <templat…

【护网】IP WhitePass:IP过滤,白名单过滤,用于护网CDN、负载地址过滤等;

免责声明 由于传播、利用本公众号夜组安全所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号夜组安全及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!所有工具安全性自测!!!工具介绍 在护…