RMQ
Range Minimum/Maximum Query
- 作用:快速求区间的最大/最小值
- 本质是一个动态规划
思考流程
-
假设我们有序列 w [ 1 ∼ N ] w[1 \sim N] w[1∼N]
-
以最大值为例
-
定义状态 f ( i , j ) f(i,j) f(i,j):代表从索引 i i i开始,长度是 2 j 2^j 2j的区间的最大值
-
也就是说, f ( i , j ) f(i,j) f(i,j)代表的就是区间 [ i , i + 2 j − 1 ] [i, i + 2 ^ j - 1] [i,i+2j−1]区间内的最大值
那么, f ( i , j ) f(i,j) f(i,j)该如何求解呢?
-
首先,我们可以发现当 j = 0 j=0 j=0时, f ( i , 0 ) = w [ i ] f(i,0)=w[i] f(i,0)=w[i],即当前代表的区间就是它自己,因此它自身就是最大值;
-
当 j ≠ 0 j \neq 0 j=0时,我们可以发现,
-
我们可以将区间 [ i , i + 2 j − 1 ] [i, i + 2 ^j - 1] [i,i+2j−1]拆分为 [ i , i + 2 j − 1 − 1 ] , [ i + 2 j − 1 , i + 2 j − 1 ] [i, i + 2 ^{j - 1} - 1], [i +2^{j-1}, i + 2^j - 1] [i,i+2j−1−1],[i+2j−1,i+2j−1]
-
分别对应了 f ( i , j − 1 ) f(i, j-1) f(i,j−1)和 f ( i + 2 j − 1 , j − 1 ) f(i + 2^{j - 1}, j - 1) f(i+2j−1,j−1)
-
按照 f ( i , j ) f(i, j) f(i,j)代表的含义,我们可以发现 f ( i , j ) = m a x { f ( i , j − 1 ) , f ( i + 2 j − 1 , j − 1 ) } f(i,j) = max\{f(i, j - 1), f(i + 2^{j - 1}, j - 1) \} f(i,j)=max{f(i,j−1),f(i+2j−1,j−1)}
至此,我们就可以从 j = 0 j=0 j=0开始向上循环预处理 f ( i , j ) f(i,j) f(i,j)
初始化完成后,对于给定的区间 l , r l, r l,r,该如何求 [ l , r ] [l, r] [l,r]范围内的最大值呢?
我们令区间长度 l e n = r − l + 1 len = r - l + 1 len=r−l+1
- 我们可以找到一个 k k k,满足性质 2 k ≤ l e n , 2 ⋅ 2 k ≥ l e n 2^k \le len, \quad 2 \cdot 2^k \ge len 2k≤len,2⋅2k≥len
- 满足这个性质的 k k k,我们可以通过计算得到 k = ⌊ log 2 l e n ⌋ k = \lfloor \log_2{len}\rfloor k=⌊log2len⌋
这样,我们可以将区间 [ l , r ] [l, r] [l,r]分为 [ l , l + 2 k − 1 ] [l, l + 2^k - 1] [l,l+2k−1]和 [ r − 2 k + 1 , r ] [r - 2^k + 1, r] [r−2k+1,r]
分别对应 f ( l , k ) f(l, k) f(l,k)和 f ( r − 2 k + 1 , k ) f(r - 2^k + 1, k) f(r−2k+1,k)
- 注意,这两个区间不是正好构成 [ l , r ] [l,r] [l,r],它们之间是有重叠部分的
- 但对于最大最小值,重复计算或者多次对比,是不会影响最终结果的
我们用一个实际的例子来看一下
-
打个比方, w = { 1 , 3 , 2 , 7 , 4 , 1 } w = \{1, 3, 2, 7, 4, 1\} w={1,3,2,7,4,1},下标从 1 1 1开始
- l = 1 , r = 6 , k = 2 l = 1, r = 6, k = 2 l=1,r=6,k=2
- 根据上面提到的分法,将区间
[
1
,
6
]
[1,6]
[1,6]转化为
[
1
,
4
]
[1, 4]
[1,4]和
[
3
,
6
]
[3, 6]
[3,6],分别对应
[
l
,
l
+
2
k
−
1
]
[l, l + 2^k - 1]
[l,l+2k−1]和
[
r
−
2
k
+
1
,
r
]
[r - 2^k + 1, r]
[r−2k+1,r]
- [ l , l + 2 k − 1 ] → f ( l , k ) → f ( 1 , 2 ) → [ 1 , 1 + 2 2 − 1 ] → [ 1 , 4 ] [l, l + 2^k - 1] \rightarrow f(l, k) \rightarrow f(1,2) \rightarrow [1, 1 + 2^2 -1] \rightarrow [1, 4] [l,l+2k−1]→f(l,k)→f(1,2)→[1,1+22−1]→[1,4]
- [ r − 2 k + 1 , r ] → f ( r − 2 k + 1 , k ) → f ( 3 , 2 ) → [ 6 − 2 2 + 1 , 6 ] → [ 3 , 6 ] [r - 2^k + 1, r] \rightarrow f(r - 2^k + 1, k) \rightarrow f(3,2) \rightarrow [6 - 2^2 + 1, 6] \rightarrow [3, 6] [r−2k+1,r]→f(r−2k+1,k)→f(3,2)→[6−22+1,6]→[3,6]
-
所以,可得区间内最大值即为: m a x { f ( 1 , 2 ) , f ( 3 , 2 ) } = 7 max \{ f(1, 2), f(3, 2) \} = 7 max{f(1,2),f(3,2)}=7
至此,我们将初始化和最大值查询结合,就是RMQ的完整写法了
时间复杂度:
- 初始化: O ( N ⋅ log N ) O(N \cdot \log N) O(N⋅logN)
- 查询:接近 O ( 1 ) O(1) O(1)
最后还有一道例题
- 之前的题目可能有朋友打不开链接,换了一道
- 但在这题中,用
Java
跑的话会MLE(Memory Limit Exceeded)
,没有想到好的解决办法- 算法运行时间方面是没问题的
- 应该是输入输出流上面占多了空间
- 同样的写法用
C++
可正常通过,因此将对应代码一起贴在这
题目链接:P3865 【模板】ST 表 - 洛谷
Java写法
import java.io.*;
public class Main {
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static int N = 100010, M = 20; // M = logN + C, C任意
static int n, m;
static int[] w = new int[N];
static int[][] f = new int[N][M];
static int Int(String s) {
return Integer.parseInt(s);
}
static void init() {
for (int j = 0; j < M; j ++) {
for (int i = 1; i + (1 << j) - 1 <= n; i ++) {
if (j == 0) {
f[i][j] = w[i];
} else {
f[i][j] = Math.max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
}
}
}
static int query(int l, int r) {
int len = r - l + 1;
int k = (int) (Math.log(len) / Math.log(2));
return Math.max(f[l][k], f[r - (1 << k) + 1][k]);
}
public static void main(String[] args) throws IOException {
String[] arr = in.readLine().split(" ");
n = Int(arr[0]); m = Int(arr[1]);
arr = in.readLine().split(" ");
for (int i = 1; i <= n; i ++) {
w[i] = Int(arr[i - 1]);
}
init();
while (m -- > 0) {
arr = in.readLine().split(" ");
int l = Int(arr[0]), r = Int(arr[1]);
out.write(query(l, r) + "\n");
}
out.flush();
}
}
C++写法
#include <iostream>
#include <cmath>
using namespace std;
const int N = 100010, M = 20;
int n, m;
int w[N];
int f[N][M];
void init()
{
for (int j = 0; j < M; j ++)
for (int i = 1; i + (1 << j) - 1 <= n; i ++)
if (j == 0) f[i][j] = w[i];
else f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
int query(int l, int r)
{
int len = r - l + 1;
int k = log(len) / log(2);
return max(f[l][k], f[r - (1 << k) + 1][k]);
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) scanf("%d", &w[i]);
init();
while(m --)
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", query(l, r));
}
return 0;
}