主席树详解

主席树详解

要想学主席树,首先要搞懂可持久化线段树,因为主席树运用了它的思想。

主席树的模板题是:静态查询区间第k小

那么主席树的做法就是, 先把全部数字离散化,然后每一个前缀建一棵权值线段树,显然,如果直接建,那么空间上是不允许的,但是我们发现,每两个相邻的前缀中,只有一个数的差别,所以,他们的公共部分是很多的,所以我们就可以用可持久化线段树了!

首先,要明确一下,主席树上每个结点的值的定义(我们设这个节点的管理范围为l~r,并且现在已经将这个数列离散化了):表示在当前前缀中,有多少个数的值在l ~ r中。

比如有下面这个数列{9,3,11,6},离散化之后就是{3,1,4,2},那么我们要以每一个前缀建一棵线段树,首先,先建一棵空的线段树,表示当前什么数也没有,然后我们把数列中的数一个一个加进去就好了,就像这样(红色数字表示每个结点的值,意义如上所述):
在这里插入图片描述
那么首先加入第一个数-----3,这颗树就会变成(结点里是这个结点法管理范围,方便理解):
在这里插入图片描述
再加上一个人数-----1,注意要在上一个人前缀法基础上加:
在这里插入图片描述
以此类推即可。。。。最后得到的图是这个样子的
在这里插入图片描述

好的,主席树就这么造出来了!那接下去就是查询,为了更好理解,我们先从简单的开始,假如要查询1~3范围内第k小的数,假设k=2,那么直接从 1 ~ 3这个前缀的根开始找,也就是绿色的那个根,那么怎么找呢,因为可以保证,左边的数都要比右边的数要小,所以我们发现,现在要找的是第2小,但左儿子只有1,说明在 1 ~ 3这个前缀里,在 1 ~ 2范围内的只有一个数,那么第2一定在右儿子里,于是我们往右儿子那里走一步,但是如果往右儿子那走,k就要减去左儿子的的值。然后因为k减小了1,所以我们现在找的就是第1小,然后发现当前节点的左儿子的值为1,k<=1,所以第k小一定在左儿子里,于是就走过去,然后发现这是叶子节点,他管理的范围是3 ~ 3,说明这个第2小的数离散化之后的数是3,那么我们输出这个数原来是多少就好了,也就是9.

好的我们解决了在某个前缀中如何查询第k小,那么,现在要解决的问题是,查询区间第k小。

可能有人就会想到,你用前缀和来维护这个东西,那么要求一个区间就可以直接用前缀和相减麽!

没错!就是相减。比如现在要求区间l~r的第k小,那么我们可以用a来存1 ~ r这个前缀和的根,b存1 ~ l-1,那每一次向下查找左右儿子的值的时候就先需要用a的左右儿子减去b的左右儿子的值,这样就可以去掉1 ~ l-1中的数,只留下l ~ r的区间,然后像上面一样查找即可。

