首页 > 其他分享 >主席树(可持久化线段树)

主席树(可持久化线段树)

时间:2024-01-23 20:11:22浏览次数:33  
标签:return int 线段 mid tot 持久 主席 define

主席树

前言

主席树也是一种数据结构,是线段树的进阶,作用是可以保留历史版本,支持回退,就是回到之前某个未修改的状态。就像我们在写博客时如果误删了重要的内容,就可以用 Ctrl + z 撤回一次操作,也可以用Ctrl + Shift +z 还原我们撤回的操作,这就需要存下每次编辑的操作。

基本原理

可持久化线段树,顾名思义,是可持久的线段树(好像是废话)。那么问题就在于怎么去可持久化,支持访问历史版本。
最暴力的,直接复制整棵树,再对新的这棵树进行修改。但这样的话,节点数为 \(n\),操作数为 \(m\),时间复杂就是 \(O(nm)\)。img
好了,直接 TLE。显然复制这棵树会有非常多的浪费,思考一下,我们只需要复制修改后发生改变的点就好了。

img
img

我们要复制的只是蓝色的点其他的保持不变,要在蓝色的点边上复制一个一模一样的点然后修改。如图:
img
红色的点即为修改后的点,继承原节点的所有信息,包括未修改的儿子。这样我们只需要存下每次修改后的根,就能访问该版本的树。修改的时间复杂度为 \(O(\log{n})\)。

操作

前置操作

定义节点,存 5 个信息:左右儿子,左右端点,权值。

#define lc a[u].l
#define rc a[u].r
#define L a[u].ls
#define R a[u].rs
struct tree
{
    int l,r;
    int ls,rs;
    int v;
}a[N];

新建节点

int tot;
int New(int u)
{
    a[++tot]=a[u];//结构体直接复制所有信息
    return tot;
}

建树

和线段树类似,不过节点编号依次增加,但左右儿子不一定是乘二和乘二加一了。

int build(int u,int l,int r)
{
    u=++tot;
    L=l,R=r;//更新区间端点
    if(l==r) 
    {
        a[u].v=w[l];
        return u;
    }
    int mid=(l+r)>>1;
    lc=build(lc,l,mid);
    rc=build(rc,mid+1,r);
    return u;
}

修改

先新建节点,然后在新建的树中修改,要记得更新修改了的儿子。

int modify(int u,int x,int v)
{
    u=New(u);
    if(L==R) a[u].v=v;
    else 
    {
        int mid=(L+R)>>1;
        if(x<=mid) lc=modify(lc,x,v);
        else rc=modify(rc,x,v);
    }
    return u;
}

查询

和线段树一毛一样的。

int query(int u,int x)
{
    if(L==R) return a[u].v;
    else 
    {
        int mid=(L+R)>>1;
        if(x<=mid) return query(lc,x);
        else return query(rc,x);
    }
}

存根

每次修改后最终返回的是新的根节点,所以直接用数组存就好了。

P3919 【模板】可持久化线段树 1(可持久化数组)

P3919 【模板】可持久化线段树 1(可持久化数组)

code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define lc a[u].l
#define rc a[u].r
#define L a[u].ls
#define R a[u].rs
const int M=3e7+5,N=1e6+5;

struct tree
{
    int l,r;
    int ls,rs;
    int v;
}a[M];

int n,tot,w[N],rt,root[N],m;
int New(int u)
{
    a[++tot]=a[u];//结构体直接复制所有信息
    return tot;
}
int build(int u,int l,int r)
{
    u=++tot;
    L=l,R=r;//更新区间端点
    if(l==r) 
    {
        a[u].v=w[l];
        return u;
    }
    int mid=(l+r)>>1;
    lc=build(lc,l,mid);
    rc=build(rc,mid+1,r);
    return u;
}
int modify(int u,int x,int v)
{
    u=New(u);
    if(L==R) a[u].v=v;
    else 
    {
        int mid=(L+R)>>1;
        if(x<=mid) lc=modify(lc,x,v);
        else rc=modify(rc,x,v);
    }
    return u;
}
int query(int u,int x)
{
    if(L==R) return a[u].v;
    else 
    {
        int mid=(L+R)>>1;
        if(x<=mid) return query(lc,x);
        else return query(rc,x);
    }
}
int main ()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i];
    root[0]=build(0,1,n);
    rt=0;
    for(int i=1,op,x,y;i<=m;i++)
    {
        cin>>rt>>op>>x;
        if(op==1) cin>>y,root[i]=modify(root[rt],x,y);
        else cout<<query(root[rt],x)<<"\n",root[i]=root[rt];
    }
    return 0;
}

