线段树之RMQ

线段树之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】感谢观看!

发布了44 篇原创文章 · 获赞 17 · 访问量 9098

猜你喜欢

转载自blog.csdn.net/qq_41765114/article/details/90181352