title : 可持久化线段树
date : 2021-8-18
tags : 数据结构,ACM

可持久化线段树

可以用来解决线段树存储历史状态的问题。

我们在进行单点修改后,线段树只有logn个(一条链)的节点被修改,我们可以让修改后的树与修改前的树共享节点,节省时间和空间。

在学习之前,我们先引入三个前置知识:离散化、动态开点,权值线段树。

离散化

对于较大的数据范围,只要将关键点记录下来,记录下rank,就能把数据缩小到可以接受的范围,以便建立线段树或其他数据结构来解决问题。

具体步骤

(1)将所有端点加入辅助数组; (2)按坐标从小到大排序; (3)去重; (4)数据离散化,用hs数组记录端点的排名而非具体数字

vector<int>vt;
for(int i=1;i<=n;i++){
   cin>>a[i];
   vt.push(a[i]); //加入辅助容器
}
sort(vt.begin(),vt.end());//排序
vt.erase(unique(vt.begin(),vt.end()),vt.end()); //去重
for(int i=0;i<vt.size();i++){
   hs[vt[i]]=++tot; //存储为rank
}

动态开点

动态开点线段树可以避免离散化。

如果权值线段树的值域较大,离散化比较麻烦,可以用动态开点的技巧。

省略了建树的步骤,而是在具体操作中加入结点。

权值线段树

线段树的叶子节点保存的是当前值的个数。

每个节点保存区间左右端点以及所在区间节点的个数。

由于值域范围通常较大,一般会配合离散化或动态开点等策略优化空间。

应用

查找一个区间的第k大的值

查询某个数的排名

查询整个数组的排序

查询前驱和后继

单点修改
void update(int node,int start,int end,int pos){
   if(start==end) tr[node]++;
   else{
       int mid=start+end>>1;
       if(pos<=mid) update(node<<1,start,mid,pos);
       else update(node<<1|1,mid+1,end,pos);
  }
}//tr[i]表示值为i的元素个数,pos是要查找的位置
查询区间中的数出现次数
int query(int node,int start,int end,int ql,int qr){
   if(start==ql&&end==qr) return tr[node];
   int mid=start+end>>1;
   if(qr<=mid) return query(node<<1,start,mid,ql,qr);
   else if(ql>mid) return query(node<<1|1,mid+1,end,ql,qr);
   else return query(node<<1,start,mid,ql,qr)+query(node<<1|1,mid+1,end,ql,qr);
}//对单点查询同样适用
查询所有数的第k大值
int kth(int node,int start,int end,int k){
   if(start==end) return start;
   int mid=start+end>>1;
   int s1=tr[node<<1],s2=tree[node<<1|1];
   if(k<=s2) return kth(node<<2|1,mid+1,end,k);
   else return kth(node<<1,start,mid,k-s2);
} //注意是第k大,从右边开始减,如果是第k小就减去左边
查询前驱(后继同)
int findpre(int node,int start,int end){ //找这个区间目前最大的
   if(start==end) return start; //找到直接返回
   int mid=start+end>>1;
   if(t[node<<1|1]) return findpre(node<<1|1,mid+1,end);
   return findpre(node<<1,start,mid);
}

int pre(int node,int l,int r,int pos){ //求pos的前驱
   if(r<pos){ //在最右边
       if(t[node]) return findpre(node,l,r);
       return 0;
  }
   int mid=l+r>>1,res;
   if(mid+1<pos&&t[node<<1|1]&& (res=pre(node<<1|1,mid+1,r,pos))) return res; //在右区间寻找
   return pre(node<<1,l,mid,pos);  //在左区间寻找
}

可持久化线段树

复杂度分析

建树O(nlogn)

询问为O(logn)

空间复杂度O(nlognlogn)。

核心思想

以前缀和形式建立,基于动态开点的存储形式。

两棵线段树之间是可减的(每一个节点对应相减)。

存储

hjt[0]充当NULL,从1开始储存根节点

struct Node{
   int lc,rc,sum; //左儿子右儿子和值sum
}hjt[maxn*32] //空间一般开到32即可
int cnt,root[maxn];//内存池计数器和根节点编号
插入

并不改变原来的树,而是新开根节点,并向下开辟

void insert(inr cur,int pre.int pos,int l,int r){
   if(l==r){ //找到这个点,当前版本点值+1
       t[cur].v=t[pre].v+1;
       return;
  }
   int mid=l+r>>1;
   if(pos<=mid){
       t[cur].lc=++e;
       t[cur].rc=t[pre].rc;
       insert(t[cur].lc,t[pre].lc,pos,l,mid);
  }else{
       t[cur].rc=++e;
       t[cur].lc=t[pre].lc;
       insert(t[cur].rc,t[pre].rc,pos,mid+1,r);
  }
   t[cur].v=t[t[cur].lc].v+t[t[cur].rc].v; //pushup
}
询问操作

