第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]
这里请大家好好体会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。因为有同值的数,会使得排名呈不连续状,所以就会出现有 排名后推、空排名 的现象。细节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]
细节2:getsum的逆序思想
代码中是这样写的 sum+s[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,注定会使这个点的编号增大。所以它会越跳越远、越跳越低。所以它一定一定不会重复以前已经累加过的数据
平时,不用getsum的树状数组就是一个占码量的数组,这回,我们开辟了船新模式,就是不需要getsum。两者都可以求和,那两者的区别究竟在哪里呢?怎么这次要突然来个创新呢?
我们都知道getsum中用的是lowbit,它能够让x跳到恰好管理不到x的地方,从远往近,从低往高,从而求和。我们这里的sum用的是bin,从0每次尝试着往远跳,从近往远,从高往低,从而求和。这可以说是getsum的逆方向的计算!也恰好适应了倍增逼近的思想。
例题:(来源:poj2985)
【题意】操作1 k 是问你当前第k大的小组大小是多少(k<=当前的最大组数)。
【正解】
并查集+树状数组
看到组的合并,很自然地想到并查集,于是用并查集维护集合,顺便记录集合的大小。求第k大的小组的大小才是重头戏。
我们发现上面介绍的树状数组求第k大的方法安全适用!
但是上面介绍的是求第k小,实际上树状数组最好是用来求第k小的。聪明的你很容易想到,第k大不就是第n-k+1小吗?然后妥妥地用树状数组轻松A过。
下面给出代码,顺便作树状数组求第k小的数的模版。【代码】
#include
#include
#include
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小的数—离散化》