要注意节点实际个数。

P3834 【模板】可持久化线段树 2

P3834 【模板】可持久化线段树 2

分析

首先,因为数据跨度太大,所以先离散化。嗯,不会?没事,看 离散化

然后,基于数值建立线段树,即权值线段树,维护一段值域内的数的个数。权值线段树求整个区间的第 \(k\) 小的数,就是在权值线段树上二分,从根节点开始,如果 \(k\) 比右儿子大,就说明第 \(k\) 小的数在右儿子所表示的值域中。接着,\(k\) 要减去左儿子,再进入右儿子中继续;如果 \(k\) 比左儿子小,就直接进入左儿子。

那么好,子区间 \([l,r]\) 的第 \(k\) 小的数怎么求呢?(区间都是离散化好了的)
首先思考求 \([1,r]\) 的第 \(k\) 小的数,用主席树,依次加入 \([1,r]\) 的值的节点,生成 \(r\) 个版本,从 \(root[r]\) 开始找就可以了。
那么右边有限制,左边也有限制,该怎么办呢?先明确一点,主席树,每次修改后,对于每个根节点对应的每一棵树,结构都是相同的,就是拓扑排序相同。
也就是说,所有节点对应的区间都是一一对应的。在根节点为 \(root[R]\) 的树中值域为 \([l,r]\) 的个数为 \(cnt1\),在根节点为 \(root[L-1]\) 的树中值域为 \([l,r]\) 的个数为 \(cnt2\),类似于前缀和的思想,\([L,R]\) 中 \([l,r]\) 的个数为 \(cnt1-cnt2\)。
这样就可以了,只需要在递归的时候统计个数,找到对应的那个数输出即可。

code

#include <bits/stdc++.h>
using namespace std;
#define lc a[u].l
#define rc a[u].r
#define L a[u].ls
#define R a[u].rs
const int N=2e5+5,M=3e7+5;

struct tree
{
    int l,r;
    int ls,rs;
    int cnt;
}a[M];

int n,m,tot,root[N],rt,idx;
int b[N],c[N],C[N];

int New(int u)
{
    a[++idx]=a[u];
    return idx;
}
void pushup(int u)
{
    a[u].cnt=a[lc].cnt+a[rc].cnt;
}
int build(int l,int r)
{
    int u=++idx;
    L=l,R=r;
    if(l==r) return u;
    int mid=(l+r)>>1;
    lc=build(l,mid);
    rc=build(mid+1,r);
    return u;
}
int find(int x)
{
    return lower_bound(c+1,c+tot+1,x)-c;
}
int modify(int u,int x)
{
    u=New(u);
    if(L==R) 
    {
        a[u].cnt++;
        return u;
    }
    int mid=(L+R)>>1;
    if(x<=mid) lc=modify(lc,x);
    else rc=modify(rc,x);
    pushup(u);
    return u;
}
int query(int u,int v,int x)
{
    if(L==R) return R;
    int cnt=a[a[v].l].cnt-a[a[u].l].cnt;
    if(x<=cnt) return query(a[u].l,a[v].l,x);
    else return query(a[u].r,a[v].r,x-cnt);
}
int main ()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>b[i],c[i]=b[i];
    sort(c+1,c+n+1);
    tot=n;//不用去重
    root[0]=build(1,tot);
    for(int i=1;i<=tot;i++) root[i]=modify(root[i-1],find(b[i]));
    while(m--)
    {
        int l,r,k;
        cin>>l>>r>>k;
        cout<<c[query(root[l-1],root[r],k)]<<"\n";
    }
}

本题离散化不需要去重,因为按值域加点每次只会增加一 QAQ。

标签:return,int,线段,mid,tot,持久,主席,define
From: https://www.cnblogs.com/zhouruoheng/p/17981970

