静态主席树

写在前面

我觉得后面参考文献中的视频讲解的很好,大家可以直接看一下那个视频!!

首先主席树解决什么样的问题?最经典的问题就是:区间第k小问题(也就是指定一个区间,要求求出这个区间中第k小的数字)

在搞懂什么是主席树之前,我们要先对权值线段树有一定的了解,下面我们就先说一下权值线段树,然后详细说一下主席树以及主席树程序的实现......

权值线段树

权值线段树:每个叶子节点的数值表示的是:数组中含有这个数值的个数是多少,父亲节点的数值=两个儿子节点数值的相加。

可以理解为:假设一个儿子表示数字x,另一个儿子表示数字y,儿子节点表示的是区间[x,x]、[y,y]中含有的数量,那么其两个的父亲节点表示的就是区间[x,y]之间含有的数量

假设数组:5 6 5 8 9 8 2 8 6,首先我们要对这个数组进行离散化,离散化后的数组为:2 3 2 4 5 4 1 4 3

那么他的权值线段树表示为:

叶子节点下面的数字表示叶子节点表示的区间[x,x]

这就是一个权值线段树,当我们查找数组中第5大的数字的时候,我们先和各节点的左儿子数字比较,8大于5,那么发现左儿子中含有8个数字,右儿子含有1个数字,那么第五位一定在左儿子中,那么进入左儿子。向下看,看当前节点的左儿子,发现左儿子为数值为3,那么5大于3,说明第五位不在左儿子中,那么一定就在右儿子中,那么在右儿子中的排名为:5-3=2,也就是在右儿子中找到第二小的数值,进入右儿子。再次判断当前节点的左儿子,左儿子数值为2,2小于等于2,那么第二小的数字就在当前节点的左儿子中,那么进入左儿子,因为当前节点已经为叶子节点,那么第5小的数值就是离散化后的3,离散化之前那么就是6

主席树

主席树实质上就是可持久化的权值线段树,可持久化指的是他保存了这棵树的所有历史版本,那么实现可持久化最简单的方法就是:每来一个新的节点n,我们就在区间[1,n]之间建立一棵权值线段树,那么如果这样做的话,内存肯定会超限,所以我们这里要做的就是尽量地去使用之前的权值线段树版本,只去更新当前节点插入后更改的节点即可

我们会发现:每插入一个节点,更新的只有根节点到更新叶子节点这条路径上的数值,这条路径上的数值都去执行+1操作,那么其余的我们使用之前版本的权值线段树就好

比如:第一张图的红色节点要添加入一个新的数值,那么更新的节点只有如第二个图所示:


我们只需要将更新红色的节点即可(新建节点),黑色的节点只需要指向前面那一棵树的节点

当我们求解区间[l,r]之间的第k小的数值的时候,我们只需要使用第r个线段树减第l-1个线段树(这里可以动手画一下,比如上面两棵权值线段树相减),那么这个线段树就是区间[l,r]之间的线段树了,求解第k小的数值就和上面权值线段树描述的步骤是相同的。

程序实现     

poj 2104

#include <iostream>
#include <vector>
#include <algorithm>
#include <cstdio>

using namespace std;
const int maxn = 1e5+6;
int n,m,cnt,root[maxn];       //root表示很节点的编号
int x,y,k,a[maxn];

struct Node
{
    int l,r,sum;       //左儿子、右儿子、当前节点的含有的个数
}T[maxn*40];

vector<int> v;         //离散化操作
int getid(int x){return lower_bound(v.begin(),v.end(),x)-v.begin()+1;}

/*
更新操作:也就是从根节点到要更新的叶子节点,我们只是寻找这个路径上的点,其余的不发生改变(包含节点的左儿子或者右儿子),当更新这路径上的节点的时候,
我们每更新一个节点,就要新建一个节点,当前节点对应于之前一个状态的sum值要+1,一直到叶子节点!!!
*/
void update(int l,int r,int &x,int y,int pos)    //pos:更新的位置     x:当前更新你的权值线段树
{
    T[++cnt] = T[y];     //后面新添节点的话,节点的sum一定会相对于前面的+1,并且这个不变的叶子节点也会跟着一起更新过来
    T[cnt].sum++;
    x = cnt;             //新的根节点!指向
    if(l == r) return ;      //表示已经到达叶子节点
    int mid = (l+r)>>1;      //得到中间节点的编号,要和更新位置pos相比较,判断更新左子树还是更新右子树
    if(mid >= pos) update(l,mid,T[x].l,T[y].l,pos);
    else update(mid+1,r,T[x].r,T[y].r,pos);
}
/*
查询操作:查询在根节点状态x-y之间,的第k大的数字
*/
int query(int l,int r,int x,int y,int k)
{
    if(l == r) return l;     //返回的离散后的位置
    int mid = (l+r)>>1;
    int sum = T[T[y].l].sum - T[T[x].l].sum;     //得到中间这个状态的左子树的权值
    if(sum >= k) return query(l,mid,T[x].l,T[y].l,k);   //在左子树中
    else                                                //在右子树中
        return query(mid+1,r,T[x].r,T[y].r,k-sum);  //右子树时排的名次也会发生变化@
}
int main()
{
    cnt = 0;
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i ++)
    {
        scanf("%d",&a[i]);
        v.push_back(a[i]);
    }
    sort(v.begin(),v.erase(unique(v.begin(),v.end()),v.end()));                     //
    for(int i =1;i <= n;i ++)
        update(1,n,root[i],root[i-1],getid(a[i]));            //每来一个节点,就从起始节点到当前节点建立一棵权值线段树,但是我们只根节点到当前节点路径上的节点,别的可以直接链接到即可!
    //创建的第i个权值线段树,需要更新的位置为getid(a[i])
    for(int i = 1;i <= m;i ++)
    {
        scanf("%d%d%d",&x,&y,&k);
        printf("%d\n",v[query(1,n,root[x-1],root[y],k)-1]);
    }
    return 0;
}

参考文献

B站的这个视频讲解的很好,虽然只有简单的几分钟,第一部分是算法讲解,第二部分是程序的讲解:https://www.bilibili.com/video/av4619406/?p=1


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值