区间第K大值与RMQ问题
这次我们讨论一下有关区间中的值的问题。如果你只想看RMQ,请跳过下面这几段,在第一段代码的后面有详细的讲解。
在竞赛中,我们经常遇到最值问题。但是出题者往往给我们出一些这样的题目,让我们找到第K优解,而不是最优,比如K小生成树、K优背包等等。这篇文章主要介绍另一个“K问题“,区间第K大值。
区间第K大值的题意很明确,对于一个区间,找到其中第K大的一个数输出。这个问题可以用O(n2)的算法枚举,但是当区间很大的时候这种方法就会很费时。我们还可以将区间内的序列排序,直接输出a[k+l-1](l是区间左端点)即可。
我们知道,快排的原理是找到一个标准点,然后进行交换、分组,直到它的左边(以递增为例)都比它小,右边都比它大为止。但是结合这道题来说,每进行一遍分 组,第K大值就会确定的位于其中一组,或者就是那个标准点。然后我们只用将有第K大值的那个组再进行分组、查找(不是标准点),或者直接输出标准点(正好是标准点)。这样,我们就可以以少于1/2的操作找到我们想要的数。
对于多次询问,我们要保留下原始序列,以免之后再寻找时出现错误。
给出一道比较水的题,大家可以试一下~
【题目描述】(rqnoj350)
给出一个长度为N的序列A1,A2,A3,...,AN,其中每项都是
小于10^5的自然数。
现在有M个询问,每个询问都是Ai...Aj中第k小的数等
于多少。
数据范围:
在60%的数据中,1≤N≤1000,1≤M≤1000
在100%的数据中,1≤N≤10000,1≤M≤2000
【输入格式】
第一行两个正整数N,M。
第二行N个数,表示序列A1,A2,...,AN。
紧着的M行,每行三个正整数i,j,k(k≤j-i+1),表示
询问Ai...Aj中第k小的数等于多少。
【输出格式】
共输出M行,第i行输出第i个询问的答案。
【样例输入】
4 3
4 1 2 3
1 3 1
2 4 3
1 4 4
【样例输出】
1
3
4
分析就不用了,直接贴代码吧~
参考代码:
1 program knum;
2 var
3 a,t:array[1..3000000]of longint;
4 n,k,i,m,l,r:longint;
5 function sort(l,r,k:longint):longint; //改编的快排
6 var
7 i,j,x,y:longint;
8 begin
9 if l=r then exit(a[l]); //两指针可能重合。这条语句可以减少时间消耗
10 i:=l;
11 j:=r;
12 x:=a[(i+j) shr 1];
13 repeat
14 while a[i]<x do inc(i);
15 while a[j]>x do dec(j);
16 if i<=j then
17 begin
18 y:=a[i];
19 a[i]:=a[j];
20 a[j]:=y;
21 inc(i);
22 dec(j);
23 end;
24 until i>j;
25 if(k>=i)then exit(sort(i,r,k)); //①
26 if(k<=j)then exit(sort(l,j,k));
27 exit(x); //如果是标准点就直接输出
28 end;
29 begin
30 readln(n,m);
31 for i:=1 to n do
32 read(t[i]);
33 for i:=1 to m do
34 begin
35 a:=t; //保留原序列,对“镜子“序列进行操作
36 readln(l,r,k);
37 writeln(sort(l,r,l+k-1)); //②
38 end
39 end.
40 P.S.① a)因为k∈[l,r],所以不用判断i是否小于r;
41 b)由于i>j,这条语句和以下两条语句没有交集。
42 ②一定是l+k-1,因为是区间的第K小值,所以寻找k肯定不对。
当然,对于区间最值问题(RMQ),这种方法也能解决。为什么还要有求区间最值(RMQ)的伟大的ST算法呢?
有句颇有哲理的话说得好,存在即合理。师傅告诉我,上面改装快排的时间效率是O(nm),所以当m很大时,这种方法就无法快速出解了。这时,强大的ST(Sparse Table)算法应运而生(为了剧情需要^_^)!ST算法可以在O(nlogn)的时间中构造一个强大的数组,然后只用O(1)的时间就能针对每次询问找到解。
这个强大的数组的名字叫做f[i,j](好俗…),表示从区间的第i位开始,长度为2i的区间的最大值(姑且以求区间最大值为例)。这句话听起来很玄乎,可是怎么构造这个数组呢?
我们先把f[i,0]赋成读入的第i个数(也就是以i为起点,长度为20=1的区间内的最值)。因为j表示长度为2j,所以我们可以把整个f[i,j]表示的区间划分[i,j-1]和[i+2(j-1),j-1]两个区间,然后调用f中存储的两个区间中的最大值,取其中较大的那个就行!
有的人可能疑惑,f[i,j]划分的两个空间的最值是什么时候求出来的呢?别忘了我们的初始化。有了j=0时的最值,j=1时的最值就很好求了。显然,j的最大值是trunc(log2n),这样,我们只要让j从1循环到trunc(log2n),把目的区间由小逐渐扩大,之后就能随意地调用原来的解啦!整个过程循环完以后,任何一个长度为2j的区间的最值就全部求出来了。
长度为2j区间的最值是有了,但是如果询问的区间的长度2j和2j+1之间呢?举一个例子。假设原序列是4 2 8 6 1 7 3,求3到7这个区间的最大值。
寻找的时候就只用寻找图中的两个区间的最大值即可,这样整个区间就都能够被覆盖,即使交集也不会影响结果。
另外一道比较水的题,大家也可以试试~
【题目描述】(tyvj p1038)
老管家是一个聪明能干的人。他为财主工作了整整10年,财主为了让自已账目更加清楚。要求管家每天记k次账,由于管家聪明能干,因而管家总是让财主十分满意。但是由于一些人的挑拨,财主还是对管家产生了怀疑。于是他决定用一种特别的方法来判断管家的忠诚,他把每次的账目按1,2,3…编号,然后不定时的问管家问题,问题是这样的:在a到b号账中最少的一笔是多少?为了让管家没时间作假他总是一次问多个问题。
【输入格式】
输入中第一行有两个数m,n表示有m(m<=100000)笔账,n表示有n个问题,n<=100000。
第二行为m个数,分别是账目的钱数
后面n行分别是n个问题,每行有2个数字说明开始结束的账目编号。
【输出格式】
输出文件中为每个问题的答案。具体查看样例。
【样例输入】
10 3
1 2 3 4 5 6 7 8 9 10
2 7
3 9
1 10
【样例输出】
2 3 1
参考代码:
1 program ST;
2 var
3 f:array[0..100000,0..19]of longint; //j的最大值是trunc(log2n),计算器算一下
4 i,j,n,m,l,r:longint;
5 function min(x,y:longint):longint;
6 begin
7 if x<y then exit(x)
8 else exit(y);
9 end;
10 begin
11 readln(n,m);
12 for i:=1 to n do //初始化f数组
13 read(f[i,0]);
14 for j:=1 to trunc(ln(n)/ln(2)) do //循环j的长度
15 for i:=1 to n-1<<j+1 do //i的最大值n-1<<j+1
16 f[i,j]:=min(f[i,j-1],f[i+1<<(j-1),j-1]); //动态规划更新f数组,注意两个区间
17 for i:=1 to m do
18 begin
19 readln(l,r);
20 j:=trunc(ln(r-l+1)/ln(2)); //j就是图示中的k
21 write(min(f[l,j],f[r-1<<j+1,j]),' '); //跟图中一样
22 end;
23 end.
终于写完了~累死了…有什么不对的地方和说得不明白的地方欢迎大家提出!我一定尽快改正!
(Saltless原创,转载请注明出处)