【码极客精讲】RMQ算法

RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题。

主要方法及复杂度如下:

1、朴素(即搜索),O(n)-O(qn) online。

2、线段树,O(n)-O(qlogn) online。

3、ST(实质是动态规划),O(nlogn)-O(q) online。

ST算法(Sparse Table),以求最大值为例,设d[i,j]表示[i,i+2^j-1]这个区间内的最大值,那么在询问到[a,b]区间的最大值时答案就是max(d[a,k], d[b-2^k+1,k]),其中k是满足2^k<=b-a+1(即长度)的最大的k,即k=[ln(b-a+1)/ln(2)]。

d的求法可以用动态规划,d[i, j]=max(d[i, j-1],d[i+2^(j-1), j-1])。

4、RMQ标准算法:先规约成LCA(Lowest Common Ancestor),再规约成约束RMQ,O(n)-O(q) online。

RMQ标准算法程序

首先根据原数列,建立笛卡尔树,从而将问题在线性时间内规约为LCA问题。LCA问题可以在线性时间内规约为约束RMQ,也就是数列中任意两个相邻的数的差都是+1或-1的RMQ问题。约束RMQ有O(n)-O(1)的在线解法,故整个算法的时间复杂度为O(n)-O(1)。

来看一下ST算法是怎么实现的(以最大值为例):

首先是预处理,用一个DP解决。设a是要求区间最值的数列,f[i,j]表示从第i个数起连续2^j个数中的最大值。例如数列3 2 4 5 6 8 1 2 9 7 ,f[1,0]表示第1个数起,长度为2^0=1的最大值,其实就是3这个数。f[1,2]=5,f[1,3]=8,f[2,0]=2,f[2,1]=4……从这里可以看出f[i,0]其实就等于a[i]。这样,DP的状态、初值都已经有了,剩下的就是状态转移方程。我们把f[i,j](j≥1)平均分成两段(因为j≥1时,f[i,j]一定是偶数个数字),从i到i+2^(j-1)-1为一段,i+2^(j-1)到i+2^j-1为一段(长度都为2^(j-1))。用上例说明,当i=1,j=3时就是3,2,4,5 和6,8,1,2这两段。f就是这两段的最大值中的最大值。于是我们得到了动规方程F[i,j]=max(F[i,j-1],F[i+2^(j-1),j-1])。

接下来是得出最值,也许你想不到计算出f有什么用处,一般要想计算max还是要O(logn),甚至O(n)。但有一个很好的办法,做到了O(1)。还是分开来。如在上例中我们要求区间[2,8]的最大值,就要把它分成[2,5]和[5,8]两个区间,因为这两个区间的最大值我们可以直接由f[2,2]和f[5,2]得到。扩展到一般情况,就是把区间[l,r]分成两个长度为2^n的区间(保证有f对应)。直接给出表达式:

k:=trunc(ln(r-l+1)/ln(2));

ans:=max(F[l,k],F[r-2^k+1,k]);

这样就计算了从l开始,长度为2^k的区间和从r-2^k+1开始长度为2^k的区间的最大值(表达式比较繁琐,细节问题如加1减1需要仔细考虑),二者中的较大者就是整个区间[l,r]上的最大值

数组A[0,N-1]的笛卡尔树C是这样一棵二叉树:当N=0,它是一棵空树,否则它的根节点是A中的一个最小元素A[i](并以这个最小元素的下标i标记),而左右子树分别是A[0,i-1]和A[i+1,N-1]的一棵笛卡尔树。注意如果A中有相等的元素,则A的笛卡尔树不一定唯一,但在这里我们限定所用的最小元素为在数组中最先出现者,在此限制下笛卡尔树是唯一的。

容易看出,数组A在闭区间[l,r]上的最小值等同于笛卡尔树C中下标为l和r的两个顶点的最近公共祖先(LCA)的值。由此,RMQ问题可以转化为LCA问题。下面说明如何在O(n)时间内实现这一转化。

我们将要将A的元素依次插入笛卡尔树C。每次插入都可能使树的形态发生变化。为了在O(N)的时间内完成整个插入过程,考虑C的右链,即根结点、根结点的右儿子、根结点的右儿子的右儿子……组成的链。注意这些元素的下标和值都是递增的。下标最大,即将要插入的元素A[i]一定是新树右链的最后一个元素。原来的右链中,值比A[i]大的元素在新树中不再属于右链,这些元素组成的链成为A[i]的左子树的右链;原来右链中的其它元素加上A[i]组成了新的右链。初看起来,寻找分界点的最佳方法是O(logN)时间的二分查找;但是对于整个过程来说,O(NlogN)的时间复杂度不是最优的。关键在于一旦一个元素比A[i]大,它就从右链中被永久地移除了。如果按照从后到前的顺序判断一个元素是否大于A[i],则每次插入的时间复杂度为O(k+1),k为本次插入中移除的右链元素个数。因为每个元素最多进出右链各一次,所以整个过程的时间复杂度为O(N)。

用一个栈结构维护右链元素的下标,上述过程可以很容易地实现。(见下面代码部分)

转化为约束RMQ

为了将LCA问题转化为约束RMQ,我们注意到任意树中两个结点u和v的LCA就是在一次从树根开始的深度优先搜索中,在u和v之间(包括回溯时)到达的结点中层数最小的一个。为了利用这一事实,我们建立三个数组:

E[1,2*N-1]:在一次深度优先搜索(恰好是树的一次欧拉环游)中每一步到达的结点。

