浅谈——RMQ问题

· · 算法·理论

## step1.倍增法(ST 表) 最朴素的方法。时间复杂度为 $O(n \log n+q)$。 预处理时间复杂度 $O(n \log n)$,每次查询 $O(1)$。 以求**最大值**为例。 ### 定义 定义 $f_{i,j}$ 表示区间 $[i,i+2^j-1]$ 的区间最大值。 ### 初始化 $f_{i,0}$ 表示区间 $[i,i]$ 的结果,即 $a_i$。故有 $f_{i,0}=a_i$。 ### 转移 由于区间最大值支持合并,则显然区间 $[l,r]$ 可以由 $[l,mid]$ 和 $[mid+1,r]$ 合并而来。$mid=\frac{l+r}{2}$。回归定义,可以得出转移方程: $$ f_{i,j}=\max\{f_{i,j-1},f_{i+2^{j-1},j-1}\} $$ 预处理复杂度为 $O(n \log n)$。 ```cpp for(int j=1;j<=log2(n);j++) for(int i=1;i+(1<<j)-1<=n;i++) f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1]); ``` ### 询问 目前已经求出了 $f$ 数组。 询问区间 $[l,r]$ 的最大值。考虑用 $f$ 去覆盖应有区间。 显然我们可以令 $k=\log_2\{r-l+1\}$。 那么答案就是 $\max\{f_{i,k},f_{r-2^k+1,k}\}$。 思考一下,其实就是覆盖左边和右边。中间有重复没关系。全部覆盖就行。 ```cpp int k=log2(r-l+1); printf("%d\n",max(f[l][k],f[r-(1<<k)+1][k])); ``` 每次询问复杂度为 $O(1)$。 优点:易懂。 ## step2.离线单调栈 复杂度 $O(q \log q+q \log n)$。 排序复杂度 $O(q \log q)$,每个询问二分 $O(\log n)$。 ### 方法 离线下来所有询问,按右端点 $r$ 排序。然后每次在序列 $a$ 上找到当前询问的右端点 $r$ 处,并把找到的元素压入到单调栈中。这样,对于每个询问时,单调栈中存储的值满足在 $a$ 中的位置 $\le r$。只需找到序列中第一个 $\ge l$ 的值。显然可以二分来找。 优点:空间复杂度 $O(n)$。 时间复杂度 $O(q \log q +q\log n)$。 ```cpp sort(query+1,query+m+1,cmp); for(int i=1,j=1;i<=m;i++){ while(j<=query[i].r){ while(cnt&&a[j]>a[st[cnt]]) cnt--; st[++cnt]=j++; } int pos=lower_bound(st+1,st+cnt+1,query[i].l)-st; ans[query[i].id]=a[st[pos]]; } ``` ## step3.线段树/树状数组 很模板的线段树。维护区间最大值。 建树 $O(n)$,每次查询 $O(\log n)$,总共为 $O(n+q\log n)$。 优点:模板且没有思维含量。 那么树状数组也能做。 ~~似乎没必要。~~ ## step4.四毛子前置算法(分块) 四毛子算法原名 $\tt{Four Russian}$。这里分析前置算法——分块。 ### 方法 考虑将原序列每 $B$ 个分进一个块,则共有 $\frac{n}{B}$ 个块。 每次询问情况可分为以下几种: - 块内:直接遍历找最大值。 - 相邻块中:前面块的一段后缀的贡献和后面块的一段前缀的贡献取并。 - 间隔很多块:考虑第一个块后缀和最后一个块的前缀,中间的整块直接用 ST 表 $O(1)$ 查询。 梳理一下就是: - 对于每一个块预处理前缀最大值和后缀最大值。复杂度 $O(\frac{n}{B}\times B)=O(n)$。 - 对于每个块总体贡献建立 ST 表,那么也就是 $\frac{n}{B}$ 个块之间的 ST 表,时间复杂度为 $O(\frac{n}{B}\log\frac{n}{B})$。 再发掘一下,发现每次询问最坏情况为同块时,需暴力扫一遍,则时间复杂度为 $O(qB)$。其余情况直接可以 $O(1)$ 解决。考虑如何优化? ### 优化 $1$:块内 ST 表 我们是否可以在每个块内建立 ST 表呢? 答案显然,故时间复杂度为 $O(n+\frac{n}{B}\log\frac{n}{B}+\frac{n}{B}\times B\log B+q)=O(n+\frac{n}{B}\log\frac{n}{B}+n \log B+q)$。 考虑取 $B=\log n$,得到时空复杂度都近似为 $O(n \log \log n+q)$。 优点:实现简单。 ### 优化 $2$:$\pm1\text{RMQ}$ 问题 继续优化同块的情况。 >[oi_wiki](https://oi-wiki.org/ds/cartesian-tree/) 对于**笛卡尔树**的解释。 不难发现,通过构建原序列的笛卡尔树,即可把原本的 $\text{RMQ}$ 问题转化成了树上 $\text{LCA}$ 问题,继续转换,就变成了 $\pm1\text{RMQ}$ 问题。可以预处理 $O(n)$,每次查询 $O(1)$,共 $O(n+q)$ 的时间内解决。 $\pm1\text{RMQ}$ 问题定义为相邻两个值的差为 $\pm1$。即 $\forall i \in \{x|1 \le x < n\},|a_i-a_{i+1}|=1$。 引用一段文字(摘自 OI_wiki,进行了格式优化): > 由于 $\tt{Four Russian}$ 算法的瓶颈在于块内 $\text{RMQ}$ 问题,我们重点去讨论块内 $\text{RMQ}$ 问题的优化。\ > 由于相邻两个数字的差值为 $\pm1$,所以在固定左端点数字时 长度不超过 $\log n$ 的右侧序列种类数为 $\displaystyle\sum_{i=1}^{\log n} 2^{i-1}$,而这个式子显然不超过 $n$。\ 这启示我们可以预处理所有不超过 $n$ 种情况的最小值-第一个元素的值。\ 在预处理的时候我们需要去预处理同一块内相邻两个数字之间的差,并且使用二进制将其表示出来。\ 在询问的时候我们找到询问区间对应的二进制表示,查表得出答案。\ 这样子 $\tt{Four Russian}$ 预处理的时间复杂度就被优化到了 $O(n)$。 ### 优化 $3$:基于状压的线性 $\text{RMQ}$ 问题 对于同块的情况,考虑上述中用单调栈求解过程。令查询 $[l,r]$ 最大值。$l,r$ 同块。 把 $1\sim r$ 的元素放入单调栈,区间 $[l,r]$ 的答案即为下标大于 $l$ 的第一个在栈中的元素(因为维护单调性)。 考虑这样想:将 $1 \sim r$ 的元素加入单调栈时,可以用一个长度为块长 $B$ 的 $01$ 串表示 $r$ 所在块的每个元素是否在栈内。显然当 $B \le 64$ 时容易状压成一个整数。 那么查询时用位运算截取 $l \sim r$ 的部分转换成求状压后整数中末尾连续的 $0$ 的数量。容易用 $O(1)$ 做到。 那么要使 $B \le 64$,不妨就取 $B=\log n$,预处理复杂度就是 $O(n)$,每次查询 $O(1)$,总时间复杂度为 $O(n+q)$。 ~~(似乎用**期望**可以玄学优化,如果出题人要卡,暴力就会被放过。这里不写了。)~~ # 总结 |算法名称 |时间复杂度 |空间复杂度 |综合分析 | |:---------:|:---------:|:---------:|:---------:| |step1.ST 表|$O(n\log n+q)$|$O(n\log n)$|朴素简单,适合新手入门。| |step2.离线单调栈|$O(q \log q+q \log n)$|$O(n+q)$|空间复杂度较低,时间复杂度朴素。| |step3.线段树/树状数组|$O(n+q\log n)$|$O(n)$|算法简单朴素,优点不突出。但好想且好写。熟记模板即可。| |step4.分块|$O(n \log \log n+q)$|$O(n \log \log n+q)$|常数较大,复杂度分析复杂,但实现简单| |^|$O(n+q)$|同上|$\pm1\text{RMQ}$ 优化。| |^|$O(n+q)$|同上|基于状压的优化。| ## 习题 [P3865 【模板】ST 表 & RMQ 问题](https://www.luogu.com.cn/problem/P3865) [P3793 由乃救爷爷](https://www.luogu.com.cn/problem/P3793) [P6640 [BJOI2020] 封印](https://www.luogu.com.cn/problem/P6640) [P2048 [NOI2010] 超级钢琴](https://www.luogu.com.cn/problem/P2048) [P3295 [SCOI2016] 萌萌哒](https://www.luogu.com.cn/problem/P3295) [P12736 [POI 2016 R2] 圣诞灯链 Christmas chain](https://www.luogu.com.cn/problem/P12736) 点个赞吧!