解析RMQ

前言

RMQ问题就是所谓的“范围最小值问题”(Range Minimum Query)。

(RMQ问题)就是给出一个数组A[1],A[2],A[3]…A[n],支持查询操作Query(L,R)=min{A[L],A[L+1]…A[R]}

1.暴力(朴素)思想

这种方法看起来应该是一种非常垃圾的方法,应该没有人不会吧。原理就是对于每次查询都循环统计一个最小值,所以说每次的最坏情况的复杂度都是O(n)。这么垃圾的算法我为什么还要写呢,因为它的空间复杂度是O(1),而且代码量相当的少:

int ForceRMQ(int* A,int L,int R)//暴力枚举RMQ
{
    int Min=A[L];
    for(int i=L+1;i<=R;i++)
        Min=min(Min,A[i]);//每一次都和当前求出的最小值进行比较
    return Min;//最后就能得到区间最小值
} 

接着这种暴力的思想想下去,能不能有一种方法能使查询的速度加快。假设我们用一个数组f[L][R]表示从L位到第R位的最小值,我们先预先把这个东西计算出来,然后我们调用的复杂度就可以是O(1)了。但是这是一种更加“辣鸡”的方法,因为初始化的时间复杂度为O(n^2)。

2.Sparse-Table算法

正如我们刚刚提到的,我们可以把一些最小值预先处理出来。如果处理f[L][R]显然是不行了,那我们就去看看能不能用“二进制优化”的思想来进行优化。我们做这样的一个尝试用数组d[i][j]表示从i号元素开始的包含有2^j个元素的序列的最小值。(这是一个思想的飞跃,看不懂没关系,耐心往下读。)

也就是说d[i][j]等价于上文中的f[i][i+2^j-1]。但是不同于上次的是,我们对d数组的初始化的复杂度是O(n log n)而不是O(n^2)。比如我们想要求得d[i][j]的值,我们可以先求d[i][j-1]值(也就是f[i][i+2^(j-1)-1]的值)再求d[i+2^(j-1)][j-1]的值(也就是f[i+2^(j-1)][i+2^j-1]的值)。如下图:

递推表达式

通过这样的方法,我们就可以建立递推表达式:d[i][j]=min(d[i][j-1],d[i+2^(j-1)][j-1])。假设总共有n个元素,那么2^j<=n。所以j=log2(n),d中的元素个数就应该为nlog2(n),又因为每个元素的计算都是在常数时间内完成的,所以总时间复杂度就为O(nlogn)。(这个代码非常好,也很少而且很高效,是个OIer就该背下来。)

int d[MaxN][MaxLgN];//Sparse-Table,MaxN表示n的最大值,MaxLgN表示logn的最大值(向上取整)
void RMQ_init(int* A,int n)//n为元素个数,A为原序列
{
    for(int i=0;i<n;i++)//如果j=0,d[i][0]=A[i]因为只有一个元素
        d[i][0]=A[i];
    for(int j=1;(1<<j)<=n;j++)//当d>0且小于等于log2(n)时
        for(int i=0;i+(1<<j)-1<=n;i++)。//1<<j=2^j(左移运算)
            d[i][j]=min(d[i][j-1],d[i+(1<<(j-1))][j-1]);
}

初始化之后我们就该查询了,但是同学们会发现我们要查询的f[L][R]中的R-L+1(也就是元素个数)不一定是一个2的整数次幂。但是思考这样一个问题:

查询方式

我们可以使用一个以L开始的已知区间,和一个以R结尾的已知区间用来覆盖我要查询的时间只要保证2^k*2>=R-L+1就可以覆盖L到R的全体元素。看代码:

int RMQ(int L,int R)
{
    int k=0;
    while((1<<(k+1))<=R-L+1)
        k++;//k每次加一,一直加到覆盖整个区间
    return min(d[L][k],d[R-(1<<k)+1][k]);
    //整个的最小值,就可以看成是对两个覆盖区间的最小值取较小的一个
}

