主席树入门详解

简介

主席树全称应为可持久化权值线段树,可用于解决区间第k小问题。但可能由于这个的影响力比较大,现在主席树也被泛指为可持久化线段树。
主席树树是由NOI选手黄嘉泰(HJT,咱也不敢说)首先使用的。黄嘉泰原话:“这种求k 大的方法(函数式线段树)应该是我最早开始用的”。

可持久化线段树

可持久化数据结构 (Persistent data structure) 是可以保留每一个历史版本,并且支持操作的不可变特性 (immutable)。
可持久化线段树即保留多个历史版本(第k个版本即第k次修改后,第k+1次版本修改前的版本)的线段树,支持对历史版本的访问和修改。

关于空间的讨论

建一个线段树的空间是O(N)的,如果有M次修改就需要O(M*N)的空间?这显然是不行的
(只会单点修改的可持久化线段树,所以下面提的修改,均为单点修改。)

若熟悉线段树应该不难发现,一次单点修改只会影响被修改的叶子结点到根这条路径上的结点,所以被影响的结点个数显然是 O ( l o g N ) O(logN) O(logN)的。

举个栗子

原序列为:A[1000,200,30,4]。
这是一颗维护区间和的线段树。
在这里插入图片描述
现在要,先将A[3]修改成50,然后将A[2]修改成600。过程如下。
在这里插入图片描述
在这里插入图片描述
发现每次确实只修改了上个版本logn个点的信息。于是乎,为了节省空间,我们每次新建一颗树,可以依托于上一个版本的线段树,如下。
第一次操作:把第 3 个位置改为 50。我们只要新建 log n 个新点,剩下的结点与第0版本共用。
在这里插入图片描述
得到第一版本线段树。
在这里插入图片描述
第二次操作:把第二个位置改成50,同样新建logn个点,其余与第1。
在这里插入图片描述
得到第二版本线段树。
在这里插入图片描述
完整视图。
在这里插入图片描述
由这个例子可以发现,每次修改操作都会增加 logn个点,所以空间复杂度变成了 nlog,一般开32倍的数组就行了
时间复杂度依旧是 O(mlogn)。
用数组实现的时候不能用 2x和 2x+1表示左右孩子结点了,所以每个结点需要增加Ls和Rs两个指针。

例题

模板题洛谷P3919
题目描述

如题,你需要维护这样的一个长度为 NN 的数组,支持如下几种操作

在某个历史版本上修改某一个位置上的值

访问某个历史版本上的某一位置的值

此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)

参考代码:

#include<bits/stdc++.h>

using  namespace std;
const int maxn=1e6+5;
int a[maxn];

struct tree
{
    int ls,rs,val;//左右孩子编号,
}node[maxn<<5];

int root[maxn];//每棵线段树根节点编号
int node_cnt;
void build(int &idx,int l,int r)
{
    idx=node_cnt++;
    if(l==r)
    {
        node[idx].val=a[l];
        return;
    }
    int mid=l+r>>1;
    build(node[idx].ls,l,mid);
    build(node[idx].rs,mid+1,r);
}
//依托的版本和新版本线段树编号,当前结点左右边界,待修改位置和值
void modify(int pre,int &now,int l,int r,const int &loc,const int &val)
{
    now=node_cnt++;
    node[now]=node[pre];
    if(l==r)
    {
        node[now].val=val;
        return;
    }
    int mid=l+r>>1;
    if(loc<=mid)
        modify(node[pre].ls,node[now].ls,l,mid,loc,val);
    else
        modify(node[pre].rs,node[now].rs,mid+1,r,loc,val);
}
//
int query(int now,int l,int r,const int loc)
{
    if(l==r)
        return node[now].val;
    int mid=l+r>>1;
    if(loc<=mid)
        return query(node[now].ls,l,mid,loc);
    else
        return query(node[now].rs,mid+1,r,loc);
}
int main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int n,m;
    cin>>n>>m;
    for (int i = 1; i <= n; i++)
        cin>>a[i];
    build(root[0],1,n);
    for(int i=1;i<=m;i++)
    {
        int v,opt,loc,val;
        cin>>v>>opt;
        if(opt==1)//修改
        {
            cin>>loc>>val;
            modify(root[v],root[i],1,n,loc,val);
        }
        else{
            cin>>loc;
            int ans=query(root[v],1,n,loc);
            root[i]=root[v];
            cout<<ans<<'\n';
        }
    }
    
    return 0;
}

