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

原创 2018年04月17日 19:47:22

第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;
	for(int i=20;i>=0;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;
}





版权声明:本文为博主原创文章,未经博主允许不得转载,除非先点了赞。 https://blog.csdn.net/A_Bright_CH/article/details/79979559

求第K小/大的数(树状数组解法)

求第K小/大数这个题目经常出现,面试,考试以及OJ上都有类似的题目。 首先声明一点,个人觉得既然是第K小(大是一样的),那么重复的元素就不应该算了,当然如果算了就相对简单一些。。 最原始的解法,快...
  • xueerfei008
  • xueerfei008
  • 2013-09-25 23:05:50
  • 4783

树状数组求第k大值

以POJ 2985为例,具体的写在程序里。思路都是基于二分的思想。 下面是(LogN)^2的方法 /* 题意:某人养了很多猫,他会把一些猫合并成一组,并且会询问第k大组有几只猫 算法...
  • z309241990
  • z309241990
  • 2013-07-30 09:51:28
  • 2523

树状数组:第K大值

转载勿喷,只为自己以后好找一点。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。 原文:...
  • u010839163
  • u010839163
  • 2014-08-01 16:37:58
  • 452

求第K小/大的数(树状数组解法)【续】

上一篇文章讲了通过树状数组来求某个数组中的第K大/小的数。今天刚好看了一道实现挺巧妙的方法,也是用树状数组做的,不过这个算法前提是需要排序,即对数组进行序列化,这样就没有了对最大元素的值有要求了。 ...
  • xueerfei008
  • xueerfei008
  • 2013-09-27 20:22:11
  • 1762

【数据结构练习】 求区间第K大数的几种方法

这类求数列上区间第K大数的题目非常非常多。 比如HDOJ 2665,SOJ 3147,POJ 2104,POJ 2761(区间不包含)。 当然求解这个问题的方法也非常多,所以在这里做一下总结。...
  • frog1902
  • frog1902
  • 2013-07-31 06:43:09
  • 4118

poj2985 The k-th Largest Group 【树状数组求第K大】

链接:http://poj.org/problem?id=2985 题意:给你n,m,代表有n个集合,开始每个集合大小为1,接下来m个操作,每个操作先输入c,c==0则将x,y集合合并,c==0,输...
  • u012483216
  • u012483216
  • 2016-03-26 15:08:44
  • 366

求第k小的数 O(n)复杂度

思路:利用快速排序的思想,把数组递归划分成两部分。设划分为x,数组左边是小于等于x,右边大于x。关键在于寻找一个最优的划分,经过 Blum 、 Floyd 、 Pratt 、 Rivest 、 Tar...
  • flyawayl
  • flyawayl
  • 2016-12-09 14:19:35
  • 273

快速排序求第k小的数

快速排序求第k小的数,思想非常简单,就是如果要查找的k比当前下标low小,则只递归左部分,大则递归右部分,相等则递归右部分,当然由于数组下标从0开始,所以应该是k-1,(比如第一大的数数组下标为0),...
  • fengsigaoju
  • fengsigaoju
  • 2016-02-24 10:56:45
  • 1201

求数组的第K小数,O(nlogn) 和 O(N)的算法

在面试中碰到求数组中第K小的数,(或者最小的的K个数)。 最直观的方法是排序之后,选择数组A的元素A[K-1];  以快速排序为例,排序的时间复杂度为O(NlogN), 选择元素的时间为O(...
  • ppslizejun
  • ppslizejun
  • 2016-06-12 14:06:41
  • 1829

求最小的k个数字和求第k小的数字

求最大的和最小的原理是一样的,只不过是求最大的在应用中用的比较多。举个比较常见的例子,大家都会购物吧,购物的时候如果去京东商城,当搜索某件商品的时候,搜索后的页面会呈现很多该类型的商品,但是京东总会给...
  • zzran
  • zzran
  • 2012-12-31 20:58:59
  • 5025
收藏助手
不良信息举报
您举报文章:树状数组—求第k小的数—入门详解
举报原因:
原因补充:

(最多只允许输入30个字)