从第K 元素看数据结构

从第K 元素看数据结构
这篇文章讨论的是序列中第K 大或第K 小元素,由于第K 大元素可以转化为求第N-K+1
小元素(N 为序列的长度),所以,本文专注于讨论第K 小元素。
本文讨论的几个问题:
1. 对给定整数序列,求该序列中第K 小的元素。
2. 对某一整数序列,允许动态更改序列中的数。动态查询序列中第K 小元素。
3. 给定一个整数序列和若干个区间,回答该区间内第K 小元素。
4. 对某一整数序列,允许动态更改序列中的数。动态查询序列中的第K 小元素。
【关键字】
第K 小元素树状数组线段树平衡二叉树归并树划分树单调队列堆
块状表
【问题一】
问题描述:
给出一个乱序整数序列a[1…n] ,求该序列中的第K 小元素。(1<=K<=N)。
算法分析:
用基于快速排序的分治算法,期望复杂度为O(N)。
代码:
int qs(int *a , int l , int r , int k){
if(l == r) return a[l] ;
int i = l , j = r , x = a[(l+r)>>1] , temp ;
do{
while(a[i] < x) ++ i ;
while(a[j] > x) -- j ;
if(i <= j){
temp = a[i] ; a[i] = a[j] , a[j] = temp ;
i++ ; j-- ;
}
}while(i<=j) ;
if(k <= j) return qs(a , l , j , k);
if(k >= i) return qs(a , i , r , k);
return x ;
}
练习
RQNOJ 350 这题数据量比较小1≤N≤10000,1≤M≤2000 。所以计算量不会超过10^7。当
然用到后面的归并树或划分树,能将复杂度降低。
【问题二】
问题描述:
给出一个乱序整数序列a[1...n] ,有3 种操作:
操作一:ADD NUM 往序列添加一个数NUM。
操作二:DEL NUM 从序列中删除一个数NUM(若有多个,只删除一个)。
操作三:QUERY K 询问当前序列中第K 小的数。
输出每次询问的数。假设操作的次数为M。
算法分析:
这题实际上就是一边动态增删点,一边查询第K 小数。这类题有两种思维方法:一是二
分答案,对当前测试值mid,查询mid 在当前序列中的排名rank , 然后根据rank 决定向
左边还是右边继续二分。另一种是直接求第K 小元素。
这个题可以用各种类型的数据结构解决,其时间复杂度和编程复杂度稍有区别:

线段树:

运用第一种思维,当添加(删除)一个数x 时,相当于往线段树上添加(删除)

