主席树入门

  主席树又叫可持久化权值线段树,一开始使用来解决第k大的问题,因其发明者黄嘉泰名字的首字母和某人的一样,所以被叫做主席树。

  在了解主席树之前,我们先认识一下什么叫做权值线段树。

  给你n个数,问你这n个数中第k小的数是哪个。像这种题我们一般都是直接排序然后暴力找,但是我们今天用线段树来试试。

  例如a[12]={1,5,7,3,2,6,8,1,3,5,5,2},你要求出a数组中第7小的数的值。我们先建棵线段树用来存每个值出现的次数,按下标从小到大依次将数组中的元素放入插入线段树中。

  插入前三个数后,线段树就变成了这样

  继续插入元素,直至数组中的元素全部插入线段树中

  

  像这样按权值建的树就叫做权值线段树

  当全部插完后,就开始找第7小的数了。我们先从根开始,看看他左儿子的值是否大于k(这里k=7),假如左儿子的值大于等于k那么我们要找的第k小的数就在左边,否则就在右边。

  在这里左儿子也就是节点2的值为6,小于7,所以我们要找的数在右边,接下来我们要找以节点3为根的子树中最小的那个数。

  还是向上面那样一直递归,直到到了叶子节点,这时我们就找到了在数组中第7小的数了。

  接下来就是找区间内第k小的数了。对于任意[l,r]区间,我们先需要知道这个区间内有些什么数,然后再去找第k小。

  对于前一步,我们可以考虑建n棵权值线段树,第i棵权值线段树用来存[1,i]内各个数字的出现情况,这样我们就能直接利用前缀和的性质,让第r棵和第l-1棵权值线段树对应相减,这样我们就得到了[l,r]区间内数字的出现情况。

  然后我们就只需递归查找第k小就行了

  但是,要这样做的话我们就必须建n棵线段树,而每个线段树又要开四倍的空间,这样空间复杂度就太大了,我们必须要想办法去优化空间。

  我们仔细观察每次建树就会发现相邻两颗树只有logn个点不同,其余的都一样,就像第7棵权值线段树和第8棵权值线段树,他们只有划杠的那几个点不一样

  所以我们只需在上一颗树的基础上再开logn个点就行了,其他不变的结点直接继承上一颗树的对应结点,这样空间的开销就能接受了。

  

  

  主席树例题:https://www.luogu.com.cn/problem/P3834

  

  给你n个数,m个询问,每个问你[l,r]内的第k小的数是什么,n和m都是在1到2e5之间,数字大小在-1e9到1e9之间。

  直接上主席树,先建n棵树然后利用前缀和弄出区间内的数,最后在递归找第k小的那个数。

  

#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 200005
int n,m,cnt,a[maxn],b[maxn],root[maxn*40],L[maxn*40],R[maxn*40],sum[maxn*40];
//root存根的编号,L存左儿子的编号,R存右儿子的编号,sum存数字数量
int update(int pre,int l,int r,int pos)//pre为上个版本的编号,pos为位置
{
    int rt=++cnt;//用rt存当前结点编号
    L[rt]=L[pre];R[rt]=R[pre];
    //先将左儿子和右儿子全部等于上个版本对应的左右儿子,后面再看具体是要更新哪个儿子
    sum[rt]=sum[pre]+1;//数量加加
    if(l==r)return rt;
    int mid=l+r>>1;
    if(mid>=pos)L[rt]=update(L[pre],l,mid,pos);//更新左儿子
    else R[rt]=update(R[pre],mid+1,r,pos);//更新右儿子
    return rt;
}
int query(int x,int y,int l,int r,int k)//x和y分别代表第l-1棵树和第r棵树的结点编号,k为第k小
{
    if(l==r)return l;//返回的是离散化后的数组中的位置
    int mid=l+r>>1,tot=sum[L[y]]-sum[L[x]];
    //tot为当前结点,两棵树的左儿子所包含的数字数量之差
    if(tot>=k)return query(L[x],L[y],l,mid,k);//如果tot>=k那就意味着第k小在左子树中
    else return query(R[x],R[y],mid+1,r,k-tot);//否则在右子树中,接下来就是找右子树中第k-tot小的数
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        b[i]=a[i];
        //一般数值范围都很大,直接按数值来建树会超空间,所以经常先进行离散化,然后再按离散化后的数组建树
    }
    sort(b+1,b+1+n);//排序
    int len=unique(b+1,b+1+n)-b-1;//去重
    for(int i=1;i<=n;i++)
    {
        int pos=lower_bound(b+1,b+1+len,a[i])-b;//a[i]在b数组中的位置
        root[i]=update(root[i-1],1,len,pos);//建第i棵线段树,第pos个数出现次数加加
    }
    int l,r,k;
    while(m--)
    {
        cin>>l>>r>>k;
        cout<<b[query(root[l-1],root[r],1,len,k)]<<endl;
        /*查询第k小,利用第r棵树和第l-1棵树来找,注意query返回的是b数组中的位置
        从两棵树的根开始递归查,若左儿子满足则往左儿子递归否则往右儿子递归
        */
    }
    return 0;
}

  对于主席树的空间大小,一般为O(nlogn)(不带修改),一般情况下开个40倍就够了。