L[1,2*N-1]:E中对应结点在树中的层数。

H[1,N]:每个结点在E中某一次出现的下标(不妨设为第一次)。

则对任意u和v,不妨设H[u]≤H[v](否则交换u和v),只要在L中找到[H[u],H[v]]中最小值的下标i,则E[i]就是u和v的LCA。注意到L满足约束RMQ的条件(相邻元素差的绝对值为1),这说明原来的LCA问题已经被转化为约束RMQ。转化过程显然能在O(N)时间内完成。

约束RMQ的解法

现在仍旧用A[0,N-1]表示问题中的数列,这里有|A[i]-A[i-1]|=1(i=1,2,...,N-1)成立。

将A分解为长度为l=[(log N)/2]的块。设A'[i]为第i块中的最小值,B[i]为该最小值的位置。A'[i]和B[i]的长度均为N/l, 所以用ST算法处理A'数组的时空复杂度均为O(N/l*log(N/l))=O(N/logN*(logN-logl))=O(N)。预处理之后,对任意多连续的块进行的查询都能在O(1)时间内实现。余下的问题是如何进行块内查询。

注意到对任意一块中的块内查询的结果有影响的唯一因素是块内每相邻两个元素间的“升降关系”构成的序列。因为每两个元素之间的关系只有两种(“+1”、“-1”),而块的长度又只有l=[(log N)/2],所以本质不同的块最多有2^I=O(sqrt N)种。对每种块中所有可能的块内查询预处理出答案的时空复杂度是O(sqrt N*l^2)=O(N)(这里的O(N)表示不超过线性时间)。预处理出所有块的“类型”,并用二进制数存储的时间复杂度是O(N)。

此后,每次查询可以分为两种情况:

1、块内查询,答案已经被预处理出,只要在数组中找到它即可。

2、块间查询,可以分解为2个块内查询,和一个A'上的RMQ,三者的时间复杂度都是O(1)。

综上,我们给出了一个预处理时间为O(n),查询时间为O(1)的在线RMQ算法。

RMQ(Range Minimum/Maximum Query)问题是求区间最值问题。你当然可以写个O(n)的(怎么写都可以),但是万一要询问最值1000000遍,估计你就要挂了。这时候你可以放心地写一个线段树(前提是不写错)O(logn)的复杂度应该不会挂。但是,这里有更牛的算法,就是ST算法,它可以做到O(nlogn)的预处理,O(1)地回答每个询问。

C++代码

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
//#include <bits/stdc++.h>
using namespace std;
int map[1000005][20];
int n;
void work()
{
    int i,j;
    for(j=1;1<<j<=n;j++)
    for(i=1;i+(1<<j)-1<=n;i++)//i+(1<<j)-1<=n是为了保证区间左端点不超出总数n
    map[i][j]=min(map[i][j-1],map[i+(1<<j-1)][j-1]);//实质是动态规划
}
int question(int z,int y)
{
    int x=int (log(y-z+1)/log(2));//注意y-z要加一才为区间长度
    return min(map[z][x],map[y-(1<<x)+1][x]);//分别以左右两个端点为基础,向区间内跳1<<x的最
//大值; 
}
int main()
{
    scanf("%d",&n);//输入数据总数
    int i,a,b,k;
    for(i=1;i<=n;i++)
    scanf("%d",&map[i][0]);//数据输入加初始化,即从i开始向右走2的0次方的区间中的最大值,(注//意i到i的长度为一)。
    work();//预处理
    scanf("%d",&k);//输入询问次数k
    for(i=1;i<=k;i++)
        {
            scanf("%d%d",&a,&b);
            printf("%d\n",question(a,b));//输出结果
        }
    return 0;
}

Java代码

public class RMQ{
    int[] nums;
    int[][] st;
    
    public RMQ(int[] nums){
        this.nums = nums;
        int n = nums.length;
        this.st = new int[n][(int)(Math.log(n + 1) / Math.log(2)) + 1];
        RMQInit();
    }
    
    private void RMQInit(){
        int n = nums.length;
        for(int i = 0; i < n; i++)  st[i][0] = nums[i];
        
        for(int j = 1; (1 << j) <= n; j++){
            for(int i = 0; i + (1 << j) - 1 < n; i++){
            st[i][j] = Math.max(st[i][j - 1], st[i + (1 << (j - 1) )][j - 1]);
            }
        }
    }
    
    public int query(int u, int v){
        return RMQQuery(u, v);
    }
    
    private int RMQQuery(int u, int v){
        int k = (int) (Math.log(v - u + 1) / Math.log(2));
        return Math.max(st[u][k], st[v - (1 << k) + 1][k]);
    }
}

C++建立笛卡尔树

voidcomputeTree(intA[MAXN],intN,intT[MAXN])//T[i]储存每个结点的父结点(左右子树是无所谓的)
{
    intst[MAXN],i,k,top=-1;
    //从空栈开始
    //第i步,我们将A[i]插入栈中
    for(i=0;i<N;i++)
    {
    //找到第一个小于等于A[i]的元素
    k=top;
    while(k>=0&&A[st[k]]>A[i])
        k--;
    //如上述,更改树的结构
    if(k!=-1)
        T[i]=st[k];
    if(k<top)
        T[st[k+1]]=i;
    //将A[i]插入栈中,并移除所有更大的元素
    st[++k]=i;
    top=k;
    }
    //栈中的第一个元素就是树根,没有父节点
    T[st[0]]=-1;
}

 

  • 8
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值