一条(x , maxlen)(注意是闭区间)长度的线段。这样询问时,覆盖[mid , mid]区间的线段数
就是比mid 小的数,加上1 就是rank。二分次数为log(maxlen) ,查一次mid 的rank , 复
杂度为O(logN) 。所以总复杂度上界为O(M*logN*logN) 。为方便比较,这里认为log(maxlen)
等于logN。
树状数组:
第一种思维:这个相对简单,因为树状数组求比mid 小的数就一个getsum(mid-1)就搞定。
复杂度同线段树一样。只是常数很小,代码量也很小。
第二种思维:我只能说很巧妙。回顾树状数组求和时的操作:
代码
对二进制数10100010 是依次累加arr[10100010] , arr[10100000] , arr[10000000] 。从而
得到小于x 的数的个数。当反过来看的时候,就有了这种方法:从高位到低位依次确定答案
的当前为是0 还是1,首先假设是1,判断累计结果是否会超过K,超过K 则假设不成立,
应为0,否则继续确定下一位。看程序就明白了。
代码:
int getsum(int x ){
int res = 0 ;
for( ; x>0 ; x-=lowbit(x) ) res += arr[x] ;
return res ;
}
复杂度:自然就比第一种少了一个阶。O(M*logN)。
平衡二叉树:
各种平衡二叉树都可以解决这个问题:Size Balance Tree ,Spaly ,Treap ,红黑树等等。
不得不说,Size Balance Tree 解这个问题是比较方便的。因为SBT 本身就有一个Select 操作,
直接调用一下,就出来了。
代码
复杂度:用的是第二种思维。O(M*logN)。
总结:
其实,综合起来,各种数据结构,各种算法,各种纠结。平衡树太复杂,线段树又高了
点(一般情况不会有问题的)。最好的方法还是树状数组的二进制算法,时间复杂度和编程
复杂度达到双赢。
但是,总结起来,发现线段树或者树状数组所消耗的空间跟数据的范围有关,当序列元
素是浮点数或者范围很大时,就有点力不从心了(当然,离线的情况可以离散化,在线的某
些情况可以离散化),而用平衡二叉树就不存在这样的问题。原来平衡二叉树才是王道。
练习:
最近发现,基于这种思想的题目还真是不少啊~ ~
[NOI2004]郁闷的出纳员工资反过来看,职员工资不变,而是工资下界在变而已。当降
低工资下界时,为了知道哪些职员走人了,我还用了个二叉堆。。。。。
POJ 2761 Feed the dogs 首先该题用接下来问题3 的一个特例。但其特殊性在于任意两
个区间不包含,导致把区间按左端点(不会存在相同左端点滴,否则必包含)排序之后,依
次扫描每个区间,当前区间和前一个区间相交的部分不动,前一区间有而当前区间没有的部
分删除,前一区间没有而当前区间有的部分添加。这能保证每个元素正好添删各一次。
POJ 2823 Sliding Window 太特殊了,最大值就是第K 小, 最小值就是第1 小(这
是一个很重要的启示:树状数组也可以动态求最值)。单调队列可以弄成线性算法。
HUNNU 10571 Counting Girls 第四届华中南邀请赛但数据与题目稍有不符但可以确
定每个数大于等于0 且不超过200000 。关键是求第X th 到第Y th 个MM 的rating 和要
注意下。
KiKi's K-Number 这题就没什么好说的了。求[a+1,MaxnInt]的第K 小的数,先求出[0,a]
有多少个数,设为cnt,只要求第K+cnt 小的数就可以了。哦,题目说是求第K 大的,实际
是求第K 小的,这有点意思~
int getkth(int k){
int ans = 0 , cnt = 0 , i ;
for(i = 20 ; i>=0 ; --i){
ans += 1<<i ;
if(ans>=maxn||cnt+c[ans]>=k) ans-=1<<i ;
else cnt +=c[ans] ;
}
return ans+1 ;
}
【问题三】
问题描述
给定一个序列a[1...n] , 有m 个询问, 每次询问a[i...j]之间第K 小的数。
(引用一句英文:You can assume that n<100001 and m<50001)
算法分析
块状表
如果这道题能够想到用块状表的话,思维复杂度和编程复杂度都不高~,考虑这样操作:
首先预处理,将序列划分成sqrt(N)个小段,每段长[sqrt(N)] ,划分时,将每小段排好序。
然后就是查询了,对区间[i,j]的查询,同样采用二分求比测试值mid 小的个数。i 和j 所在的
零散的两小段直接枚举求,中间完整的小段则二分查找求。这样一次查询时间复杂度为
O(logMaxInt * N *log N )
于是总复杂度为:
O(M *logMaxInt * N *log N )
当然,这里计算量是比较大的,实际写了个程序也是超时的。但当M 比较小时,也未尝不
是一种好的选择(或者当开阔思路吧,但对问题四却正好打个擦边球)。
划分树
划分树应该是解决这道题复杂度最低的方法,复杂度为
O(N *logN +M *logN )
思想其实很简答,用线段树把整个序列分成若干区间。建树时,对区间[l,r]划分:选择该
区间的中位数value(注意:可以先用快速排序对原来序列排个序,于是可以速度得到中位
数),将小于等于value 的不超过mid-l+1 个数划分到[l,mid]区间,其余划分到[mid+1,r]区间,
用一个数组把每层划分后的序列保存起来。
然后,查找的时候,Find(x , l , r , k)表示超找x 节点内区间[l,r]第K 小的数。将该节点区
间分成三个区间[seg_left , l-1] , [l , r ] , [r+1 , seg_right]来讨论问题,他们在划分过程中分
到[l,mid] 区间的个数依次为ls , ms , rs 。若ms<=K 自然查左边区间, Find(2*x , l+ls ,
l+ls+ms-1,K)。否则自然查右边,计算下标很烦啊。有代码在~
代码:
归并树
归并树思想就跟简单了,说白了就是线段树每个区间[l,r]内的数都排好序然后保存起来,
从两个儿子到父亲节点,其实就是两个有序序列归并成一个有序序列,所以就称归并树了。
用前面说的二分答案,对测试值mid 求rank。查找的时候,将查找区间划分成线段树中
若干子区间之并,很明显各个子区间小于mid 的个数加起来,就是该区间小于mid 的个数。
而每个子区间又是有序的,所有二分可以很快找到小区间小于mid 的个数。
总结起来,有三次二分:
1.二分答案;
2.查找区间[a,b]划分成不超过log(b - a)个小区间;
3.对每个子区间,二分查找小于mid 的个数;
于是,整个算法复杂度为:
O(N *logN +M *logMaxInt *log2 N )
void build(lld d ,lld l , lld r ){
if(l == r) return ;
lld i , mid = (l+r)>>1 , j=l , k=mid+1 ;
for(i = l ; i <= r ; ++ i){
s[d][i] = s[d][i-1] ;
if(tr[d][i] <= mid){
s[d][i]++ ;
tr[d+1][j++] = tr[d][i];
}else{
tr[d+1][k++] = tr[d][i];
}
}
build(d+1 ,l , mid);
build(d+1 , mid+1 , r);
}
lld getkth(lld d ,lld lp ,lld rp , lld l , lld r , lld k){
if(lp == rp ) return tr[d][lp] ;
lld mid = (lp + rp)>>1 ;
if(k<=s[d][r]-s[d][l-1])
return getkth(d+1 ,lp , mid , lp+s[d][l-1]-s[d][lp-1] , lp+s[d][r]-s[d][lp-1]-1 , k );
else
return getkth(d+1 ,mid+1 , rp , mid+1+(l-lp)-(s[d][l-1]-s[d][lp-1]) ,
mid+(r-lp+1)-(s[d][r]-s[d][lp-1]) , k-(s[d][r]-s[d][l-1]) );
}
总结:
对本问题,提供了三种算法,其中基于快排的划分算法是最快的;块状表思维和编程都比较
简单,但是复杂度比较高;归并树算法思想很好,二分答案,求rank , 后面问题四的算法
就是根据这种思维设计出来的。
练习
POJ 2761 Feed the dogs
POJ 2104 K-th Number
NOI 2010 超级刚琴这题还真有点难度。
思路:划分树+堆+单调队列
对每个区间,起点为i+1,终点在区间[i+L,i+R]。计s[i]=a[0]+a[1]+...+a[i] (规定a[0]=0), 设
opt[i,k]表示第k 大的s[j] (i+L<=j<=i+R),即opt[i,k] = Max_k { s[j] | i+L<=j<=i+R} (Max_k
表示第k 大)。
先进行两个预处理工作:
1.将s[1] , s[2] , s[3] , .. s[n] 建成一颗静态划分树。
2.用单调队列预处理出所有opt[i,1]的值,并将所有opt[i,1]-s[i]用一个堆维护起来。当然
也可以直接通过查询[i+L,i+R]区间第1 大的值来预处理opt[i,1]。
下面开始取值,并更新:依次从堆中取出最大值,设是opt[i,j]-s[i],把它累加至答案,
再查询划分树中[i+L , i+R]区间第j+1 大的值opt[i,j+1] , 将opt[i,j+1]-s[i]后入堆。直到累加次
数为K 停止。
【问题四】
问题描述
给定一个原始序列a[1...n] , 有两种操作:
操作一: QUERY i j k 询问当前序列中,a[i...j]之间第k 小的数是多少
操作二: CHANG I T 将a[i]改为T ;
输出每次询问的结果。N<=50000 , 操作次数M<=10000 。
算法分析
块状表
将a[1...n]分成sqrt(N)段,每段长[sqrt(N)],为方便二分查找每段,将每段排好序,对操
作二,先删去a[i] , 在插入T , 维护该块得有序性,复杂度为O(sqrt(N))。
对操作一,二分答案,设当前测试值为mid , 先统计两端零散块,O(2*sqrt(N)) 。对
中间完整块,每块二分查找,总复杂度为:
O(M *logMaxInt * N *log N )
线段树+平衡二叉树
序列被线段树划分为区间节点,而每个节点又是一颗平衡二叉树,平衡二叉树放的是该
区间段的所有数。
对操作二,依次更新从跟区间到叶子节点区间的平衡树即可(先删除a[i],在插入T),
考虑复杂度,第一层规模为N,第二层规模为N/2,第k 层规模为N/2^k 。进行依次操作
二的复杂度为:
1
2
2 ( 1)
2
(log log log ... log ) (log ) (1 ( 1)) (log )
2 2 2 2 2
k
k k k
O N N N N O N O k k O N
+
+ + + + + = = + =
这里k = [logN ]
对操作一:询问区间被划分为不超过[logN]个线段树节点之并,每次区间查找上界为
logN 。所以,对每个测试值mid , 耗时(logN)^2 , 故查询一次的复杂度为
O(log2 N *logMaxInt )
总复杂度为
O(M *log2 N *logMaxInt)
练习
ZOJ 2112 Dynamic Rankings
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值