RMQ问题 常用解法

Posted -wallace-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RMQ问题 常用解法相关的知识,希望对你有一定的参考价值。

( ext{0-RMQ})概况

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

本文所采用的例题Luogu P3865

题目所述“请注意最大数据时限只有0.8s,数据强度不低,请务必保证你的每次查询复杂度为O(1)”,而这里却不止一种解法。

( ext{I-}O(n^2))解法

朴素做法:

对于每个询问,从(A_i)扫到(A_j),途中记录最大值即可,这里就不贴Code了。

( ext{II-}O(n imessqrt n))解法

分块(平方分割)

将原序列(A)分割成许多“块”,并由(pos_i)标记(A_i)位于的块序号,由(Max_i)来管理第(i)块的最大值。对于每个询问:

  • (pos_i=pos_j),直接暴力;
  • (pos_i e pos_j),会出现“整块”和“边角块”,边角块暴力即可,整块直接取(Max)中的值。

代码:

//[Template]Sqrt Block 
//Luogu P3865 【模板】ST表
//https://www.luogu.org/problemnew/show/P3865 
#include<cstdio>
#include<iostream>
using namespace std;

const int N=1e6+10;
const int M=1e6+10;
const int SQRTN=1e3*2;
const int size=1000;
int n,m;
int A[N];
int Max[SQRTN];
int pos[N];

inline int Query_max(int l,int r)
{
    register int i;int res=-0x3f3f3f3f;
    if(pos[r]==pos[l])
    {
        for(i=l;i<=r;i++)
            res=max(res,A[i]);
        return res;
    }
    for(i=l;i<=pos[l]*size;i++)
        res=max(res,A[i]);
    for(i=pos[l]+1;i<=pos[r]-1;i++)
        res=max(res,Max[i]);
    for(i=(pos[r]-1)*size+1;i<=r;i++)
        res=max(res,A[i]);
    return res; 
}

int main()
{
    register int i;
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++)
        scanf("%d",&A[i]),pos[i]=(i/size)+1;
    for(i=1;i<=n;i++)
        Max[pos[i]]=max(Max[pos[i]],A[i]);
    while(m--)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        printf("%d
",Query_max(l,r));
    }
    return 0;
}

效率略逊于BIT等数据结构,可能会TLE,但比起朴素做法,也有较大优势。而且分块好理解,而且拓展性较高。

( ext{III-}O(n imes log(n)))解法

( ext{III-1-} Segment Tree) 线段树

总的来说,比LG的P3372简单多了(不用(lazy-tag)了)。

每个节点维护最大值,但其实只要数组即可。

代码:

//[Template]Segment Tree
//Luogu P3865 【模板】ST表
//https://www.luogu.org/problemnew/show/P3865 
#include<cstdio>
#include<iostream>
using namespace std;

const int N=1e6+10;
const int M=1e6+10;
const int ROOT=1;
int Max[4*N];
int A[N];
int n,m;

void Build(int rt,int l,int r)
{
    if(l==r)
    {
        Max[rt]=A[l];
        return;
    }
    Build(rt<<1,l,(l+r)>>1);
    Build(rt<<1|1,((l+r)>>1)+1,r);
    Max[rt]=max(Max[rt<<1],Max[rt<<1|1]);
    return;
}

int Query_max(int rt,int l,int r,int x,int y)
{
    if(x<=l&&y>=r)return Max[rt];
    if(x>r||y<l)return -0x3f3f3f3f;
    return max(Query_max(rt<<1,l,(l+r)>>1,x,y),Query_max(rt<<1|1,((l+r)>>1)+1,r,x,y));
}

int main()
{
    register int i;
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++)
        scanf("%d",&A[i]);
    Build(ROOT,1,n);
    while(m--)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        printf("%d
",Query_max(ROOT,1,n,l,r));
    }
    return 0;
}

理解以后,线段树查询的用途几乎是最广的。

( ext{III-3-} Binary Index Tree) 树状数组

BIT的空间、代码长度都比线段树更优。但是比较难理解的。比如:

