RMQ标准算法

RMQ 是英文 Range Maximum/Minimum Query 的缩写,表示区间最大(最小)值。

笛卡尔树是一棵二叉树,包含了一个序列中的每个元素。对于每个元素在原序列中的下标来说,笛卡尔树是一棵二叉搜索树(即,中序遍历结果单调递增);对于每个元素的大小来说,笛卡尔树是类似于堆的(子树中的最值恰好为子树的根)。

笛卡尔树的建树过程是 O ( n ) 的。利用笛卡尔树,在原序列上的 R M Q  问题 等价于笛卡尔树上的 l c a  问题。因为 l c a 恰好为最小值——这是由定义决定的。

好,接下来让我们详细进入每一步。

将RMQ(Range Minimum Query)问题转化为LCA(Lowest Common Ancestor)问题:

首先,我们要知道笛卡尔树是一种特殊的二叉树结构,它满足以下性质:

每个节点对应输入数组中的一个元素,且节点的值恰好是该元素的值。

对于任意一个节点 u,它的左子树中所有节点的值都小于 u 的值,右子树中所有节点的值都大于 u 的值。

这样构造出来的笛卡尔树具有以下重要性质:

RMQ(i, j) 问题等价于找到区间 [i, j] 内值最小的元素的索引。

这等价于在笛卡尔树上找到区间 [i, j] 内两个叶子节点的最近公共祖先 (LCA)。

原因在于:

由于笛卡尔树的性质,区间 [i, j] 内值最小的元素一定对应到该区间内两个叶子节点的 LCA。

因为 LCA 是这两个节点的共同祖先,而在二叉搜索树的性质下,LCA 节点的值一定是区间 [i, j] 内的最小值。

所以通过构造笛卡尔树,我们就成功地将 RMQ 问题转化为了 LCA 问题。后续的步骤就是将 LCA 问题进一步优化成简单的 RMQ 问题。

现在我们的问题是将 LCA 问题转化为简化的 RMQ 问题。这一步的关键在于利用欧拉游览(Euler Tour)的技术。

欧拉游览的基本思路如下:

从根节点开始,采用深度优先搜索的方式遍历整个笛卡尔树。

在遍历过程中,每次访问一个节点,就将其加入到一个线性序列中。

对于每个节点,在第一次访问时将其加入序列,在最后一次访问时将其从序列中删除。

这样做的结果是,我们得到了一个线性序列,它满足以下性质:

对于任意两个节点 u 和 v,它们在序列中第一次出现的位置之间的最小值,就是这两个节点在笛卡尔树上的 LCA。

这个序列上每个点对应的值就是该节点在笛卡尔树上的深度。

这样我们就成功将 LCA 问题转化为了一个简化的 RMQ 问题:

  • 在这个线性序列上找到区间 [i, j] 内的最小值,就等价于找到区间 [i, j] 内两个叶子节点的 LCA。

  • 这个 RMQ 问题相比原始问题更加简单,因为序列上每个点对应的值就是节点在笛卡尔树上的深度。

通过这一步转化,我们将 LCA 问题成功地转化为了一个简单的 RMQ 问题,后续就可以使用分治策略和对数划分来解决了

最后这一步的核心思想是利用动态规划的方法来预处理这个简化的RMQ问题,从而实现O(1)的查询时间复杂度。

具体做法如下:

  1. 将输入序列划分成长度为2^i的块(i从0开始递增),并计算每个块内的最小值。

    对于长度为1的块,直接将序列中的元素作为最小值。对于长度为2^i的块,我们可以利用前一步计算出的长度为2^(i-1)的块内最小值,通过取最小值的方式来计算出当前块的最小值。
  2. 然后利用这些块内最小值,继续递归地计算较大块内的最小值。

    比如说,我们可以计算长度为2^2=4的块内的最小值,是通过取长度为2^1=2的两个相邻块内最小值的最小值得到的。以此类推,直到计算出整个序列的最小值。

这种分治策略的时间复杂度是O(nlogn),因为我们需要对每一个块都进行计算。空间复杂度也是O(nlogn),因为我们需要存储每个块的最小值。

最终,当我们需要查询区间[i, j]内的最小值时,只需要从预处理好的结果中取出对应的块内最小值即可,时间复杂度为O(1)。

这样做的关键在于利用了分治和对数划分的思想,将原本复杂的RMQ问题简化为一个可以高效预处理的形式。通过这种方法,我们巧妙地将LCA问题转化为了一个可以快速求解的RMQ问题。

最后附上代码:

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
const int LOGN = 17;

int n,p,q; // 输入,n表示长度,p表示起点,q表示重点
int a[MAXN]; 
int dp[MAXN][LOGN];

void init() {
    for (int i = 0; i < n; i++) {
        dp[i][0] = a[i];
    }
    for (int j = 1; (1 << j) <= n; j++) {
        for (int i = 0; i + (1 << j) - 1 < n; i++) {
            dp[i][j] = max(dp[i][j-1], dp[i + (1 << (j-1))][j-1]);
        }
    }
}
// 查询区间[l, r]内的最值
int query(int l, int r) {
    int k = __lg(r - l + 1); // 计算区间长度的二进制指数
    return max(dp[l][k], dp[r - (1 << k) + 1][k]);
}
int main() {
    cin >> n >> p >> q;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
    init(); // 预处理

    cout << query(p-1, q-1) << endl; // 输出区间[p, q]内的最值
    return 0;
}
//若想更改为查询最大/最小值只需要在17和24行改成max/min即可

  • 27
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值