权值线段树

个人感觉叫值域线段树更符合。
每个结点存储的信息为,当前值域的数的个数。如图所示:
在这里插入图片描述
但如果数字值域很大的话,例如[0,1e9],这样建树显然是不行的。而事实上,我们只关心数的大小关系就可。因此,我们需要也可以进行离散化。
以序列{245, 112, 45322, 98988}为例,把序列离散化为{2, 1, 4, 3},得到值域线段树如下:
在这里插入图片描述
若想要在值域线段树上查询第k小,我们只需要拿k和当前结点的左孩子权值(设为val[lson])和k比较,
如果k<=val[lson],去左子树找第k大的数,
否则去右子树找第k-val[lson]大的数。

主席树

将权值线段树进行可持久化就得到主席树啦。
具体是把每个序列区间[1,i]分别建一棵权值线段树,并称其为第 i i i版本线段树。如下图所示:
在这里插入图片描述
怎样查询序列中区间[L, R]的第k小。

如果能得到区间[L, R]的线段树,就能高效率地查询出第k小。根据前缀和的思想,区间[L, R]包含的元素等于区间[ 1 , R ] 减去区间[ 1 , L − 1 ]。把前缀和思想用于线段树的减法,线段树的减法,是在两棵结构完全的树上,把所有对应结点的权值相减。线段树R减去线段树L − 1 ,就得到了区间[ L , R ] 的线段树。
  例如区间[2, 4]的线段树,等于把第4个线段树与第1个线段树相减(对应圆圈内的数字相减),得到下图的线段树:
  在这里插入图片描述
但事实上,我们并不需要真的将两个线段树相减得到一棵新的树,只需要同时访问两个线段即可。类似于上述的可持久化线段树中的修改操作,具体见代码实现。
洛谷P3834
参考代码

#include <bits/stdc++.h>

using namespace std;

const int maxn = 2e5 + 5;

int num[maxn], num2[maxn];
int root[maxn];

struct node
{
    int l, r, val; //左右孩子下标,值域内数字个数
} Node[maxn << 5];
int tot, node_cnt;
//前一个版本,当前版本,当前结点表示值域,要插入的值
void modify(int pre, int &now, int L, int R, const int &id)
{
    now = ++node_cnt;
    Node[now] = Node[pre];
    Node[now].val++;
    if (L == R)
        return;
    int mid = (L + R) >> 1;
    if (id <= mid)
        modify(Node[pre].l, Node[now].l, L, mid, id);
    else
        modify(Node[pre].r, Node[now].r, mid + 1, R, id);
}
//第l-1和第r个版本编号对应结点编号,当前结点表示值域,要查询的数
int query(int lx, int rx, int L, int R, int k)
{
    int mid = (L + R) >> 1;
    int sub = Node[Node[rx].l].val - Node[Node[lx].l].val;//获得当前序列区间对应线段树的值
    if (L == R)
        return L;
    if (k <= sub)
        return query(Node[lx].l, Node[rx].l, L, mid, k);
    return query(Node[lx].r, Node[rx].r, mid + 1, R, k - sub);
}
int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        scanf("%d", num + i), num2[i] = num[i];
    //离散化
    sort(num2, num2 + n + 1);
    tot = unique(num2 + 1, num2 + n + 1) - num2 - 1;
    for (int i = 1; i <= n; i++)
    {
        int id = lower_bound(num2 + 1, num2 + tot + 1, num[i]) - num2;
        modify(root[i - 1], root[i], 1, tot, id);
        //cout<<id<<endl;
    }
    while (m--)
    {
        int l, r, k;
        scanf("%d%d%d", &l, &r, &k);
        int loc = query(root[l - 1], root[r], 1, tot, k);
        printf("%d\n", num2[loc]);
    }
    return 0;
}

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值