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

前言


每次比赛总是有那么几个搞数据结构的题目,从最开始了解的单调栈,单调队列,线段树再到后来的主席树,树链剖分,后缀自动机,虽然有的还没有学,看来数据结构没我想的那么好搞,光是搞懂一个新的知识点就要花点功夫,慢慢做题就会发现,其实数据结构还是难在加上思维去运用。

主席树据说是一个叫黄嘉泰的同学发明的,至于为啥叫“主席树呢”,你看下这位大佬的缩写$(HJT)$是不是我们历届哪位主席,是吧,咋们不敢说也不敢问

抛出问题


 给定$N$个数,共有 $M$次询问,每次都要询问区间 $[l,r]$的第 $k$大的数。其中 $N,M,l,r$均不超过$2 imes 10^{5}$ ,保证询问有答案。

解决问题


 暴力法

 显而易见,最暴力的办法就是区间排序然后输出排序后第$k$个数。最坏情况的时间复杂度是$ O(nmlgn)$,不超时才怪。

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

 于是针对这个问题,新的数据结构诞生了,也就是主席树。

 主席树本名可持久化线段树,也就是说,主席树是基于线段树发展而来的一种数据结构。其前缀"可持久化"意在给线段树增加一些历史点来维护历史数据,使得我们能在较短时间内查询历史数据。

我们先来看一段话来了解下主席树

首先,学习主席树要点的前置技能是权值线段树。权值线段树之所以会带上“权值”二字,是因为它是记录权值的线段树。因此需要用到离散化操作来处理a[1-n]。记录权值指的是,每个点上存的是区间内的数字出现的总次数。比如一个长度为10的数组[1,1,2,3,3,4,4,4,4,5]。

其中1出现了两次,那么[1,1]这个节点的值为2,2出现了1次,那么[2,2]这个节点的值为1,那么显然[1,2]这个节点的值为3,即1出现的次数和2出现的次数加和。那么如果我想要知道这个数组上的第k小,我就可以在这棵权值线段树上用logn的时间来实现。比如我想要求这个区间上的第7小,那么我先找到这棵树的根节点,根节点上的数字显示的是10,表示在[1,8]这个区间上一共有10个数字,那么我只要去看它的左孩子上的个数是多少。这时我看到左孩子上的数字是9,说明前9小的数字都在左子树上,那么我要找的第7小也在左子树上,那么我就递归去找左子树。当我再看左孩子的时候,看到数字是3,说明前3小的数字在左子树上,那么我要找的就是右子树上的第k-sum[i]小,即7-3=4,找到右子树上的第4小即可。直到找到某一个叶子节点,说明找到了我要找的第k小。这是通过权值线段树找到区间[1,n]上的第k小/大的应用。

 

那么知道了权值线段树是什么之后,主席树又是什么呢。主席树是一棵可持久化线段树,可持久化指的是它保存了这棵树的所有历史版本,最简单的办法是:如果你输入了n个数,那么每输入一个数字a[i],就构造一棵保存了从a[1]到a[i]的权值线段树。之所以这么做,是因为我们可以把第j棵树和第(i-1)棵树上的每个点的权值相减,来得到一颗新的权值线段树,而这个新的权值线段树相当于是输入了a[i]到a[j]以后得到的。如果这么说不太好理解的话,我们可以思考另外一个模型:求数组a[1]到a[n]的和。如果只是求[1,n]这一段的和,那么我们直接全部加起来就可以了,或者求一个前缀和sum[n]即可。那么如果我给定了l和r,想要知道[l,r]这段区间上的和呢?是不是利用前缀和sum[r]-sum[l-1]就可以轻松得到?那么主席树的思想也是如此,将tree[r]-tree[l-1]得到的一棵权值线段树即为属于[l,r]的一棵权值线段树,那么在这么一棵权值线段树上求第k大不是就转变为之前的问题了么。如果还是没有理解为什么可以用tree[r]-tree[l-1]来表示属于[l,r]的权值线段树,可以自己构造一个数组,然后画出属于[1,l-1],[1,r]和[l,r]的三颗权值线段树,来自己研究研究,多自己动手也不是一件坏事嘛。

想必看到这里你对主席树应该有了大概的印象

接下来我们先看道权值线段树的例题:[NOI2004]郁闷的出纳员 

说起权值线段树,其实写完后发现真的和线段树差不多,线段树维护的是下标区间内的信息,而权值线段树维护的是权值区间内的信息,例如维护权值属于[2,7]这个区间中各个数出现的次数

本题维护一个偏移量$infu$,当 $A$ 操作时,加工资$ infu+=x$;$S$ 操作 $infu-=x$,这时有区间修改,低于最低工资的要清0 。注意在有新员工插入的时候加入$x -infu$即可,这样仍然是全局偏移量

所有数据在处理的时候都要加上$base$,$base$ 是防止负数出现

Code