本质上与权值线段树相同,只需要在区间上作差。

int query(int l,int r,int x,int y,int k){
if (l == r){
return l;
  }
int mid=(l+r)/2;
int sum=tr[tr[y].l].sum-tr[tr[x].l].sum;
if(sum >= k){
return query(l,mid,tr[x].l,tr[y].l,k);
}
else{
       return query(mid+1,r,tr[x].r,tr[y].r,k-sum);
}
}
区间第K小

luoguP3834 【模板】可持久化线段树 2(主席树)

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 2e5 + 7;

int n, m, cnt, rt[maxn], a[maxn], x, y, k;
//a[i]是原始的序列,rt[i]是第i版本的主席树

struct node
{
int l, r, sum; //树的左端、右端和元素个数
} tr[maxn * 32];

vector<int> vt; //辅助容器

void read(int &data)  //快读优化
{
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
{
f = f * -1;
}
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
x = x * 10 + ch - '0';
ch = getchar();
}
data = x * f;
}

int getid(int x)  //返回每个元素的rank
{
return lower_bound(vt.begin(), vt.end(), x) - vt.begin() + 1;
}

void insert(int l, int r, int &x, int y, int pos)
{ //每次修改或插入,新建一个根节点,并向下递归新建节点
tr[++cnt] = tr[y];
tr[cnt].sum++;  //区域元素数量+1
x = cnt; //将原来的树地址指向当前树
if (l == r)
{ //已经是叶子了,返回
return;
}
int mid = (l + r) / 2;
if (mid >= pos)
{  //向左子树插入
insert(l, mid, tr[x].l, tr[y].l, pos);
}
else
{  //向右子树插入
insert(mid + 1, r, tr[x].r, tr[y].r, pos);  
}
}

int query(int l, int r, int x, int y, int k)
{ //l,r表示当前区间,x,y表示旧树和新树,k要在该区间找k小
if (l == r)
{
return l;    //找到则直接返回第k小的值l
}
int mid = (l + r) / 2;
int sum = tr[tr[y].l].sum - tr[tr[x].l].sum;
//左子树相减,其差值为两个版本之间左边相差多少个数
if (sum >= k)
{  //结果大于等于k,询问左子树
return query(l, mid, tr[x].l, tr[y].l, k);
}
else
{  //否则找右子树第k-sum小的数
return query(mid + 1, r, tr[x].r, tr[y].r, k - sum);
}
}

signed main()
{
read(n);
read(m);
for (int i = 1; i <= n; i++)
{
read(a[i]);
vt.push_back(a[i]); //辅助容器用于离散化
}
sort(vt.begin(), vt.end()); //排序
vt.erase(unique(vt.begin(), vt.end()), vt.end()); //去重
for (int i = 1; i <= n; i++)
{  //每次插入都从根节点开始建立新树(形成一条链)
insert(1, n, rt[i], rt[i - 1], getid(a[i]));
}

for (int i = 1; i <= m; i++)
{//第y棵数和第x-1棵数做差,就能查出[x,y]区间第k小值
read(x);
read(y);
read(k);
printf("%d\n", vt[query(1, n, rt[x - 1], rt[y], k) - 1]);

}
return 0;
}

参考资料

https://www.cnblogs.com/young-children/p/11787490.html

https://www.cnblogs.com/young-children/p/11787493.html

https://blog.csdn.net/ModestCoder_/article/details/90107874

https://blog.csdn.net/a1351937368/article/details/78884465