技术分享图片

显然,它的空间省了许多而且更快。

//[Template]Binary Index Tree 
//Luogu P3865 【模板】ST表
//https://www.luogu.org/problemnew/show/P3865 
#include<cstdio>
#include<iostream>
#define lowbit(x) (x&-x) 
using namespace std;

const int N=1e6;
const int M=1e6;
int Max[N];
int n,cnt,temp,m;
int a[N];

inline void Add(int x)
{
    int low, i;
    while (x<=n)
    {
        Max[x]=a[x];
        low=lowbit(x);
        for (i=1;i<low;i<<=1)
            Max[x]=max(Max[x],Max[x-i]);
        x+=lowbit(x);
    }        
}
int Query_max(int x, int y)
{
    int ans=0;
    while (y>=x)
    {
        ans=max(a[y], ans);
        y--;
        while(y-lowbit(y)>=x)
        {
            ans=max(Max[y],ans);
            y-=lowbit(y);
        }
    }
    return ans;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(register int i=1;i<=n;i++)
        scanf("%d",&a[i]),Add(i);
    for(register int i=1,x,y;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        printf("%d
",Query_max(x,y));
    }
    return 0;
}

线段树虽然在许多方面逊于BIT,但之所以有线段树的存在,是因为线段树能适用于很多方面,不仅仅是区间、单点的查询修改,还有标记等等,可以用于模拟、DP等等。

参考:https://tjor.blog.luogu.org/xian-duan-shu-yu-shu-zhuang-shuo-zu

( ext{III-4} ST)

ST表是一种初始化(O(nlogn)),而查询只要(O(1))的高效算法。

我们用(Max[i][j])表示,从(i)位置开始的(2^j)个数中的最大值,例如(Max[i][1])表示的是(i)位置和(i+1)位置中两个数的最大值

那么转移的时候我们可以把当前区间拆成两个区间并分别取最大值(注意这里的编号是从(1)开始的)

查询的时候也比较简单

我们计算出(log_2{ ext{区间长度}})
然后对于左端点和右端点分别进行查询,这样可以保证一定可以覆盖查询的区间:

技术分享图片

代码:

//[Template]ST Table
//Luogu P3865 【模板】ST表
//https://www.luogu.org/problemnew/show/P3865 
#include<cstdio>
#include<cmath>
#include<iostream> 
using namespace std;

const int N=1e6+10;
const int M=1e6+10;
int n,m;
int Max[N][21];//从i位置开始的2^j个数中的最大值
 //a * (2^n) 等价于 a<< n
 
inline int Query_max(int l,int r)
{
    int k=log2(r-l+1);
    return max(Max[l][k],Max[r-(1<<k)+1][k]);
}
 
int main()
{
    register int i,j;
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++)
        scanf("%d",&Max[i][0]);
    for(j=1;j<=21;j++)
        for(int i=1;i+(1<<j)-1<=n/*===>>[i+2^j-1<=N]*/;i++)
            Max[i][j]=max(Max[i][j-1],Max[i+(1<<(j-1))/*i+2^(j-1)*/][j-1]);
    for(i=1;i<=m;i++)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        printf("%d
",Query_max(l,r));
    }
    return 0;
}

目前,最优的算法。

参考:http://www.cnblogs.com/zwfymqz/p/8581995.html

( ext{IV-})EXT拓展:

(RMQ)问题还可以有笛卡尔树莫队的做法,这里就不再写了。(其实我不会)

暂时在此。

推荐一篇文章:https://pks-loving.blog.luogu.org/senior-data-structure-gao-ji-shuo-ju-jie-gou-chu-bu-yang-xie-fen-kuai


以上是关于RMQ问题 常用解法的主要内容,如果未能解决你的问题,请参考以下文章

POJ3264 (RMQのST解法)

提高篇:RMQ问题与ST表

动态规划-RMQ问题(ST算法)

动态规划-RMQ问题(ST算法)

浅谈RMQ

RMQ问题 ST算法