这篇文章中,我以RMQ问题为例,来介绍随机查询类问题。RMQ是Random Maximum/ Minimum Query的简称,顾名思义,是求解区间最值的问题。假设有一个无序的数组arr
(大小为n
),给定两个下标l
和r
,让我们求出arr[l]
到arr[r]
之间元素的最大值或最小值(我们称之为一次查询,即query),我们当然可以直接遍历这些元素,然后将结果输出。但是如果query的次数不是仅仅只有一次,而是成百上千次,难道我们每次输出对应结果的时候都需要遍历一遍相应的区间吗?
答案显然是否定的,这种方法处理每条query的时间复杂度是O(n)
,会出现大量的重复计算。而本篇文章要讲的RMQ-ST算法,在经历过一个O(nlogn)
的预处理步骤之后,处理每次query的时间复杂度是O(1)
。在query数量极大的情况下,RMQ-ST算法的效率肯定是要远远好于最原始的方法的。
一、输入和输出
要解释一个算法,首先明确其输入与输出。RMQ-ST算法的输入分为两部分,首先是一个长度为n
的数组,然后是一系列query,每条query包含两个下标l
和r
。而该算法的输出,则是针对输入的每条query,依次输出arr[l]
到arr[r]
之间元素的最大值或最小值(本文以最小值为例)。
二、算法概述
上文提到过,RMQ-ST算法需要进行一个O(nlogn)
的预处理步骤,该步骤建立了一个表格table
(ST是Sparse Table的简称,该算法被命名为RMQ-ST算法正是由此而来),其中:
table[i][0]
等于arr[i]
;
table[i][1]
等于arr[i]
到arr[i+1]
之间元素(共2
个元素)的最小值;
table[i][2]
等于arr[i]
到arr[i+3]
之间元素(共4
个元素)的最小值;
table[i][3]
等于arr[i]
到arr[i+7]
之间元素(共8
个元素)的最小值;
table[i][k]
等于arr[i]
到arr[i+2^k-1]
之间元素(共2^k
个元素)的最小值。
假设我们已经建立好了这样的一张表,当我们接收到一条query,其下标为[i,j]
时,我们应该怎么办?
首先,我们可以求出区间长度len=j-i+1
,并且可以求出一个最大的k
满足2^k <= len
。
然后,我们可以将原区间分割为两段,[i,i+2^k-1]
和[j-2^k+1,j]
,它们长度均为2^k
。我们知道,这两段区间的并集肯定完整地覆盖了区间[i,j]
(因为我们找到的是一个最大的满足条件的k
)。我们只需要求出这两段区间中元素的最小值min1
和min2
,然后我们就可以知道,区间[i,j]
中元素的最小值,就是min1
和min2
中较小的那一个。(当然这两个区间之间可能有重叠,不过对于求最值的问题来讲,重叠并不影响最终结果。)
我们惊奇地发现,[i,i+2^k-1]
和[j-2^k+1,j]
这两段区间的最小值都已经被我们记录在表中了,它们分别对应table[i][k]
和table[j-2^k+1][k]
。所以我们只需要对每条query,从表中获取这两个元素,然后选取这两个元素中较小的那一个,就是这条query对应的结果。这一步的确是O(1)
的复杂度。
三、预处理的步骤
说了这么半天,table
到底是怎么生成的呢?其实,table
的生成利用了动态规划的思想。当我们想要求出table[i][k]
的值时,我们的想法是将长度为2^k
的当前区间分为两半,每一半长度为2^(k-1)
,然后分别获得两半的最小值,并取出两个最小值中较小的那一个。而这两半的最小值,则是我们已经计算出来的table[i][k-1]
以及table[i+2^(k-1)][k-1]
。我们可以写出该动态规划的状态转移方程:
F(i, 0) = arr[i]
;
F(i, k) = min(F(i, k-1), F(i+2^(k-1), k-1))
对于k>0
。
这个预处理的步骤其实就是生成一个nlogn
大小的表格的步骤,所以时间复杂度为O(nlogn)
,空间复杂度也为O(nlogn)
。
四、完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
五、展望
RMQ-ST算法高效地解决了RMQ问题,但是,该算法也有一些局限性:
1. 如果输入数据可能会多次发生变化,该算法在每次变化后,都需要重新生成一次Sparse Table,时间复杂度为O(nlogn)
,比较耗时;
2. 如果询问的内容不只局限于最大/最小值,而是涉及到其他内容(比如一段区间内元素的和),该算法便无法解决;
这个时候,就要轮到线段树或者树状数组出马了。我将会在之后的文章中介绍这两种算法。