数据结构之主席树

这里讲静态的主席树,关于静态区间第k小。(有兴趣的朋友还可以去看看我写的整体二分,代码实现略优于主席树我觉得,当然静态主席树是很好写的)

题目描述:

题目描述

如题,给定N个正整数构成的序列,将对于指定的闭区间查询其区间内的第K小值。

输入输出格式

输入格式:

 

第一行包含两个正整数N、M,分别表示序列的长度和查询的个数。

第二行包含N个正整数,表示这个序列各项的数字。

接下来M行每行包含三个整数 l, r, kl,r,k , 表示查询区间 [l, r][l,r] 内的第k小值。

 

输出格式:

 

输出包含k行,每行1个正整数,依次表示每一次查询的结果

 

输入输出样例

输入样例#1: 
5 5
25957 6405 15770 26287 26465 
2 2 1
3 4 1
4 5 1
1 2 2
4 4 1
输出样例#1:
6405
15770
26287
25957
26287

那么我们明确主席树是个什么东西。它就是,可持久化线段树。
首先我们不考虑主席树,而是对于这个区间第k小问题做一个分析。如果我们对每一个区间暴力快排,毋庸置疑,绝对炸上天,T到你想哭。
于是聪明的前人发现了这个问题的一个特点,下面我来说说。
先把每一个数值离散化,树中每一个值就即是权值也是排名了(离散化建议看看我之前写的离散化博客里的代码)。好吧我到现在还没有说明这个树到底是个什么玩意儿。

   发明者的原话:“对于原序列的每一个前缀[1···i]建立出一棵线段树维护值域上每个数出现的次数,则其树是可减的”

   可以加减的理由:主席树的每个节点保存的是一颗线段树,维护的区间信息,结构相同,因此具有可加减性(关键)

   是的,我们对于每一个前缀建一颗树,每一段区间维护的是当前区间的点的个数,注意这里的区间不是位置的区间,而是权值的区间,

   这就是为什么要离散化了。

   那么减可以干什么呢?注意这是个很重要的思想,前缀和。

   先考虑一个问题,如果每次询问的l都是数组一开头的位置1,是不是很容易维护?

   下面举一个例子:维护数组6 2 3 1 4 5。 对前缀1~6建树 (图难看。。。不打紧的咳咳),这个求第k小应该一目了然吧

 

然而,区间第k小其实很容易,对于区间[l,r]而言,只需要在每个节点用前缀r的节点值减去前缀l-1的节点值就好了,其他的和上面是一样的

下面附上一组大佬的图

 

注意和我的数据是不一样的,他的是4 1 1 2 8 9 4 4 3

然后读者按照我刚刚说的用前缀r的树的每一个节点减去对应的前缀l-1的树的节点得到一颗新的树就好

 

好的,现在我们回到原来的问题,主席树。其实上面就是主席树,但是若是我们对于每一个前缀都暴力建一次树的话,在时间和空间上都不能接受。

这时候我们注意到相邻的两棵树其实是很相似的,我们可以让这一棵树和上一棵树共用一些节点,从而达到减低空间和时间复杂度的效果。前缀每次向右一位,其实就是多插入了一个数值,新的

树和上一棵树的唯一区别其实就是一条链上变了而已。

事实证明,这种方法十分的有效。

就像这样,在原来的基础上加上几个点

下面我附上我的代码供大家研究,其实我认为代码更加好懂

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

const int maxn=2e5+15;
int n,m,cnt;
int a[maxn],b[maxn],tree[maxn<<5],L[maxn<<5],R[maxn<<5],sum[maxn<<5];
int build(int l,int r)
{
	int rt=++cnt;
	sum[rt]=0;
	if (l<r)
	{
		int mid=(l+r)>>1;
		L[rt]=build(l,mid);
		R[rt]=build(mid+1,r);
	}
	return rt;
}
int update(int pre,int l,int r,int x)
{
	int rt=++cnt;
	L[rt]=L[pre];R[rt]=R[pre];sum[rt]=sum[pre]+1;//多插入了点于是加个1 
	if (l<r)
	{
		int mid=(l+r)>>1;
		if (x<=mid) L[rt]=update(L[pre],l,mid,x);//看看插到哪一边,另一边其实是一样的 
		else R[rt]=update(R[pre],mid+1,r,x);
	}
	return rt;
}
int query(int u,int v,int l,int r,int k)
{
	if (l>=r) return l;
	int x=sum[L[v]]-sum[L[u]];//减一下就好 
	int mid=(l+r)>>1;
	if (x>=k) return query(L[u],L[v],l,mid,k);
	else return query(R[u],R[v],mid+1,r,k-x);
}
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		b[i]=a[i];
	}
	sort(b+1,b+1+n);//离散化 
	int q=unique(b+1,b+1+n)-b-1;
	tree[0]=build(1,q);
	for (int i=1;i<=n;i++)
	{
		int t=lower_bound(b+1,b+1+q,a[i])-b; 
		tree[i]=update(tree[i-1],1,q,t);//通过上一个树建树 
	}
	while (m--)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		int t=query(tree[x-1],tree[y],1,q,z);//通过x-1树和y树相减 
		printf("%d\n",b[t]);
	}
	return 0;
}

  上文部分图来自大佬 Lpy_Now,感谢大佬

转载于:https://www.cnblogs.com/xxzh/p/9158819.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值