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. Hystrix 使用说明

    1.什么情况下会触发 fallback 方法 名字 描述 触发fallback EMIT 值传递 NO SUCCESS 执行完成,没有错误 NO FAILURE 执行抛出异常 YES TIMEOUT ...

  2. [心得笔记]Java多线程中的内存模型

    一:现代计算机的高速缓存 在计算机组成原理中讲到,现代计算机为了匹配 计算机存储设备的读写速度 与  处理器运算速度,在CPU和内存设备之间加入了一个名为Cache的高速缓存设备来作为缓冲:将运算需要 ...

  3. 题解 guP2421 【[NOI2002]荒岛野人】

    本题珂以转换成一个式子 即求Ci + Pi × x ≡ Cj + Pj × x (mod M) 的最小答案是否大于寿命最小值 以人数为最小值开始枚举山洞数,用扩展欧几里得计算最优答案是否大于寿命 若不 ...

  4. CSS 世界中的方位与顺序

    在 CSS 中,我们经常会与各种方向方位打交道. 譬如 margin.padding,它们就会有 margin-left.margin-right 或者是 padding-left.padding-r ...

  5. React事件绑定的方式

    一.是什么 在react应用中,事件名都是用小驼峰格式进行书写,例如onclick要改写成onClick 最简单的事件绑定如下: class ShowAlert extends React.Compo ...

  6. if函数+isna函数+vlookup函数实现不同列相同单元格内容排列在同一行

    1,首先学习的网址:https://jingyan.baidu.com/album/22a299b5dd0f959e19376a22.html?picindex=1 2,excel 这也许是史上最好最 ...

  7. 【Uva1025 A Spy in the Metro】动态规划

    题目描述 某城市地铁是线性的,有n(2≤n≤50)个车站,从左到右编号1~n.有M1辆列车从第1站开始往右开,还有M2辆列车从第n站开始往左开.列车在相邻站台间所需的运行时间是固定的,因为所有列车的运 ...

  8. spring boot+mybatis plus出现Invalid bound statement (not found)

    qlSessionFactory不要使用原生的,请使用MybatisSqlSessionFactory

  9. vulnhub-DC:1靶机渗透记录

    准备工作 在vulnhub官网下载DC:1靶机https://www.vulnhub.com/entry/dc-1,292/ 导入到vmware 打开kali准备进行渗透(ip:192.168.200 ...

  10. C++第四十四篇 -- MFC使用ChartCtrl绘制动态曲线

    前言 目的:使用控制台程序带MFC类库画一个动态曲线图 参考链接: https://blog.csdn.net/sinat_29890433/article/details/105360032 htt ...