树状数组—求第k小的数—入门详解

第k大的数有太多的方法来求了,这是一个十分基础的问题,可以由很多种数据结构来完成。

常用的有排序、主席树……。今天我要介绍一种更快更简洁的算法(Duang!)——树状数组。哦?它也可以求第k大?它不是只用于求区间和的算法吗?怎么还可以用来求大小关系?哈哈,一会就让你大开眼界。


思路

建一个权值树状数组。何为权值树状数组?大家有没有听说过权值线段树?权值线段树就是记录同数值的数的个数的线段树。例如有3,5,5,6四个数,那么ask[3,3]=1,ask[5,5]=2,ask[6,6]=1,ask[3,6]=4。权值树状数组同理。

这样利用树状数组就是个创举,在赋予树状数组这样特殊的含义后,我们开始有点感觉了,求第k小的数,不就是求getsum[1,val]≈k的数嘛。(为什么是约等于,在知道原理后我们再细细分析,前面可以初略理解成等于)

我们采用逐步逼近的方法,利用倍增的思想,我们从2^n开始,试着从1加上一个2^n,如果getsum[1,val]<k,就加上一个2^n。这说明值2^n比值1更加地靠近了第k小的值。

假设现在到了值now的位置,排名sum为getsum[1,now],继续尝试2^i(i从n(据题目取)到0)全面地扫一遍。最后,当sum≈k时,第k小的值≈now。


这里请大家好好体会getsum[a,b]的一些深层意义

1、getsum[1,b]表示的是 小于等于 b的值的数的个数。

2、getsum[1,b]可以表示值b的排名,这个排名是以最后的排名为准的排名。举个例子:1,3,3,4,4,7,根据getsum算出来的1,(2,)3,4,(5,6,)7排名分别为1,(1,),3,(5,)6。因为有同值的数,会使得排名呈不连续状,所以就会出现有 排名后推、空排名 的现象。


看到这里,应该对树状数组求第k大已经有了个框架吧。下面通过抠几个细节,来更加充分地理解它。


细节1:约等号的秘密

这样就可以得到接近第k小的数了。再仔细地考虑一下,如果我们设定的getsum[1,val]目标是等于k,意思就是从值1到val共有k个数。如果值为val的数有5个,值小于val的数有7个,就是getsum[1,val-1]=7,getsum[val,val]=5,我们要求第(k=) 8 小,答案显然为val。如果按设定做,那就是getsum[1,val]=12 >8(k),应该是不会跳到val上的,它会停在(now=) val-1 上,故答案为now+1。若在以上数据求第12小,答案显然为val。getsum[1,val]=12 =12(k),它会恰好停在(now=) val 上,故答案为now。

这就出问题了,当恰好求的是有多个数同值中的最后一名,或特殊些的,是无同值的名次时,答案是now。当求的是多个数同值中的非最后一名时,答案是now+1。这些都是有同值时才会出现的问题。具体一点,我们考虑的就是等于号的问题。我们很希望能恰好直接找到一个排名等于k的值。但是,不一定会有这个排名(根据意义2可以理解),它可能要往后推一点。这就是等号造成的麻烦。

于是我们去掉等号,设定目标为getsum[1,val]<k,这样出来的答案一定比正确答案小1。因为它想无限逼近getsum[1,val],但不等于它。




细节2:getsum的逆序思想

代码中是这样写的   sum+s[now+bin[i]]<k      { sum+=s[now+bin[i]]; now+=bin[i]; }

含义解释:sum记录名次,now记录数值。sum+s[now+bin[i]]是新的排名。

为什么sum这个排名会加上s呢?s是一个十分没有规律的东西,一般不会单独拿来用,这里是什么意思呢?怎么sum从大到小,s管理的数据也是从大到小,前面加到sum的s不会已经包含了新加的s,不是重复计算了一些数值吗?

提出这个问题的同学,一定是树状数组学得不扎实的。我学得很扎实

看着这张很经典的图,我们思考,根据上面的分析,s[now+bin[i]]一定是在sum所计算的之外的。于是我们猜测,now是不是管不到now+bin[i],换句好说,now+bin[i]是不是在now的管理范围之外?到这里,我们就要好好理解理解bin[i]在起了什么所用了。

