首发于算法随笔

一个RMQ问题的快速算法,以及区间众数

之前写这个回答的时候从电脑里翻出了这个解决RMQ问题的算法。由于年代久远出处记不太清了,框架是受到MODULE 1e9+7在blog里写的 O(n\log^* n)-O(1) RMQ做法启发得到的(现在那个blog页面已经不存在了,还好我本地离线存了一份。大概就是对[1,2]这两篇文章的介绍)。<del>优化到 O(n) 预处理的位运算做法或许是我自己原创的也说不定</del>。

update. 评论区提示这个算法在02年的文章[6]中介绍related results的部分已经被提到过了。所以果然又是重新发明。那么好奇众数的那部分是新的么...?

(原来那个网页的标题叫这个)

首先回顾一下常见的几种RMQ做法。

  1. 线段树。 O(n) - O(\log n) (这里前一个是预处理复杂度,后一个是单次询问复杂度),或者 O(n\log n) - O(1) (比如在[5]中有提到。现在好像有的人把这个变种叫作猫树)。

2. ST表。 O(n\log n) - O(1)

3. RMQ转LCA,离线tarjan LCA。 O(n\alpha(n)) 。(为了方便,这里假设询问数 mn 同阶)

update. 最近学到了怎么把tarjan LCA做到 O(n) ,可以看这里。但目测不好写。

4. 离线并查集。O(n\alpha(n)) 。本质和上一个类似但不需要显式地和LCA扯上关系。

5. RMQ转LCA再转±1RMQ。O(n) - O(1) 。这是大家熟知的那个理论最好的做法,但是常数比较大。

6. RMQ转LCA,然后用LCA的Schieber Vishkin algorithm(可以在这里看到相关介绍)。O(n) - O(1) 。历史上是先有的这个做法,然后出现了方法5,大家认为方法5更简单

对于常见数据范围(几十万, n\approx m )实际速度最快的应该是方法4,然后是方法1的zkw线段树实现。方法6我没写过,但目测不会比zkw快。我猜BZOJ上跑得最快的那些除了我之外都是方法4。


下面介绍一个实际速度更快的位运算做法,在BZOJ(题号1699)和poj(题号3264)上都是rank1。其实跟方法5有一点像但是不需要归约到LCA问题。

考虑把序列按 B=\Theta(\log n) 大小分块,把每个块缩成一个数之后用ST表预处理询问的两端点恰好为块的端点的情况。因为在 O(\frac{n}{\log n}) 个数上建ST表,所以预处理的复杂度为 O(n) 。如果询问的一个端点落在某个块的中间,且询问区间与至少两个块相交,那么只需要对每块内预处理前/后缀min就可以 O(1) 回答询问。难点在于询问的两端点落在同一块内的情况。方法5的解决方案是对于 ±1RMQ,一共只有 O(2^B)=O(n^{1-\epsilon}) 种本质不同的块,所以可以预处理所有可能询问的答案。但这对于一般的RMQ不适用。

在word-RAM model中,一个常见的假设是字长 w\geq \log n 。现在考虑对每个块从左到右维护递增的单调队列,队列中最多只存放了 O(\log n) 个数(所对应的块内下标),可以pack到一个word里。如果我们要询问一块内 lr 之间的最小值,只需要求第 r 个时刻所对应的单调队列中第一个 \geq l 的下标就行。这可以使用__builtin_ctz O(1) 计算。(我之前的一篇文章介绍了为什么这个操作在理论上是 O(1) 的。另外因为这里 W=\mathop{\mathrm{poly}}(n) ,所以预处理打表也行。)

代码如下:

