RMQ

RMQ(range min/max query)区间最值查询算法。经典场景:给定一个数组A[1...N],有N个数字。查询区间[l,r],求该区间的最大/最小值。常规方法是对[l,r]进行遍历,复杂度O(n)。当查询次数K巨大时,性能会很差。RMQ算法需要nlogn时间进行预处理,之后以O(1)时间响应查询。(当然还可以使用线段树)

原理

使用二维数组dp,dp[i][j]表示从第i位开始连续2^j个数中的最值。例如dp[2][1]就是第二个数开始连续两个数的最值(A[2],A[3]),dp[i][0]表示第i个数本身。
求解dp[i][j]时,我们知道共2^j个数,必定是2的次幂,根据二进制数的性质,这2^j个数可以通过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])

void rmq_init(vector<int>& arr)
{
    int  N = arr.size();
    for(int i=1;i<=N;i++)
        dp[i][0]=arr[i];//初始化
    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]);
}
// j控制迈的步子,分别求得每2个、每4个、每8个数..的最值。
// i j 不可互换,否则变成了求第一个数开始时的2个数、4个数、8个数的最值。

分析

RMQ的查询部分,假定需要查询[l,r]的最小值,令k = log2(r-l+1),则区间[l,r]的最小值 = min(dp[l][k],dp[r-(1<<k)+1][k])
dp[l][k]维护的是区间[l,l+2^k-1],dp[r-(1<<k)+1][k]维护的是区间[r-2^k+1,r]。
只需要保证r-2^k+1<=l+2^k-1就可以保证RMQ算法的准确性。
理由:
若假设 r-2^k+1<=l+2^k-1这个式子成立,
即r-l+2<=2* 2^(k)
又k=log2(r-l+1),故r-l+2<=2*(r-l+1)
得r-l>=0
当r>=l时,可令k=log2(r-l+1)使RMQ算法成立。
查询部分的代码:

int rmq(int l,int r)
{
    int k=log2(r-l+1);
    return min(dp[l][k],dp[r-(1<<k)+1][k]);
}

例子

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
vector<vector<int>>dp(100,vector<int>(100));
void rmq_init(vector<int>& arr)
{
    int  N = arr.size();
    for(int i=1;i<=N;i++)
        dp[i][0]=arr[i];//初始化
    // nlogn的预处理
    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)
{
    int k=log2(r-l+1);
    return min(dp[l][k],dp[r-(1<<k)+1][k]);
}
int main()
{
    // 数组长度10,有效部分从下标1开始
    vector<int>v = {0,1,2,4,3,5,6,9,12,34,7};
    for(int i=1;i<=10;++i) cout<<v[i]<<" ";
    cout<<endl;
    rmq_init(v);
    while(1)
    {
        int a,b;
        cin>>a>>b;
        cout<<rmq(a,b)<<endl;
    }
    return 0;
}