原文地址:https://www.cnblogs.com/chen99/p/12191489.html

时间: 2024-10-04 19:04:07

主席树入门的相关文章

Poj 2104(主席树入门

题目:静态查询区间第k大. 主席树入门题目,之前看的很多资料一上来就是动态区间第k大,看得很费劲,后来找了个写得清晰的,感觉静态的还不算难,代码也不长. /* * @author: Cwind */ //#pragma comment(linker, "/STACK:102400000,102400000") #include <iostream> #include <map> #include <algorithm> #include <cs

hdu 5919 主席树入门题

主席树是从右往左初始化,每次把这个数出现过的位置消去,然后在当前位置加一. 然后我的做法是查两遍,第一遍能找出不同的个数,除一半:再用这个值查,一直到底,最后返回位置,比较套路的一题. #include <stdio.h> #include <string.h> #include <iostream> #include <algorithm> #include <vector> #include <queue> #include &l

POJ 2104&amp;HDU 2665 Kth number(主席树入门+离散化)

K-th Number Time Limit: 20000MS   Memory Limit: 65536K Total Submissions: 50247   Accepted: 17101 Case Time Limit: 2000MS Description You are working for Macrohard company in data structures department. After failing your previous task about key inse

SPOJ3267--D-query (主席树入门练习)

题意:查找区间内不同数字的个数. 两种做法,一种是 树状数组离线,另一种就是主席树. 树状数组离线操作的链接 http://www.cnblogs.com/oneshot/p/4110415.html 两种方法思路差不多,都是扫一遍,如果这个数曾经出现过那么就 在上次位置-1,如果没有出现过就在 当前位置+1,同时更新该数字的最新的位置. 这样的话,在主席树里面 以x为根的线段树存的就是1-x之间不同的数字的个数.我们只需要查找以r为根的线段树同时大于l的区间内的个数就行了. 给主席跪了,orz

[知识点]主席树入门 区间k值

入坑主席树主要是因为昨天考试的后两道题不可改2333 而且觉得这个还挺有用,于是果断入坑 不过蒟蒻的我只是在上午看了看大概思路,下午开的运动会没时间码,于是晚上码了出来.但是目前只会无修改的区间K值问题,加上又比较抽象,思路屡了半天才刚刚醒悟,于是写下来记录一下. 不扯那么多辣鸡套路,直接说思路打法(以k小值为例): 我们先要以权值建一颗线段树,然后每个节点记录的是这个节点管辖的范围内数列中数的个数. 我们需要建好多好多线段树.简单来讲呢,就是[1,1],[1,2]···[1,n]这么多颗,每棵

poj2104 K-th Number 主席树入门;

题目链接:K-th Number 题解:我们先把数组离散离散化一下,然后先不考虑L,R的区间的关系,我们有一个棵线段树sum[]保存的是第几大到第几大出现的个数,这样我们想要询问这颗线段数的第k大是多少可以在log(n)次下就找到,但是区间的不同,一颗线段树是解决不了的,那我们如何得到L,R区间的sum数组呢?.我们可以建N棵线段树,第一棵树是空树,然后第一个数过来我们再建一课线段树在原来树的基础上,加上这个数对sum数组的贡献,这样从第一个到第N个建N棵线段树建好,我们可以发现sum[]有前缀

主席树入门详解+题目推荐

主席树学名可持久化线段树,就是这个可持久化,衍生了多少数据结构 为什么会有主席树这个数据结构呢?它被发明是用来解决什么问题的呢? 给定n个数,m个操作,操作类型有在某个历史版本下单点修改,输出某个历史版本下某个位置的值的值,n和m小于等于1e6 乍一看是不是一点头绪也没有.我们先来想想暴力怎么做,暴力存储第i个状态下每个数的值,显然这样做不是TLE就是MLE,我们不妨管这种状态叫做TM双LE. 如果没有这个历史状态显然处理很简单,一个线段树就解决了.那么加上历史状态呢?如果我们优化一下暴力,我们

poj2104求区间第k小,静态主席树入门模板

看了很久的主席树,最后看https://blog.csdn.net/williamsun0122/article/details/77871278这篇终于看懂了 #include <stdio.h> #include<algorithm> using namespace std; typedef long long ll; const int maxn = 1e5+5; int T[maxn],L[maxn*20],R[maxn*20],sum[maxn*20]; //sz[]为原

主席树入门野生动物园

有一个很大的野生动物园.这个动物园坐落在一个狭长的山谷内,这个区域从南到北被划分成N个区域,每个区域都饲养着一头狮子.这些狮子从北到南编号为1,2,3,…,N.每头狮子都有一个觅食能力值Ai,Ai越小觅食能力越强.饲养员cmdButtons决定对狮子进行M次投喂,每次投喂都选择一个区间[I,J],从中选取觅食能力值第K强的狮子进行投喂.值得注意的是,cmdButtons不愿意对某些区域进行过多的投喂,他认为这样有悖公平.因此cmdButtons的投喂区间是互不包含的.你的任务就是算出每次投喂后,