模板1:

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10,M=5e6+10;
int n,m,len=0,cnt=0;
int a[N],first[N];
struct nod
{
    int x,y;
}b[N];
struct node
{
    int l,r,zuo,you,c;
}tree[M];
int gen[N];
bool cmp(nod x,nod y)
{
    return x.x<y.x;
}
void buildtree(int x,int y)//标准的线段树建树
{
    len++;
    tree[len].l=x,tree[len].r=y;
    tree[len].c=0;
    tree[len].zuo=tree[len].you=-1;
    int now=len,mid=(x+y)/2;
    if(x<y)
    {
        tree[now].zuo=len+1;
        buildtree(x,mid);
        tree[now].you=len+1;
        buildtree(mid+1,y);
    }
}
void add(int x)
{
    int now=gen[x-1];   //从上一个前缀开始,now记录到了那个节点
    gen[x]=len+1;
    do
    {
        int zuo=tree[now].zuo,you=tree[now].you;//记录下当前节点的左右儿子
        len++;  //新增一个节点
        tree[len].c=tree[now].c+1; //这个节点的值+1,因为有值的变动才会新建
        tree[len].l=tree[now].l;//管理范围不变
        tree[len].r=tree[now].r;
        if(zuo==-1)//加入当前这个是叶子节点
        {
            tree[len].zuo=tree[len].you=-1;//标记没有左右儿子
            break;
        }
        if(a[x]<=tree[zuo].r)
        {//假如比当前结点的左儿子的管理范围的右端点小
            tree[len].you=you;  //右儿子不变直接使用
            tree[len].zuo=len+1;  //新建左儿子
            now=zuo;    //往下走
        }
        else    //同上
        {   
            tree[len].zuo=zuo;
            tree[len].you=len+1;
            now=you;
        }
    } while(now!=-1);
}
int find(int x,int y,int k)//查询x~y区间内的第k小
{
    int a=gen[x-1],b=gen[y];//找到1~x-1这个前缀的根以及1~y的根
    while(tree[a].zuo!=-1) //加入还不是叶子节点
    {
        int zuo=tree[tree[b].zuo].c-tree[tree[a].zuo].c;
        int you=tree[tree[b].you].c-tree[tree[a].you].c;
        if(k<=zuo) a=tree[a].zuo,b=tree[b].zuo;
        else a=tree[a].you,b=tree[b].you,k-=zuo;
    }
    return tree[b].l;  //返回这个第k小的数
}
int main()
{
    scanf("%d%d",&n,&m);    //n个数,m个查询
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        b[i].x=a[i],b[i].y=i;   //为离散化做准备
    }
    buildtree(1,n);
    sort(b+1,b+n+1,cmp);    //排序,按b[i].x为关键字
    b[0].x=-375024688;
    for(int i=1;i<=n;i++)
    {
        if(b[i].x!=b[i-1].x) a[b[i].y]=++cnt;   //直接在a数组上修改
        else a[b[i].y]=cnt;
        first[cnt]=b[i].x;  //标记一下cnt这个离散化的数原来是多少
    }
    gen[0]=1;   //记录每个前缀的根,gen表示的是什么都没有的一开始的线段树的根
    for(int i=1;i<=n;i++) add(i);   //逐个添加
    for(int i=1;i<=m;i++)
    {
        int x,y,k;
        scanf("%d%d%d",&x,&y,&k); //每次询问查询区间内的第k小
        printf("%d\n",first[find(x,y,k)]);
    }
    //system("pause");
    return 0;
}

模板2:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n,m;
int cnt;
struct node
{
    int L,R;   //分别指向左右子树
    int sum;   //该节点所管辖区间范围内数的个数
    node()
    {
        sum=0;
    }
}Tree[maxn*20]; 
struct value
{
    int x;  //值的大小
    int id; //离散前在原数组的位置
}Value[maxn];
bool cmp(value v1,value v2)
{
    return v1.x<v2.x;
}
int root[maxn]; //多棵线段树的根节点
int ans[maxn];//原数组离散后的数组
void init()
{
    cnt=1;
    root[0]=0;
    Tree[0].L=Tree[0].R=Tree[0].sum=0;
}
void update(int num,int &rt,int l,int r)
{
    Tree[cnt++]=Tree[rt];   //将上一棵树的点复制到当前这棵树
    rt=cnt-1;  //当前这个节点的编号
    Tree[rt].sum++;
    if(l==r) return ;
    int mid=(l+r)/2;
    if(num<=mid) update(num,Tree[rt].L,l,mid);
    else update(num,Tree[rt].R,mid+1,r);
}
int query(int i,int j,int k,int l,int r)
{
    int d=Tree[Tree[j].L].sum-Tree[Tree[i].L].sum;
    if(l==r) return l;
    int mid=(l+r)/2;
    if(k<=d) return query(Tree[i].L,Tree[j].L,k,l,mid);
    else return query(Tree[i].R,Tree[j].R,k-d,mid+1,r);
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&Value[i].x);
        Value[i].id=i;
    }
    //进行离散化
    sort(Value+1,Value+n+1,cmp);
    for(int i=1;i<=n;i++)
        ans[Value[i].id]=i;
    init();
    for(int i=1;i<=n;i++)
    {
        root[i]=root[i-1];
        update(ans[i],root[i],1,n);
    }
    int left,right,k;
     for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&left,&right,&k);
        printf("%d\n",Value[query(root[left - 1], root[right], k, 1, n)].x);
    }
    system("pause");
    return 0;
}
  • 7
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值