线段树之RMQ
RMQ算法,全称为 Range Minimum/Maximum Query,即区间最值(最小值或者最大值)查询。
其实这个问题至少有三种解法:
① 朴素算法:每次查询都遍历一遍,找到最值,时间复杂度:O(n)
② 线段树:维护每个区间的最小值,时间复杂度:建树 O(n),查询 O(log2n)
③ ST算法:实质就是动态规划,需要推出转移方程,时间复杂度: 打表 O(nlog2n),查询 O(1)
一般来说 ST 算法效率是最高的,对于查询次数不是特别多的 RMQ 来说线段树也是可以的。
一、线段树之RMQ
个人认为使用线段树来完成区间最值问题的查询,实现起来还是比较轻松的,一般就是普通的单点修改以及区间查询,都是线段树的基本操作。
这里直接给出一道裸题:hihoCoder #1077 : RMQ问题再临-线段树
题目大意:货架上从左到右有 N 个货品,有各自的重量,现在有 Q 次询问或者修改:
0 a b:表示查询区间 [a, b] 中的最小重量。
1 a b:表示将第 a 个物品重量换为 b。
sample input:
10
3655 5246 8991 5933 7474 7603 6098 6654 2414 884
6
0 4 9
0 2 10
1 4 7009
0 5 6
1 3 7949
1 3 1227
sample output:
2414
884
7474
思路:一道裸题,只要在每个结点中保存当前区间中的最小值,区间查询的时候只需要得到包含在 [a, b] 区间中的每个子区间的最小值即可,详见代码。
参考代码:
// 线段树之RMQ
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 1e6+10;
int n, ans;
struct Node {
int left, right;
// 保存当前区间中的最小重量
int minValue;
} tree[4*maxn];
void pushUp(int k) {
if(tree[k].left == tree[k].right) return;
// 当前结点的最小值去左右孩子较小的那个最小值
tree[k].minValue = min(tree[2*k].minValue, tree[2*k+1].minValue);
}
void build(int k, int l, int r) {
tree[k].left = l;
tree[k].right = r;
if(l == r) {
scanf("%d", &tree[k].minValue);
return;
}
int mid = (l+r)/2;
build(2*k, l, mid);
build(2*k+1, mid+1, r);
pushUp(k);
}
void change_point(int k, int x, int c) {
if(tree[k].left == tree[k].right){
tree[k].minValue = c;
return;
}
int mid = (tree[k].left+tree[k].right)/2;
if(x <= mid) change_point(2*k, x, c);
else change_point(2*k+1, x, c);
pushUp(k);
}
void ask_interval(int k, int l, int r) {
if(tree[k].left >= l && tree[k].right <= r){
// ans为包含在区间 [l, r] 中的各个子区间的最小值
ans = min(ans, tree[k].minValue);
return;
}
int mid = (tree[k].left+tree[k].right)/2;
if(l <= mid) ask_interval(2*k, l, r);
if(mid < r) ask_interval(2*k+1, l, r);
}
int main() {
while(~scanf("%d", &n)) {
build(1, 1, n);
int m;
scanf("%d", &m);
for(int i = 0; i < m; i++) {
int choice;
int l, r;
scanf("%d%d%d", &choice, &l, &r);
if(choice) {
change_point(1, l, r);
} else {
ans = maxn;
ask_interval(1, l, r);
printf("%d\n", ans);
}
}
}
return 0;
}
(二、ST算法之RMQ)
ST算法的本质就是动态规划,我们需要用一个二维的 dp 数组来保存区间最值(下面假设是最小值)。
dp[i][j]:表示 [i, i+2j-1] 这个长度为 2j 的区间中的最小值。
这个区间可以被分解为 [i, i+2j-1-1] 以及 [i+2j-1, i+2j-1] 这两个长度相等的子区间,
因此 dp[i][j] = min(dp[i][j-1], dp[i+2j-1][j-1]);
其中 dp[i][j-1] 表示区间 [i, i+2j-1-1]
dp[i+2j-1][j-1] 表示区间 [i+2j-1, i+2j-1]
显然 dp[i][j] 只要取这两个完全的子区间中的较小值即可。
边界条件:dp[i][0] = arr[i],因为 dp[i][0] 表示区间 [i, i+20-1] = [i, i] 的最小值。
有了这样一个 dp 数组,怎么利用来求区间最小值呢?
这里直接给出结论:RMQ[l, r] = min(dp[l][k], dp[r-2k+1][k]);
其中 k = log2(r - l + 1)
dp[l][k] 表示区间 [l, l+2k-1]
dp[r-2k+1][k] 表示区间 [r-2k+1, r]
为什么?画个图。
看图可以发现只要两个区间有公共部分,即 l+2k-1 >= r-2k+1,那么就可以保证区间 [l, r] 整个都被覆盖,因此两者的最小值就是区间 [l, r] 的最小值。
(什么?你说如果区间超过了边界 l 和 r,导致其他值被考虑,怎么办?求 k 的时候向下取整就可以,只少不多!)
那么现在我们需要保证的是这个不等式: l+2k-1 >= r-2k+1
使用假设法:假设 l+2k-1 >= r-2k+1 成立,
则 r - l + 2 <= 2 * 2k
将 k = log2(r - l +1) 代入:
r - l + 2 <= 2 * (r - l +1)
r - l <= 2 * (r - l)
r- l >= 0
由于 r - l 保证为正整数,故不等式 l+2k-1 >= r-2k+1 得证。
参考代码:
void init() {
// 设置边界
for(int i = 1; i <= N; i++){
dp[i][0] = arr[i];
}
// 打表 O(nlgn)
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]);
}
int RMQ(int l, int r) {
// k 向下取整
int k = log2(r-l+1);
return min(dp[l][k], dp[r-(1<<k)+1][k]);
}
【END】感谢观看!