ACM算法总结 数据结构(三)(可持久化数据结构)

本文深入解析可持久化线段树和主席树的数据结构与算法原理,探讨其在处理历史版本信息、区间查询及更新操作中的应用。通过实例说明,展示如何利用这些高级数据结构解决复杂问题,如区间第k小值查询。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >



可持久化线段树

之所以说这种结构可持久化,是因为它存储了历史版本信息。对于可持久化线段树来说,我们可以找到任何历史修改版本的区间信息。

不论是单点修改,还是区间修改,线段树+懒惰标记就决定了每次修改的复杂度是 O(logn) 级别的,也就是说,每次修改只会改变 logn 个结点的值。那么我们对于当前的新线段树,就只用建立 logn 个新结点,所以总空间复杂度是 O(nlogn) 。

下图是一个维护区间最小值的线段树,红色结点表示当执行“将第二个元素修改为1”的操作时建立的新结点。

对于每一次操作,我们建立一个新的根结点,然后看它的两个儿子是否被修改,如果需要修改,我们就将对应的儿子指向一个新建立的儿子,并且递归处理这个儿子;如果不需要修改,我们直接指向原来的儿子。注意这里的操作可以基于之前任意的历史版本。

这样的一个数据结构,对于每一个根结点,我们都可以“提取”出一个完整的线段树,所以我们分析的时候,可以看成有 n 棵完整的线段树,并且每棵线段树都有一样的结构。另外这里的儿子结点编号再也不满足 2k 和 2k+1 的规律,所以我们要记录每个结点的儿子编号。




主席树

主席树就是一棵挂满了主席(比如学生会主席)的树。

主席树也叫做可持久化权值线段树,表示它是一种可持久化线段树,并且每棵线段树都是权值线段树。

主席树通常用于解决区间第 k 小的问题,我们建立一个可持久化的权值线段树,并且第 q 棵权值线段树(也就是对应根结点 rt[q] )记录的是数列前 q 项的信息,也就是数列前 q 项中每个数出现了多少次。这里可以看出主席树维护的是一个前缀信息。然后区间第 k 小的处理过程就有前缀差分的思想,因为出现次数是可以作差的,所以对于区间 [l,r] 的信息,我们可以用线段树 rt[r] 减去线段树 rt[l-1](这里的减法运算是指对应线段树的每个结点的值作差),然后在上面寻找第 k 小的数即可。

一般来说我们都要预处理出涉及到的所有数,然后用离散化的方法。

下面是主席树的模板,对应题目 第k小数 或者题目 可持久化线段树1

#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define REP(i,a,b) for(int i=(a);i<=(int)(b);i++)
#define REP_(i,a,b) for(int i=(a);i>=(b);i--)
#define pb push_back
using namespace std;
typedef long long LL;
typedef vector<int> VI;
int read()
{
    int x=0,flag=1;
    char c=getchar();
    while((c>'9' || c<'0') && c!='-') c=getchar();
    if(c=='-') flag=0,c=getchar();
    while(c<='9' && c>='0') {x=(x<<3)+(x<<1)+c-'0';c=getchar();}
    return flag?x:-x;
}

template<class T>
struct president_tree
{
    T *a,*aa;
    int n,len,tot,*rt,*L,*R,*t;
    #define mid ((l+r)>>1)
    #define ID(x) (lower_bound(aa+1,aa+len+1,x)-aa)

    president_tree(int n,T *origin)
    {
        this->n=n; len=tot=0;
        a=new T[n+5](); aa=new T[n+5](); t=new int[n*30]();
        rt=new int[n*30](); L=new int[n*30](); R=new int[n*30]();
        REP(i,1,n) a[i]=aa[i]=origin[i];
        sort(aa+1,aa+n+1);
        len=unique(aa+1,aa+n+1)-aa-1;
        rt[0]=build_(1,n);
        REP(i,1,n) rt[i]=update_(rt[i-1],1,n,ID(a[i]));
    }

    int build_(int l,int r)
    {
        int k=++tot;
        if(l>=r) return k;
        L[k]=build_(l,mid); R[k]=build_(mid+1,r);
        return k;
    }

    int update_(int k,int l,int r,int loc)
    {
        int now=++tot;
        L[now]=L[k]; R[now]=R[k]; t[now]=t[k];
        if(l==r) {t[now]++; return now;}
        if(loc<=mid) L[now]=update_(L[k],l,mid,loc);
        else R[now]=update_(R[k],mid+1,r,loc);
        t[now]=t[L[now]]+t[R[now]];
        return now;
    }

    T query_(int l,int r,int ll,int rr,int rk)
    {
        if(l==r) return aa[l];
        int x=t[L[rr]]-t[L[ll]];
        if(x>=rk) return query_(l,mid,L[ll],L[rr],rk);
        else return query_(mid+1,r,R[ll],R[rr],rk-x);
    }
    T query(int l,int r,int rk) {return query_(1,n,rt[l-1],rt[r],rk);}
};

const int maxn=2e5+5;
int a[maxn];

int main()
{
    //freopen("input.txt","r",stdin);
    int n=read(),q=read();
    REP(i,1,n) a[i]=read();
    president_tree<int> t(n,a);
    while(q--)
    {
        int l=read(),r=read(),k=read();
        printf("%d\n",t.query(l,r,k));
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值