const int N=100105,D_max=18,L=27,inf=1<<30;
int f[N/L][D_max],M[N/L],a[N+L],stack[L+1],n,q,D;
struct node{
    int state[L+1],*a;
    int Qmin(int x,int y){return a[x+__builtin_ctz(state[y]>>x)];}
    void init(int *_a){
        int top=0;a=_a;
        for (int i=1;i<=L;++i){
            state[i]=state[i-1];
            while (top&&a[i]<=a[stack[top]])state[i]-=1<<stack[top],--top;
            stack[++top]=i;state[i]+=1<<i;
        }
    }
}c[N/L];
void build(){
    int nn=n/L;M[0]=-1;
    for (int i=1;i<=nn;++i){
        f[i][0]=inf;for (int j=1;j<=L;++j)f[i][0]=min(f[i][0],a[(i-1)*L+j]);
    }
    for (int i=1;i<=nn;++i)M[i]=!(i&(i-1))?M[i-1]+1:M[i-1];
    for (int j=1;j<=D;++j)
        for (int i=1;i<=nn-(1<<j)+1;++i)f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]);
    for (int i=1;i<=nn+1;++i)c[i].init(a+(i-1)*L);
}
inline int Qmin_ST(int x,int y){
    int z=M[y-x+1];return min(f[x][z],f[y-(1<<z)+1][z]);
}
inline int Qmin(int x,int y){
    int xx=(x-1)/L+1,yy=(y-1)/L+1,res;
    if (xx+1<=yy-1)res=Qmin_ST(xx+1,yy-1);else res=inf;
    if (xx==yy)res=min(res,c[xx].Qmin(x-(xx-1)*L,y-(yy-1)*L));
    else res=min(res,c[xx].Qmin(x-(xx-1)*L,L)),res=min(res,c[yy].Qmin(1,y-(yy-1)*L));
    return res;
}

(交BZOJ1699的完整代码可以看这里。)


我还曾经思考过能不能把这个做法推广到 O(n) - O(1) 的静态区间最大子段和询问(或者出现次数严格大于半数的区间众数询问),但似乎不太行。其中一个区别是对于RMQ我们可以 O(1) 合并两个相交区间的答案,而对于后两个问题不行;对于完整包含几个块的情况我们可以把ST表换成猫树,但是处理块内询问看起来不太容易(即使块的大小非常小)。后两个问题的更一般化情况是序列 A[1,\dots,n] 中的元素为一个半群中的元素(运算为 \circ ,有结合律),然后询问 A[l]\circ\dots\circ A[r] ,称为semi-group sum problem。对这个一般化的情况[1,2]给了一个 O(n\alpha(n)) 的做法(对于一般 m 的话是每次询问\alpha(m,n) 。p.s. [1]是Yao写的)。对于静态区间最大子段和这个特例,[3]给了一个 O(n) - O(1) 的做法,但看起来和本文不是一个思路。

对于出现次数严格大于半数的众数询问的目前最优做法我一下子还没找到。看起来这个领域的paper希望优化的是预处理的空间而不太关心预处理时间,比如[4]可以做到 O(n\log n) 预处理, O(n) 空间, O(1) 询问,当然它做的是一个更一般的情况 \alpha -majority。[8]推广到了动态众数的情况。


update:

静态区间众数询问

下面介绍在静态情况下对于出现次数严格大于半数的区间众数询问如何做到O(n) - O(1)。这也就是LeetCode 1157. Online Majority Element In Subarray的题解:

Loading...leetcode.com图标

首先考虑不要求检测出答案不存在的情况,即如果询问区间内存在出现次数严格大于半数的众数,则返回那个数,否则可以返回任何值。

就像常见的那个线段树做法一样,我们维护的信息是一个pair(数字,抵消后的出现次数)。比如我们要合并两个信息为 (v_1,s_1)(v_2,s_2) 的区间,若 v_1=v_2 则合并结果为 (v_1,s_1+s_2) ,否则不妨设 s_1\geq s_2 ,合并结果为 (v_1,s_1-s_2) ,表示 v_1的一部分出现次数和 v_2抵消了。容易验证我们可以用之前提到的对于半群中元素求区间和的算法维护这个信息。(这里还是有必要详细说明一下:直接套定义的话我们维护的信息是不满足结合律的,但是在存在出现次数严格大于半数的众数的情况下,按任意顺序结合计算,再将答案经过映射 f:(v,s)\mapsto v 得到的结果是唯一的那个众数,所以相当于满足结合律。)

现在我们还是把序列按 B=\Theta(\log n) 大小分块,对于完整包含几块的询问,可以把ST表换成猫树,复杂度不变。那么只需要处理询问区间完整包含在一个长度为 \Theta(\log n) 的块内的情况。将这个算法递归一层,即按 B'=\Theta(\log \log n) 大小再分块,那么只需要处理询问区间完整包含在一个长度为 \Theta(\log \log n) 的小块内的情况。将块内数字离散化后只有 (\log\log n)^{\Theta(\log\log n)} 种本质不同的块,对每个块有 O((\log\log n)^2) 种可能的询问,所以可以预处理答案然后 O(1) 回答。