相关文章

  • 「笔记」李超线段树
    目录写在前面引入线段区间修改单点查询完整代码例题P4254[JSOI2008]BlueMary开公司P3081[USACO13MAR]HillWalkG写在最后写在前面LiChaoTree也是LCT(智将和典中典之楼房重建都是\(O(\log^2n)\)地进行修改的样子,但是楼房重建是拆分完区间后再\(O(\log^2n)\)地向上......
  • 线段树二分
    问题描述维护长度为\(n\)的序列\(a\),支持以下操作:1ix:把\(a_i\)修改为\(x\)。2ix:询问最小的\(j\)满足\(j>=i\)且\(a_j>=x\)。\(1\leqn\leq10^6\)解决方法在线段树外直接二分可以做到\(O(n\log^2n)\)的时间复杂度,加上简单的剪枝可能效率会高一些,但都无......
  • 面试官:Redis持久化能关吗?怎么关?
    数据持久化是指将数据从内存中,保存到磁盘或其他持久存储介质的过程,这样做的目的是为了保证数据不丢失。而Redis的持久化功能默认是开启的,这样做的目的也是为了保证程序的稳定性(防止缓存雪崩、缓存击穿等问题)和数据不丢失。Redis持久化能关吗?怎么关?Redis持久化默认是开启的,......
  • docker容器使用存储卷进行数据持久化
    1.将存储卷"test01"挂载到容器,若不存在则直接创建,默认权限为rw[root@centos201~]#dockercontainerrun-vtest01:/usr/share/nginx/html-d--nameweb01nginx:1.20.168f7609b7d72ba6e328605103cfb315b1a38aa2631ce69a576a228d1037300aa[root@centos201~]#[17:22:......
  • docker数据持久化(存储卷)
    1.查看现有的存储卷[root@centos201~]#dockervolumels#查看现有的存储卷DRIVERVOLUMENAME[root@centos201~]#2.创建随机(匿名)的存储卷[root@centos201~]#dockervolumecreate#创建随机(匿名)的存储卷050d2f963345d595c827551adc27ee48d61d482bfcf7c86......
  • CF-240-F-线段树
    240-F题目大意给定一个长为\(n\)的字符串,由小写字母组成。由\(m\)次操作,每次操作给定一个区间\([l,r]\),要求你把区间中的字符进行重新排列,要求重排后的子串是字典序最小的回文串,如果无法得到回文串则忽略这次操作。输出\(m\)次操作之后的字符串。Solution涉及区间操作,考虑用......
  • CF-242-E-线段树
    242-E题目大意给定一个长为\(n\)的数组\(a\),\(q\)次操作,操作分两种:\(1.l,r\):输出\({\sum_{i=l}^{r}a_i}\)。\(2.l,r,x\):把区间\([l,r]\)中的元素都异或上\(x\)。Solution区间信息具有可并性,线段树能够快速得到所求区间的信息。线段树节点中维护一个数组\(bit\),\(bit[i]\)......
  • 最大异或和 可持久化数据结构入门
    最大异或和题目描述给定一个非负整数序列\(\{a\}\),初始长度为\(N\)。有\(M\)个操作,有以下两种操作类型:Ax:添加操作,表示在序列末尾添加一个数\(x\),序列的长度\(N\)加\(1\)。Qlrx:询问操作,你需要找到一个位置\(p\),满足\(l\lep\ler\),使得:\(a[p]\oplusa[p+1]......
  • 数据结构——线段树 入门以后 学习笔记
    数据结构——线段树入门以后学习笔记入门笔记有时间写。才发现我不会线段树。/ll可以看出来我很喜欢class/cf有的代码需要前置:usingll=longlong;constexprllmod=998244353;constexprintroot=1;P3372线段树1classseg_t{private:structemm{......
  • redis数据持久化篇
    为什么需要持久化Redis是个基于内存的数据库。那服务一旦宕机,内存中的数据将全部丢失。通常的解决方案是从后端数据库恢复这些数据,但后端数据库有性能瓶颈如果是大数据量的恢复,1、会对数据库带来巨大的压力,2、数据库的性能不如Redis。导致程序响应慢。所以对Redis来说,实现数......