RMQ(Range Minimum / Maximum Query)指的是“区间最值问题”
即求多次查询区间最大值和最小的问题。
一般常用的解决方法有:
1.暴力:O(n^2)
2. ST表:预处理O(nlogn), 单次查询O(1),空间O(nlogn),只能静态查询,局限性大。
3. 线段树:预处理O(nlogn), 单词查询O(logn),空间O(n),可以静态和动态查询。
4. LCA和RMQ:笛卡尔数,LCA(least common ancestor)是最近公共祖先
Note:读到(1 << j)的时候就把他当成2^j次会更容易理解题意。
ST表
基于dp和倍增的思想。我们用二维数组dp[i][j]表示从第i位开始,往后连续2^j的区间的最小值。
初始条件:dp[i][0] = a[i].表示从第i位开始,共1位的最小值。也即a[i]本身了。
如何写状态转移方程呢?我们可以将2^j的区间分成两部分,每一部分都是2^(j - 1),即将[i, i + 2^j - 1] 分成[i, i + 2^(j - 1) - 1] 和 [i + 2^(j - 1), i + 2^j - 1]两部分。
则状态转移方程可以写成dp[i][j] = min(dp[i][j - 1], dp[i + (1 << j - 1)][j - 1];
(把1<< j - 1换成2^(j - 1)次更直观理解。因为是分成两半了嘛,所以dp第二项都是j - 1次。第一个数从i开始,第二个数从i + 2^(j - 1)开始),如图所示。
总结:
初始条件:dp[i][0] = a[i]
状态转移方程:dp[i][j] = min(dp[i][j - 1], dp[i + (1 << j - 1)][j - 1].
预处理代码如下:
for (int j = 1; (1 << j) <= n; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
dp[i][j] = min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
}
}
但是ST表如何查询某个区间的最值呢?
我们还是可以将区间分为两部分,设k = (int)log2(j - i + 1)。则每个分区间的长度为2^k。
则ans = min(dp[i][k], dp[j - (1 << k) + 1][k])。
查询过程如下。(C语言没有专门的求log2的函数,有log(x)代表以自然常数e为底求logx,需要用换底公式,设log(a,b)表示以a为底log(b),则log(a, b) = log(c, b) / log(c, a)。我们用c语言的log函数,以自然常数为底,则公式如下)
int query(int l, int r) {
int k = log(r - l + 1.0) / log(2.0);
return max(dp[l][k], dp[r - (1 << k) + 1][k]);
}
加入(l, r)区间的大小刚好是2的指数,则平分的子区间刚好是一半,也即2^(k-1).如果不是2的倍数,则区间会比2^(k-1)大,这会表示他们之间有重叠,但并不影响求最值。看图:
模板题:
洛谷P3865 : https://www.luogu.com.cn/problem/P3865
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
int a[100005];
int dp[100005][30];
void init_ST(int n) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
dp[i][0] = a[i];
}
for (int j = 1; (1 << j) <= n; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
}
}
}
int query(int l, int r) {
int k = log(r - l + 0.1) / log(2.0);
return max(dp[l][k], dp[r - (1 << k) + 1][k]);
}
int main() {
int n, m;
while(~scanf("%d%d", &n, &m)) {
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
init_ST(n);
while (m--) {
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", query(l, r));
}
}
return 0;
}