p.s. 这里比一般半群的情况复杂度更好的原因是,当询问区间足够小的时候我们可以根据问题的特殊性质直接做到 O(1) 。这个做法对最大子段和不work的原因是虽然当 B' 足够小时本质不同的块数可以不超过 O(n^{1-\epsilon}) ,但是不容易在 O(B') 的时间内识别出每块属于哪种本质不同的情况。

现在考虑如何检验我们得到的候选数 v 是否真的在询问区间[l,r]内出现了半数以上。不妨直接统计 v 在区间内的出现次数。令s_v[i] 表示数 v 在区间[1,i] 中的出现次数,我们只需要知道 s_v[r]-s_v[l-1] 。问题是如何在预处理时快速计算 s_v[\cdot] (不需要也没法对全部下标都算)。

Lemma 1. 如果数 v 在整个数组中一共出现了 t 次,那么把所有存在严格众数且为 v 的区间 [l',r'] 拿出来取并集,并集最多覆盖了 O(t) 个数组中的元素。

Proof. 考虑那个(大家或许见过的)求区间并的贪心算法,归纳假设是已经求出了并集在 [1,i] 中的部分,每次在未被当前并集完整包含的区间中找到左端点 \leq i ,右端点最大的一个区间,和当前的并集取并。如果找不到左端点 \leq i 的区间则取左端点最小(且右端点最大)的区间。这样我们可以用一部分区间的并表示并集,且每个元素最多被覆盖2次。接下来令 x_i\in \{0,1\} 表示数组的第 i 个元素是否为 v ,用线性规划写一系列不等式就能得证。

Lemma 2. 我们可以在 O(t) 的时间内求出这个并集。

Algorithm. 不妨假设所有数 v 的出现位置已经按顺序存在一个数组中。对于每个下标 k ,我们希望计算 k 是否在并集中,这当且仅当存在 i<kj\geq ks_v[j]-s_v[i]\geq (j-i)/2 ,即 2s_v[j]-j\geq 2s_v[i]-i (为了简单,这里我们忽略取整的细节)。那么我们只需要维护一下 2s_v[i]-i 的前后缀min/max就行。可以发现有意义的 k 最多只有 O(t) 个。

现在对于数 v ,我们只需要预处理计算下标在并集中的 s_v[\cdot] ,直接算就行。对于所有数 v 所需复杂度的和是 O(n) 的。询问时若某个需要的s_v[\cdot]未被预先计算则说明一定不存在严格众数。


References:

[1] Yao A C. Space-time tradeoff for answering range queries[C]//Proceedings of the fourteenth annual ACM symposium on Theory of computing. ACM, 1982: 128-136.

[2] Alon N, Schieber B. Optimal preprocessing for answering on-line product queries[M]. Tel-Aviv University. The Moise and Frida Eskenasy Institute of Computer Sciences, 1987.

[3] Chen K Y, Chao K M. On the range maximum-sum segment query problem[C]//International Symposium on Algorithms and Computation. Springer, Berlin, Heidelberg, 2004: 294-305.

[4] Durocher S, He M, Munro J I, et al. Range majority in constant time and linear space[C]//International Colloquium on Automata, Languages, and Programming. Springer, Berlin, Heidelberg, 2011: 244-255.

[5] Yuan H, Atallah M J. Data structures for range minimum queries in multidimensional arrays[C]//Proceedings of the twenty-first annual ACM-SIAM symposium on Discrete Algorithms. Society for Industrial and Applied Mathematics, 2010: 150-160.

[6] Alstrup S, Gavoille C, Kaplan H, et al. Nearest common ancestors: a survey and a new distributed algorithm[C]//Proceedings of the fourteenth annual ACM symposium on Parallel algorithms and architectures. ACM, 2002: 258-264.

[7] Navarro G, Thankachan S V. Optimal encodings for range majority queries[J]. Algorithmica, 2016, 74(3): 1082-1098.

[8] Elmasry A, He M, Munro J I, et al. Dynamic range majority data structures[C]//International Symposium on Algorithms and Computation. Springer, Berlin, Heidelberg, 2011: 150-159.

编辑于 04-06

文章被以下专栏收录