虽然这个查询函数里面用到了循环,但是由于k最多不会超过(logn)/2太多,而位移运算又“极快无比”,所以仅此看成O(1)。(这个“近似”实际上是非常合理的。)

3.线段树

没学过线段树的同学强烈建议看一下这个:我与线段树的故事(纯新手请进)

建立一棵线段树花费的时间复杂度为O(nlogn),每次查询的复杂度为O(logn)。这实际上并没有上文所说的Sparse-Table算法要高效,但是线段树可用于维护动态区间最小值,而不仅仅是RMQ(这个代码应该还是比较“优质”的)。但是单论我们现在的这个“RMQ”问题来说,上文中的“Sparse-Table”不仅代码少,而且效率也要比线段树高。

#include<iostream>
#include<cstdlib>
using namespace std;

#define MaxNLogN 20971521
#define MaxN 1048576

struct NODE//定义线段树结点
{
    int l,r;//定义结点表示的闭区间的范围
    int lch,rch;//定义结点的左子和右子
    int Min;//表示区间最小值
    void clr(int L,int R,int Lch,int Rch,int MIN)//懒人专用初始化函数
    {
        l=L;r=R;lch=Lch;rch=Rch;Min=MIN;
    }
}ns[MaxNLogN];

int newnode=1;
int A[MaxN];

void build(int root,int l,int r)//构建一棵以root为根节点,表示[l,r]闭区间的树
{
    if(l==r)//如果确定到一个元素
    {
        ns[root].clr(l,r,-1,1,A[l]);//-1表示没有字结点,最小值就是这个元素本身
        return;
    }
    int nl=newnode++;
    int nr=newnode++;//申请结点
    int mid=(l+r)/2;
    build(nl,l,mid);
    build(nr,mid+1,r);//递归构建左右子树
    ns[root].clr(l,r,nl,nr,min(ns[nl].Min,ns[nr].Min));
}

#define INF 2147483647
int query(int root,int l,int r)
{
    if(ns[root].l>r || ns[root].r<l)//当前区间与所求区间没有交集
        return INF;//返回无穷大不会影响结果(因为我们要取最小值)
    if(l<=ns[root].l && ns[root].r<=r)//如果这个区间完全在范围之内
        return ns[root].Min;//返回这个区间的最小值
    int nl=ns[root].lch;
    int nr=ns[root].rch;
    return min(query(nl,l,r),query(nr,l,r));
    //否则返回左右子树在这个区间内的最小值取较小值
}

int main()
{
    int NumCount,QuestionCount;
    cin>>NumCount;//输入序列长度
    for(int i=1;i<=NumCount;i++)
        cin>>A[i];//输入这个序列
    int root=newnode++;
    build(root,1,10);//建树
    cin>>QuestionCount;//输入询问数量
    for(int i=1;i<=QuestionCount;i++)
    {
        int L,R;
        cin>>L>>R;//输入询问
        cout<<query(root,L,R)<<endl;//输出结果
    }
    system("pause");
    return 0;
}

这就是一棵简单的线段树,一棵没有“lazy”标记的线段树。

4.后记

其实RMQ是有标准算法的,叫做“标准RMQ算法”,实际上并不一定有我们的nlogn的Sparse-Table要快,而且代码量相当大(大概是线段树代码的两倍还要多)。“标准RMQ算法”里涉及了很多奇奇怪怪的东西,比如“笛卡尔树”什么的。而唯一的优势就是时间复杂度为O(n),可能这个复杂度在n相当大的时候会发挥出作用,但是对我们OIer实在是没有什么意义,在此不再介绍,有兴趣的同学可以自行百度。

赶稿匆忙,如有谬误,望同学们谅解。不过我这回的代码都是自己编译测试通过了的,应该不会有什么问题。

后文来自百度百科:

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

主要方法及复杂度如下:

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。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值