由于bin是从大到小的枚举,因此now不可能成倍的增加。既然增加幅度有限,意思就是可能无法跳到(old=) +bin[i]前的now(就是上一个now) 的头上,换句话说,它跳到的s可能是不包含old的。再换句话说,now只能越跳越低、越跳越远。

下面我们来简单证明它。众所周知,树状数组中每个点管理的范围和区间与它的二进制有关。bin是越来越小的,也就是说1000000 (2),可能会变成1010000 (2),就是把最后的那些0改成1。最后的那些0越多,这个点管理的范围也就越大。而现在在不停地补0,就是想让这个点的管理范围减小;因为是改0为1,注定会使这个点的编号增大。所以它会越跳越远、越跳越低。所以它一定一定不会重复以前已经累加过的数据


now的问题解决了,sum是求和用的。根据now的神奇走位,sum的原理也就引刃而解了。这里再强调一下,由于now是由近而远、由高而低,而且不包含old,所以只要把now走过的s累加,即可求出getsum[1,now]的值。


平时,不用getsum的树状数组就是一个占码量的数组,这回,我们开辟了船新模式,就是不需要getsum。两者都可以求和,那两者的区别究竟在哪里呢?怎么这次要突然来个创新呢?

我们都知道getsum中用的是lowbit,它能够让x跳到恰好管理不到x的地方,从远往近,从低往高,从而求和。我们这里的sum用的是bin,从0每次尝试着往远跳,从近往远,从高往低,从而求和。这可以说是getsum的逆方向的计算!也恰好适应了倍增逼近的思想。


例题:(来源:poj2985)

【题意】
有N只猫,开始每只猫都是一个小组。下面要执行M个操作:
操作0 i j 是把i猫和j猫所属的小组合并;

操作1 k   是问你当前第k大的小组大小是多少(k<=当前的最大组数)。


【正解】

并查集+树状数组

看到组的合并,很自然地想到并查集,于是用并查集维护集合,顺便记录集合的大小。

求第k大的小组的大小才是重头戏。

我们发现上面介绍的树状数组求第k大的方法安全适用!

但是上面介绍的是求第k小,实际上树状数组最好是用来求第k小的。聪明的你很容易想到,第k大不就是第n-k+1小吗?然后妥妥地用树状数组轻松A过。

下面给出代码,顺便作树状数组求第k小的数的模版。


【代码】

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=200010;
int bin[30];


int n,m,tot;
int fa[maxn],num[maxn];
int s[maxn];//权值树状数组 


//************树状数组 
int lowbit(int x)
{
	return x&-x;
}


void add(int x,int c)//意思是值为x的数,增加了c个 
{
	for(;x<=n;x+=lowbit(x))
	{
		s[x]+=c;
	}
}


//************并查集 
int find_fa(int x)
{
	if(fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}


void link(int x,int y)
{
	int fx=find_fa(x),fy=find_fa(y);
	if(fx!=fy)
	{
		tot--;//tot用于求目前还剩几个组 
		fa[fy]=fx;
		add(num[fx],-1);add(num[fy],-1);
		num[fx]+=num[fy];
		add(num[fx],1);
	}
}


//************主要函数 
int query(int k)//求第k小的数 
{
	int sum=0,now=0;//sum记录名次,now记录数值
	for(int i=20;i>=0;i--)
	{                          //sum+s[now+bin[i]]是新的排名
		if(now+bin[i]<=n && sum+s[now+bin[i]]<k)//注意是小于不带等 
		{
			now+=bin[i];
			sum+=s[now];
		}
	}
	return now+1;//记得+1才能到准确值 
}


int main()
{
	bin[0]=1;for(int i=1;i<=20;i++) bin[i]=bin[i-1]*2;
	
	scanf("%d%d",&n,&m);tot=n;
	for(int i=1;i<=n;i++)
	{
		fa[i]=i;
		num[i]=1;
	}
	
	add(1,n);//初始集合大小都是1,有n个1 
	
	while(m--)
	{
		int opt,x,y,k;
		scanf("%d",&opt);
		if(opt==0)
		{
			scanf("%d%d",&x,&y);
			link(x,y);
		}
		else
		{
			scanf("%d",&k);
			int ans=query(tot-k+1);//求第k大就是求第tot-k+1小 
			printf("%d\n",ans);
		}
	}
	return 0;
}
推荐 :《 树状数组—求第k小的数—离散化
  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值