RMQ问题的离线近线性算法
北京市清华附中高逸涵
RMQ问题是这样一种问题:给定一长度为n的数组A,回答询问RMQ(A,I,J),即A[i…j]之间的最小数的下标。在不引起误会的情况下,直接用RMQ(i,j)来表示这个询问。
对于这一问题,存在一个非常好的在线算法Sparse Table,能在O(nlogn)-O(1)的时间复杂度解决问题。该算法记录了所有长度形如2i的所有询问的结果,对于一般的RMQ询问,取k=[log2(j-i+1)],则有:
RMQ(i,j)=min{d[i,k],d[j-2^k+1,k]}
其中d[i,j]表示从i开始长度为2j区间内的RMQ,有递推公式:
d[i,j]=min{d[i,j-1],d[i+2^(j-1),j-1]}
对于离线问题,我们希望做的更好。首先先确定RMQ问题的存储方式:采用链表存储,这样的好处是可以在O(1)的时间内得到以某个下标结尾的所有询问。
算法是基于这样一个简单的性质:如果按照RMQ问题询问的区间结束位置处理,则对于A[i]和A[j],如果有j>i且A[j]<=A[i],则i不可能是后续询问的答案。
因此,考虑建立一个栈,每次向栈顶插入一个数,插入前将栈中所有大于等于该数的数删除,很明显这样操作后栈中元素呈递增序列。这样的话,每次询问时在栈中二分查找起始下标的位置,如果栈中相邻的两个数的下标满足B[i]<start,B[j]>=start,则B[j]即为所求下标。
这样,问题的算法复杂度总计为O(Qlogn),实际上栈中的数的个数一般来说远小于数组的大小,所以实际运行效率还是很快的。
当然,这样的算法与Sparse Table相比并没有任何的优势。所以我们希望进一步改进算法。
考虑问题中每个元素被询问时所给出的答案,我们发现它可以表示为以下的集合形式,为了便于说明,下面给出了算法运行的图表。
步骤1 |
|
步骤2 | {1}{2} |
步骤3 | {1}{2}{0} ->{1}{2 0}->{1 2 0} |
步骤4 | {1 2 0}{3} |
步骤5 | {1 2 0}{3}{2} -> {1 2 0}{3 2} |
步骤6 | {1 2 0}{3 2}{6} |
步骤7 | {1 2 0}{3 2}{6}{0}->{1 2 0}{3 2}{6 0}->{1 2 0}{3 2 6 0}->{1 2 0 3 260} |
{1}
表1(集合的代表元素用加粗字体表示)
可以看到,每一次插入数即为新建一个集合,而删除一个数即将两集合合并,每次询问则为查询一个数所在的集合的代表元素。这正好是并查集所支持的各种操作。而并查集的每次操作的均摊复杂度接近O(1),所以RMQ的离线问题也可以在O(N+Q)的时间内解决,空间复杂度也是O(N+Q)。
对于N和Q相差不大的问题来说,本文所述的离线无论时间和空间都远好于Sparse Table算法。这也是离线算法所能达到的一个非常好的复杂度了。
值得一提的是,LCA问题的Tarjan算法即为本文所述算法在±1-RMQ问题中的特例。