蒟蒻的主席树学习笔记

终于学习了传说中的主席树,开森… …

前置知识

  • 权值线段树
  • 前缀和思想

思路

题意:给定一个长为n的数列 a i a_i ai,m次询问,每次询问给定 l , r , k l,r,k l,r,k,求区间 [ l , r ] [l,r] [l,r]内的第k小数。 n , m ≤ 2 e 5 n,m\le2e5 n,m2e5
首先,我们要离散化,因为 a i a_i ai可能很大,但 n n n只有 2 e 5 2e5 2e5

然后,我们可以枚举1~n,建 n n n棵权值线段树,表示 [ 1 , i ] [1,i] [1,i]区间内每个数出现的次数。

因为每棵树的节点数都等于离散化后 s z sz sz的大小,所以每棵树是可以相减的。
于是,我们就可以很惊喜地发现:我们可以通过相减得到任意 [ l , r ] [l,r] [l,r]内的权值线段树!
这就是所谓的前缀和思想!

接下来,我们就要面对一个更严峻的问题:建 n n n个权值线段树时空复杂度都要炸!
这就来到了主席树的精髓(敲黑板)!!!
我们可以发现其实我们每建一个新的权值线段树,其中大部分节点都是不变的,最多只有 l o g n logn logn个节点发生变化,因此,我们每次只需要暴力新建这 l o g n logn logn个节点就OK了!

到此为止,一颗优秀的主席树就大功告成了!

最后就是如何查询。
这个问题在有了之前的前缀和思想以后就迎刃而解了。
结合二分,假设我们已经分别找到了第 r r r棵线段树和第 l − 1 l-1 l1棵线段树的 u , v u,v u,v节点,计算出此时 [ l , r ] [l,r] [l,r]中左儿子的 s u m sum sum c n t = t r [ t r [ u ] . l ] . s u m − t r [ t r [ v ] . l ] . s u m cnt=tr[tr[u].l].sum-tr[tr[v].l].sum cnt=tr[tr[u].l].sumtr[tr[v].l].sum。判断 c n t ≥ k cnt\ge k cntk
如果是,就在 u , v u,v u,v的左儿子中继续二分;如果不是,就在 u , v u,v u,v的右儿子中寻找第 k − c n t k-cnt kcnt大的数。

模板

#include<bits/stdc++.h>
using namespace std;

int read()
{
	int i=0;char ch;
	while(!isdigit(ch)) ch=getchar();
	while(isdigit(ch)) {i=i*10+ch-'0';ch=getchar();}
	return i;
}

const int N=2e5+5;
struct node{
	int l,r,sum;
}tr[N*20];
int n,m,l,r,p,tot,ans,a[N],b[N],rt[N];

void build(int y,int &x,int l,int r,int p)
{
	tr[x=++tot]=tr[y];
	++tr[x].sum;
	if(l==r) return;
	int mid=l+r>>1;
	if(p<=mid) build(tr[y].l,tr[x].l,l,mid,p);
	else build(tr[y].r,tr[x].r,mid+1,r,p);
}

int query(int y,int x,int l,int r,int k)
{
	if(l==r) return l;
	int s=tr[tr[x].l].sum-tr[tr[y].l].sum;
	int mid=l+r>>1;
	if(s>=k) return query(tr[y].l,tr[x].l,l,mid,k);
	else return query(tr[y].r,tr[x].r,mid+1,r,k-s);
}

int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++) a[i]=b[i]=read();
	sort(b+1,b+n+1);
	int sz=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+sz+1,a[i])-b;
	for(int i=1;i<=n;i++) build(rt[i-1],rt[i],1,sz,a[i]);
	for(int i=1;i<=m;i++)
	{
		l=read();r=read();p=read();
		ans=b[query(rt[l-1],rt[r],1,sz,p)];
		printf("%d\n",ans);
	}return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值