RMQ问题--ST算法

建议转到另一个链接上看
sdut算法专栏
Background
了解并掌握一些入门级的动态规划

Question
RMQ (Range Minimum/Maximum Query)问题是指:

对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在[i,j]里的最小(大)值。

也就是说,RMQ问题是指求区间最值的问题。

例子:poj3264

题目链接:POJ-3264

题目大意:

给出 n 个数和 q 次询问,每次询问一个区间(a,b)上的区间最大值与最小值的差值

数据范围:1 ≤ N ≤ 50,000,1 ≤ Q ≤ 200,000

给出一串的数字,然后给出一个区间a b,输出从a到b的最大的数和最小的数的差

即先输入n和q,接下输入n个数字,接着输出q次查询,每次查询输入一个区间(a,b)

输出区间(a,b)的区间最大值与最小值的差值

Common
都是进行区间最值查询

Why can’t?
主要方法及复杂度(处理复杂度和查询复杂度)如下:

1.朴素(即搜索) O(n)-O(n)

2.线段树(segment tree) O(n)-O(logn)

3.ST(实质是动态规划) O(nlogn)-O(1)

4.树状数组(类型类似于线段树)

从上面可以看出,朴素算法时间复杂度过高,线段树logn的查询复杂度可以满足要求,但是书写起来可能代码量不小。

对于求解像最大最小值的这类问题,平时并不会单独考到,往往是一个较大的算法过程中可能会有类似的需求

这时候,我们就需要找到一个代码量少,且时间复杂度优秀的算法

因此,我们采用一种动态规划的ST算法来解决此问题。

Algorithm
ST算法(Sparse Table)是一种动态规划的方法。
以最小值为例。a为所寻找的数组.
用一个二维数组f(i,j)记录区间i,i+2^j-1区间中的最小值。其中f[i,0] = a[i];
所以,对于任意的一组(i,j),f(i,j) = min{f(i,j-1),f(i+2^(j-1),j-1)}来使用动态规划计算出来。
这个算法的高明之处不是在于这个动态规划的建立,而是它的查询:它的查询效率是O(1).
假设我们要求区间[m,n]中a的最小值,找到一个数k使得2^k

#include<stdio.h>
#include<math.h>
#include<iostream>
#include<algorithm>
#include<set>
#include<stdlib.h>
#define N 1000005
using namespace std;
long long a[N];
long long rmq[N][25];
long long mm[N];//最大的小于等于i的2^mm[i],即Log(2,i)
void initRMQ(long long n)
{
    mm[0] = -1;
    for(long long i=1;i<=n;i++)
    {
        mm[i] = ((i&(i-1))==0)?mm[i-1]+1:mm[i-1];
        //预处理计算log(2,i)
        rmq[i][0] = a[i];
    }
    for(long long j=1;j<=mm[n];j++)
        for(long long i=1;i+(1<<j)-1<=n;i++)
        rmq[i][j] =min(rmq[i][j-1],rmq[i+(1<<(j-1))][j-1]);
}
/*
        求解从x-y区间a数组最小值
*/
long long RMQ(long long x,long long y)
{
    long long k = mm[y-x+1];
    return min(rmq[x][k],rmq[y+1-(1<<k)][k]);
}
int main()
{

    long long n,l,r;
    cin>>n;
    long long ans = 0;
    for(long long  i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    initRMQ(n);
    cin>>l>>r;//输入区间端点
    cout<<RMQ(l,r)<<endl;
}




How to use it?
首先输入数组a[],然后调用initRMQ(n),n为数组a的大小

接着查询RMQ(l,r)即可,如果改成最大值,只需把min改成max

Why can?


ST算法,本质就是一个DP。

有一个数字序列记为L,比如这里给了

1   2   3   4   5   6   7   8   9   10 11 12 13 14 15 16

35 13 65 99 88 75 64 51 42 55 66 83 12 44 65 12



f[i,j]表示的是从i开始的2^j个数里面的最值。以最大值来举例说明。

f[1,0]就是从第一个开始的1个里最大值,那么就是第一个自己本身,f[1,0]=35;

f[2,2]就是从第二个开始的4个里最大值,那么是13,65,99,88里面的最大值,明显是f[2,2]=99;

以此类推。



先前说了,是DP。状态转移方程就是。

f[i,j]=max(f[i,j-1],f[i+2^(j-1),j-1]);

比如f[5,3],包含的值有88 75 64 51 42 55 66 83。

求它的最大值,用二分的思想,就是[88 75 64 51],[42 55 66 83]的最大值中较大的。

即f[5,2] 和 f[9,2]。

以此类推,求i至其后2^j个数的最大值,即把2^j 分成前后两个2^(j-1),分别取其最大值,再通过比较获得此状态最大值。

code:

void initRMQ(long long n)
{
    mm[0] = -1;
    for(long long i=1;i<=n;i++)
    {
        mm[i] = ((i&(i-1))==0)?mm[i-1]+1:mm[i-1];
        //预处理计算log(2,i)
        rmq[i][0] = a[i];
    }
    for(long long j=1;j<=mm[n];j++)
        for(long long i=1;i+(1<<j)-1<=n;i++)
        rmq[i][j] =min(rmq[i][j-1],rmq[i+(1<<(j-1))][j-1]);
}
徐红博 > RMQ问题--ST算法 > QQ截图20180129102847.png

以上是f数组的构造。

以下才回归到RMQ问题。

貌似,对于任意的i至j之间的最值,和之前构造的f数组有什么关系呢?



我们可以知道,任意的i至j之间的j-i+1个连续的值,一定可以分成两个2^n个数的两个区域(可以重合)。

徐红博 > RMQ问题--ST算法 > QQ截图20180129102901.png



再比如说要求311之间的最值。

那么可以通过310之间的最值和411之间的最值求得311之间的最值,那么就可以和之前的f数组联系在一起了。



j-i+1是个数,p=2^(  (int) (log(2,j-i+1))  )就是可以连续分的最大2^n大小的块(不难理解)。

比如311之间的9个元素,最大可以分8大小的块。

310411(从头开始数8个,从尾开始数8个)

那么就是计算 i~i+p-1,j-p+1~j的最值,每一个区域的个数是p个。

那么可以转化为f数组的f[i,log(2,p)],f[j-p+1,log(2,p)]两个范围。

即是计算

k=(int)(ln(j-i+1)/ln(2));

rmq(i,j)=max(f[i,k],f[i-2^k+1,k]);



在这里,为了避免精度等问题,我先用mm数组预处理出了对数log(2,i)的值

code:

/*
        求解从x-y区间a数组最小值
*/
long long RMQ(long long x,long long y)
{
    long long k = mm[y-x+1];
    return min(rmq[x][k],rmq[y+1-(1<<k)][k]);
}


附一个简单易懂的PPT

RMQ-PPT
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值