主席树学习笔记

本文章大部分内容来自 Menci

主席树是一种数据结构,其主要应用是区间第 k 大问题。

权值线段树

传统的线段树用于维护一条线段上的区间,可以方便地查询区间信息。而如果将线段树转化为『权值线段树』,每个叶子节点存储某个元素出现次数,一条线段的总和表示区间内所有数出现次数的总和。

利用权值线段树可以方便地求出整体第 k 大 —— 从根节点向下走,如果 k 小于等于左子树大小,说明第 k 大在左子树的区间中,在左子树中继续查找即可;否则,说明第 k 大在右子树的区间中,此时将 k 减去左子树大小,并在右子树中继续查找。

查找过程类似平衡树,时间复杂度为 O(logn)

前缀和

上述算法可以用来处理整个序列上的第 k 大,而我们可以对于一个长度为 k 的序列 a 建立 n 棵上述的权值线段树,第 i 棵表示『 a1 ~ ai 的所有数』组成的权值线段树。如果要查询 [l,r] 中的第 k 大,可以使用第 r 棵线段树减去第 l1 棵线段树,得到整个区间组成的权值线段树,并进行上述算法得到区间中的第 k 大。

这个算法存在两个问题:

每个线段树要占用 O(nlogn) 的空间,算法的空间复杂度为 O(n2logn)
​​ logn),占用空间过多;
建立每棵线段树至少要用 O(nlogn) 的时间,每次查询又要用 O(nlogn) 的时间构建区间的权值线段树,总时间复杂度 O((n+m)nlogn)
看上去还不如每次直接提取出区间,并使用后线性选择得到答案的 O(n2)
​​ ) 的朴素算法优秀。

主席树

仔细思考,发现上述算法的 n 棵线段树中,相邻的两棵线段树仅有 O(logn) 个节点不同,因此本质不同的节点只有 O(nlogn) 个。我们可以充分利用这一特点,每次只重新创建与上次所不同的节点,相同的节点直接使用前一棵的即可。

为了节省空间,可以将第 0 棵线段树置为空,每次插入一个新叶子节点时接入一条长度为 O(logn) 的链。总空间、时间复杂度仍为 O(nlogn)

查询时构造整棵线段树,需要构造 O(nlogn) 个节点,但每次查询只会用到 O(logn) 个节点,直接动态构造这些节点即可。为了方便,可以不显式构造这些节点,而是直接用两棵线段树上的值相减。


这里写图片描述

模板

题目 K-th Number

#include<cstdio>
#include<vector>
#include<algorithm>
#define MAXN 100005
using namespace std;
int n,m,cnt,root[MAXN],a[MAXN],x,y,k;
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;}
void update(int l,int r,int &x,int y,int pos)
{
    T[++cnt]=T[y],T[cnt].sum++,x=cnt;
    if(l==r) return ;
    int mid=l+r>>1;
    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);
}
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()
{
    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.end());
    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]));
    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;
}

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值