ACM学习笔记:可持久化线段树的更多相关文章

  1. [学习笔记] 可持久化线段树&主席树

    众所周知,线段树是一个非常好用也好写的数据结构, 因此,我们今天的前置技能:线段树. 然而,可持久化到底是什么东西? 别急,我们一步一步来... step 1 首先,一道简化的模型: 给定一个长度为\ ...

  2. [学习笔记]可持久化数据结构——数组、并查集、平衡树、Trie树

    可持久化:支持查询历史版本和在历史版本上修改 可持久化数组 主席树做即可. [模板]可持久化数组(可持久化线段树/平衡树) 可持久化并查集 可持久化并查集 主席树做即可. 要按秩合并.(路径压缩每次建 ...

  3. HDU 5820 (可持久化线段树)

    Problem Lights (HDU 5820) 题目大意 在一个大小为50000*50000的矩形中,有n个路灯.(n<=500000) 询问是否每一对路灯之间存在一条道路,使得长度为|x1 ...

  4. HDU 4348 To the moon 可持久化线段树,有时间戳的区间更新,区间求和

    To the moonTime Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://acm.hust.edu.cn/vjudge/contest/view.a ...

  5. HDU 5919 Sequence II(可持久化线段树)

    [题目链接]http://acm.hdu.edu.cn/showproblem.php?pid=5919 [题目大意] 给出一个数列,每次查询数列中,区间非重元素的下标的中位数.查询操作强制在线. [ ...

  6. 计蒜客 38229.Distance on the tree-1.树链剖分(边权)+可持久化线段树(区间小于等于k的数的个数)+离散化+离线处理 or 2.树上第k大(主席树)+二分+离散化+在线查询 (The Preliminary Contest for ICPC China Nanchang National Invitational 南昌邀请赛网络赛)

    Distance on the tree DSM(Data Structure Master) once learned about tree when he was preparing for NO ...

  7. bzoj 3673&3674 可持久化并查集&加强版(可持久化线段树+启发式合并)

    CCZ在2015年8月25日也就是初三暑假要结束的时候就已经能切这种题了%%% 学习了另一种启发式合并的方法,按秩合并,也就是按树的深度合并,实际上是和按树的大小一个道理,但是感觉(至少在这题上)更好 ...

  8. HDU 4417.Super Mario-可持久化线段树(无修改区间小于等于H的数的个数)

    Super Mario Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  9. 洛谷——P3919 【模板】可持久化数组(可持久化线段树/平衡树)

    P3919 [模板]可持久化数组(可持久化线段树/平衡树) 题目背景 UPDATE : 最后一个点时间空间已经放大 标题即题意 有了可持久化数组,便可以实现很多衍生的可持久化功能(例如:可持久化并查集 ...

随机推荐

  1. php 扩展 rabbitmq popt

    首先是rabbitmq-c-master.tar.gz包, 可以访问https://github.com/alanxz/rabbitmq-c去下载最新的 wget https://github.com ...

  2. Netty实现对Websocket的支持

    一.WebSocket的简介及优势 WebSocket 是一种网络通信协议.RFC6455 定义了它的通信标准.WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的 ...

  3. 开发必备linux命令大全-稳赚不亏

    我们的服务一般都是在linux系统运行,因此了解一些关于linux命令是必须.接下来将一一详细介绍一些常用的linux的命令 文件操作 远程登录与操作 磁盘挂载 进程管理 启动和结束 系统性能参数查看 ...

  4. shell编程之循环语句for / while / until

    shell编程之循环语句与函数 一.条件测试 二.循环语句 ① for循环语句结构(遍历) 示例1 示例2 ② while循环语句结构(迭代) 示例1 示例2 ③ until 循环语句结构 示例1 一 ...

  5. nmcli device and nmcli connection

    NetworkManager是RHEL 7之后推出的有效管理网络的服务 NetworkManager通过工具nmcli来管理网卡 网卡属于物理硬件,NetworkManager属于软件层面,如何通过软 ...

  6. WORD2010

    WORD2010表格,利用公式计算第一行,将光标定位到第二行相应位置,点F4即可进行相应的运算,第三行,相应位置,再点F4,继续计算

  7. JDK安装与环境搭建.

    卸载JDK 1.删除Java安装目录 2.删除Java Home 3.删除path下Java的目录 4.打开cmd命令输入java-version 出现''不是内部或外部命令,也不是可运行的程序 或批 ...

  8. [SHOI2014]概率充电器 题解

    注意到本题的贡献是不带权的,所以期望其实就是每个点的概率之和. 本题正着做好像不是很好做,要考虑 \(P(A+B)=P(A)+P(B)-P(A)P(B)\) 的容斥(因为这是两个条件至少满足一个,所以 ...

  9. gitbook导出markdown文件为pdf格式

    前言本人想完成如题所示的操作,可是兜兜转转在网上搜了好久,不是Linux下的教程就是mac OS下的教程,而且一大堆windows的教程能够说明白的寥寥无几,本着分享的态度,鄙人今天特整理教程如下: ...

  10. SSM中如何上传图片

    1.文件配置 2.jsp页面 文件的name值不能跟数据库列名一致 3.控制层收集数据转发到逻辑层 4.逻辑层处理把用户信息存到数据库 5.注册成功后跳到jsp页面进行展示