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

可持久化线段树

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

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

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

离散化

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

具体步骤

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

  1. 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=0i<vt.size();i++){
       hs[vt[i]]=++tot; //存储为rank
    }

动态开点

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

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

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

权值线段树

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

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

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

应用

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

查询某个数的排名

查询整个数组的排序

查询前驱和后继

单点修改
  1. 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是要查找的位置
查询区间中的数出现次数
  1. 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大值
  1. 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小就减去左边
查询前驱(后继同)
  1. 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开始储存根节点

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

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

  1. 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
    }
询问操作

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

  1. 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(主席树)

  1. #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. tomcat shutdown后,进程还存在linux系统中的解决办法

    基本原理为启动tomcat时记录启动tomcat的进程id(pid),关闭时强制杀死该进程 第一步 :vim修改tomcat下bin/catalina.sh文件,添加点东西,主要是记录tomcat的p ...

  2. Robotframework学习笔记之一Common Resource导入的Library库显示红色(导入失败)

    第一次使用Robotframework,所以也遇到了很多的坑,导入项目后 ,一些自带的库显示红色,导入失败!(ps:自带的库也显示红色) Ride日志如下(Tools--view ride log): ...

  3. 青蛙跳台阶问题(斐波那契数列) python

    一只青蛙一次可以跳上1级台阶,也可以跳上2级.求该青蛙跳上一个n级的台阶总共有多少种跳法 class Solution: def jump(self,n): if n ==0: return 0 el ...

  4. Entity Framework Core中的数据迁移命令

    使用程序包管理控制台输入命令. 数据迁移命令: Add-Migration  对比当前数据库和模型的差异,生成相应的代码,使数据库和模型匹配的. Remove-Migration 删除上次的迁移 Sc ...

  5. Python装饰器、迭代器&生成器、re正则表达式、字符串格式化

    Python装饰器.迭代器&生成器.re正则表达式.字符串格式化 本章内容: 装饰器 迭代器 & 生成器 re 正则表达式 字符串格式化 装饰器 装饰器是一个很著名的设计模式,经常被用 ...

  6. 抓包工具-Charles

    1.简介Charles Charles其实是一款代理服务器,通过成为电脑或者浏览器的代理,然后截取请求和请求结果达到分析抓包的目的.charles有Window版本和Mac OS版本,也同时支持ios ...

  7. adb 记录ADB执行记录

    自动化测试需要获得当前的activity,来判断处于的页面是否正确: hierarchy view经常连不上真机,无法获得activity,所以直接用 adb命令来查看当前运行的 activity就可 ...

  8. C语言:特殊点

    编译器给变量分配内存在函数外部定义的变量叫做全局变量(Global Variable),在函数内部定义的变量叫做局部变量(Local Variable)一个变量,即使不给它赋值,它也会有一个默认的值, ...

  9. python -- os处理模块

    # --------------------------------# 使用os模块操作目录和文件# --------------------------------# getcwd() 获取当前目录 ...

  10. vue(23)Vuex的5个核心概念

    Vuex的核心概念 Vuex有5个核心概念,分别是State,Getters,mutations,Actions,Modules. State   Vuex使用单一状态树,也就是说,用一个对象包含了所 ...