#include <cstdio>
#include <algorithm>
#define lson rt<<1, l , mid
#define rson rt<<1|1, mid+1 , r
using namespace std;
const int maxn = 4e5 + 20;
const int base = 2e5 + 10;
int n, mink, k;
char ch;
int tree[maxn<<2], lazy[maxn<<2];
void push_up(int rt){
    tree[rt] = tree[rt<<1] + tree[rt<<1|1];
}
void push_down(int rt){
    if(!lazy[rt]) return;
    tree[rt<<1] = tree[rt<<1|1] = 0;
    lazy[rt] = 0;
    lazy[rt<<1] = lazy[rt<<1|1] = 1;
}
//单点修改 
void insert(int rt, int l, int r, int pos){
    if(l==r){
        tree[rt]++; //tree[rt]才代表区间[pos,pos]节点的值 
        return ;
    }
    push_down(rt); //单调修改也需要push_down, 不然会WA(废话)
    int mid = (l+r)>>1;
    if(pos<=mid) insert(lson, pos);
    else insert(rson, pos);
    push_up(rt); //其实用tree[rt]++也可,这点在主席树里面会有所体现 
}
//区间修改 
void update(int rt, int l, int r, int ul, int ur){
    if(ul<=l&&r<=ur){
        tree[rt] = 0;
        lazy[rt] = 1;
        return ;
    }
    push_down(rt);
    int mid = (l+r)>>1;
    if(ul<=mid) update(lson, ul, ur);
    if(ur>mid) update(rson, ul, ur);
    push_up(rt); 
}
//全区间查询第k大 
int query(int rt, int l, int r, int k){
    if(l==r) return l;
    push_down(rt);
    int mid = (l+r)>>1;
    if(tree[rt<<1|1]>=k) return query(rson, k);
    else return query(lson, k-tree[rt<<1|1]);
}
 
int main(){
    scanf("%d%d", &n, &mink);
    int ans = 0, infu = 0;
    while(n--){
        scanf(" %c%d", &ch ,&k);
        if(ch=='I'){
            if(k<mink) continue;
            ans++;
            insert(1, 1, maxn, k-infu+base);         //k-infu+base(注意读题,是当集体扣工资时才会有人离开公司) 
        }
        else if(ch=='A') infu += k;
        else if(ch=='S'){
            infu -= k;
            update(1, 1, maxn, 1, mink-infu+base-1); //x+infu<mink, x<mink-infu,区间是闭区间,所以 mink-infu+base-1
        }
        else {
            if(tree[1]<k) printf("-1
");
            else printf("%d
", query(1, 1, maxn, k)-base+infu);
        }
    }
    printf("%d
",ans-tree[1]);
    return 0;
}
 
View Code

我们的前置技能点完了,下面正式来看我们又爱又恨的主席树

[POJ 2104]K-th Number

#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 4e6+10;
int n, q, pntnum = 0;//pnt是Persistable_Segment_Tree的缩写 
int a[maxn], b[maxn];
int rt[maxn], ls[maxn], rs[maxn], tree[maxn];
void build(int &pos,int l,int r){ //这里传的是&pos!!! 
    pos = ++pntnum;
    if(l==r) return;
    int mid=(l+r)>>1;
    build(ls[pos],l,mid);
    build(rs[pos],mid+1,r);
}
//单点修改
void insert(int &pos,int vsn,int l,int r,int loc){//pos新版本的当前节点编号,vsn旧版本的当前节点编号,l左端点,r右端点,loc要修改的节点编号
    pos = ++pntnum; //新增节点 
    if(l==r){
        tree[pos]=tree[vsn]+1;//当前节点加1
        return;
    }
    ls[pos]=ls[vsn]; //继承左子树
    rs[pos]=rs[vsn]; //继承右子树
    tree[pos]=tree[vsn]+1; //当前路径上的所有点权值加1,这里相当于push_up,只不过对于这道题直接在父节点写比较方便 
    int mid=(l+r)>>1;
    if(loc<=mid) insert(ls[pos],ls[vsn],l,mid,loc);
    else insert(rs[pos],rs[vsn],mid+1,r,loc);
}
//查询第k小 
int query(int lv,int rv,int l,int r,int k){
    if(l==r) return l;
    int mid=(l+r)>>1, sum = tree[ls[rv]]-tree[ls[lv]]; 
    if(sum>=k) return query(ls[lv],ls[rv],l,mid,k);
    else return query(rs[lv],rs[rv],mid+1,r,k-sum);
}
int main(){
    scanf("%d%d", &n, &q);
    for(int i = 1; i <= n; i++)
        scanf("%d", &a[i]), b[i] = a[i];
    sort(b+1, b+n+1);
    int m = unique(b+1, b+n+1)-b-1;
    build(rt[0], 1, m);
    for(int i = 1; i <= n; i++){
        int p = lower_bound(b+1, b+1+m, a[i])-b;//离散化映射 
        insert(rt[i], rt[i-1], 1, m, p);
    }
    while(q--){
        int l, r, k;
        scanf("%d%d%d", &l, &r, &k);
        int ans = query(rt[l-1], rt[r], 1, m, k);
        printf("%d
", b[ans]);
    }
    return 0;
}
View Code
原文地址:https://www.cnblogs.com/wizarderror/p/11779845.html