笔记:RMQ(区间最值)之ST算法

Posted wozaixuexi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了笔记:RMQ(区间最值)之ST算法相关的知识,希望对你有一定的参考价值。

RMQ即Range Minimum/Maximun Query,中文意思:查询一个区间的最小值/最大值

比如有这样一个数组:A{3 2 4 5 6 8 1 2 9 7},然后问你若干问题:

数组A下标2~7区间最小的值是多少?       最小值是(1)

数组A下标3~6区间最小的值是多少?       最小值是(4)

数组A下标1~10区间最小的值是多少?      最小值是(1)

......

专业术语:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j之间的最小/大值。

 

这个问题其实可以用线段树轻松实现O(N)预处理,O(logN)查询,已经是很不错的复杂度了。

但是:有的时候查询操作很多,所以我们需要一个O(1)的查询算法。

  那就是Sparse Table 即ST算法

ST算法是比较高效的在线算法。所谓在线算法,是指用户每输入一个查询便马上处理一个查询。该算法一般用较长的时间做预处理,待信息充足以后便可以用较少的时间回答每个查询。ST算法是一个非常有名的在线处理RMQ问题的算法,它可以在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询。

 

(一)首先是预处理,用动态规划(DP)解决。

设A[i]是要求区间最值的数列,F[i, j]表示从从i开始的连续2^j个数中的最大值。(DP的状态)

例如:A数列为:3  2  4  5  6  8  1  2  9   7   12   3   21

             下标为:1  2  3  4  5  6  7  8  9  10  11  12  13

 

F[1,0]表示第1个数起,长度为2^0=1的最大值,其实就是A数列中3这个数。同理:

F[1, 1] = max(3,2) = 3,

F[1,2]=max(3,2,4,5) = 5,

F[3,2]=max(4,5,6,8) = 8, F[3,2]表示从第三个数4开始连续(2^2)4个数中的最大值8

F[1,3] = max(3,2,4,5,6,8,1,2) = 8;

    

并且我们可以容易的看出F[i,0]就等于A[i]。(DP的初始值)

假如i=3,那么F[i, 0]=max(4,4)=4,  A[3]=4, 所以DP的初值就是F[i,0]=A[i]

 

这样,DP的状态、初值都已经有了,剩下的就是状态转移方程。 

我们把F[i,j]平均分成两段(因为F[i,j]一定是偶数个数字),

前半段为 i 到i + 2 ^ (j - 1) - 1,     (请动手计算一下)

后半段为i + 2 ^ (j - 1)到i + 2 ^ j - 1  需要注意的是2 ^ (j - 1)2 ^ j - 1 的区别

前后段的长度都为2 ^ (j - 1)。

用上例说明,当i=1,j=3时就是3,2,4,5 和 6,8,1,2这两段。

F[i,j]就是这两段各自最大值中的最大值。

于是我们得到了状态转移方程: 

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

方程前半段为f[i,j-1] 表示 从i 起连续2^(j-1)个数的最大值。 

方程后半段f[i+2^(j-1),j-1] 表示i + 2 ^ (j - 1)起连续2^(j-1)个数据的最大值。

然后取前后两段各自最大值中的最大值。 

总结:求从i起连续2^j个数的最大值,就是把2^j 分成前后两个2^(j-1),分别取其最大值,再通过比较获得此状态最大值。

 

我们先来学习个小知识点“<<”位移符号的使用

C或C++中:

<<可作为左移运算符 (向左移一位,右边自动补0)

比如:i<<4,是按位左移4位。

比如8<<4:

二进制状态下:

0000 0000 0000 1000 =  十进制8

                 ↓

0000 0000 1000 0000 =  十进制128   128=8^4

 

十进制状态下:

1<<1 等价于 1*2^1

1<<2 等价于 1*2^2

3<<3 等价于 3*2^3

5<<4 等价于 5*2^4        

听说位移"<<" 速度比乘法快。

注意+-运算符优先于<<符号

比如:5-1<<1等同(5-1)*2^1 ,而不是5-1*2^1

所以如果要表示 5-1*2^1,要写成5-(1<<1)

 

DP预处理的代码如下:请选择一种适合自己的方法作为模板。 

//第一种写法:请理解好循环条件的意义

int n;  //n是数组元素个数
int a[100004]; //数列
int f[100004][30]; //f[i][j]
//f[i][j]表示以i为起点,区间长度为2^j的一段区间的最小(大)值
void RMQ_ST (  )   ////预处理ST表,数组中共n个元素  O(nlogn)  
{  
for (int i=1;i<=n;i++)   f[i][0]=a[i];//dp初始值
    for(int j=1;j<=20;j++)  //为什么是20? 2^j最大可以是多大?
        for ( int i = 1; i <= n; i++ )  //思考为什么外循环j套i,而不是外循环i套j
            if(i+(1<<j)-1<=n)  //“<<”符号请看前面知识点 (1<<j)注意括号
            {  
              int s=i+(1<<(j-1)); //后半段的起点  等价 i+2^(j-1)
              f [i][j]=max ( f [i][j-1], f [s][j-1] );  //区间最大值,最小值用min函数
            }  
}

 

//第二种写法:请理解好循环条件的意义 

void RMQ_ST2 ( )   //预处理ST表,数组中共n个元素  O(nlogn)
{
for (int i=1;i<=n;i++)   f[i][0]=a[i];  //dp初始值
for (int j=1;(1<<j)<=n; j++)         //注意(1<<j)加上括号
   for (int i=1;i+(1<<j)-1<=n;i++)  //
      f [i][j]=max( f [i] [j-1], f [i+(1<<(j-1))] [j-1] );//求最小值是函数min
 }

 

这里我们需要注意的是循环的顺序,我们发现外层是j,内层所i,这是为什么呢?

可以是i在外,j在内吗?

答案是不可以。因为我们需要理解这个状态转移方程的意义。

状态转移方程的含义是:我们先得到F[i,0](即1个元素)的值,然后通过2个1个元素的最值,获得所有长度为F[i,1](即2个元素的最值),然后再通过2个2个元素的最值,获得所有长度为F[i,2](即4个元素的最值),以此类推更新所有长度的最值。 

而如果是i在外,j在内的话,我们更新的顺序就是F [1,0],F [1,1],F [1,2],F [1,3],表示更新从1开始的1个元素,2个元素,4个元素,8个元素(a[0],a[1],....a[7])的最值,这跟我们的思维和计算方法完全不同,所以这样的方法肯定是错误的。

为了避免这样的错误,一定要好好理解这个状态转移方程所代表的含义。

 

(二)接着解决怎么样查询。 

 

先看小知识:

幂:指乘方运算的结果。nm指将n自乘m次(针对m为正整数的场合)。把nm看作乘方的结果,叫做“n的m次幂”或“n的m次方”。

其中,n称为“底数”,m称为“指数”(写成上标)。当不能用上标时,通常写成n^m。

 

对数:如果 ,即a的x次方等于N(a>0,且a≠1),那么x叫做以a为底N的对数,记作。其中,a叫做对数的底数,N叫做真数

特别注意:如果在c或c++语言里,注意加上头文件cmath或math.h。

对数形式应该写成x=loga (N)其中a是底数,(N)是真数,真数N一定要用()。

比如x=log2 (8),那么x=3。注意:log和2之间不要有空格。

 还可以写成另外一种形式:x=log(N) / log(a),真数和底数都要加括号,而且真数在前面,不建议用这个形式。

 

查询:

假设要查询从(i, j)这一段的最大值, 这个区间的长度我们可以计算得出是j - i + 1

那么我们先求出一个最大的k, 使得k满足2^k <= (j - i + 1).

我们可以取k=log2( j - i + 1),举例说明:

要求区间[1,5]的最大值,k =(int)(log2(5 - 1 + 1))= 2。(计算机语言写法),

(int)是用来向下取整。(想想为什么要向下取整)

 

我们就可以把[i, j]分成两个(部分重叠的)长度为2^k的区间: [i, i+2^k-1], [j-2^k+1, j];

比如查询数列A:5,6,7,8,9,我们可以查询(1, 4) 和(2, 5)这两个区间。

就是查询5 6 7 86 7 8 9 ,原数列左边界i和右边界j是不变的, 只是中间的三个数6 7 8重叠。

 

又比如数列B: 4 5 6 7 8 9   经计算数列B长度为 j-i+1=6,那么:

k=int(log2(j-i+1))=2  (int)是用来向下取整。(想想为什么要向下取整)

[i, i+2^k-1]区间计算后为[1,4]  包含元素为 4 5 6 7共4个。

[j-2^k+1, j]区间计算后为[3,6]  包含元素为 6 7 8 9 也是4个。

 

而我们之前dp预处理时已经求出了:

f(i, k)为区间[i, i+2^k-1]的最大值, 比如上面的f(1, 4)我们在预处理时已经求出来了。

f(j-2^k+1, k)为区间[j-2^k+1, j]的最大值,比如上面的f(2, 5)我们在预处理时也已经求出来了。

我们只要返回其中更大的那个, 就是我们想要的答案, 这个算法的时间复杂度是O(1)的.

则有:RMQ(A, i, j)=max{ f [i , k], f [ j - 2 ^ k + 1, k]}。(伪代码)

 

又比如:要求区间[2,8]的最大值,k =(int)( log2(8 - 2 + 1))= 2

即求max (f [2, 2],f [8 - 2 ^ 2 + 1, 2]) = max (f [2, 2],f[5, 2]);

而f [2, 2]和f[5, 2]在前面已经预处理过了

请计算一下区间[1 ,11]的最大值,像上面一样手写过程。

 

查询代码如下:

int rmq_ask (int l, int r) //查询区间(l, r)
{
int k=(int) (log2(r-l+1));  
//l->r区间长度---为2^k(向下取整) 千万记住对数一定要整体括起来
    return max (f[l][k], f[r-(1<<k)+1][k]); //根据需要取min或者max
}

 

ST算法解RMQ模板(洛谷1816 忠诚)

https://www.luogu.org/problemnew/show/P1816

P3865 【模板】ST表

https://www.luogu.org/problemnew/show/P3865

P3379 【模板】最近公共祖先(LCA)请用RMQ

https://www.luogu.org/problemnew/show/P3379

 

参考文章:

http://blog.csdn.net/qq_35776409/article/details/62890728

http://blog.csdn.net/u012860063/article/details/40752197

http://blog.csdn.net/qq1169091731/article/details/51981497

https://wenku.baidu.com/view/6a7d691aa8114431b90dd877.html

 

以上是关于笔记:RMQ(区间最值)之ST算法的主要内容,如果未能解决你的问题,请参考以下文章

RMQ问题之ST算法

RMQ问题之ST算法

RMQ st算法 区间最值模板

ST算法(RMQ区间最值问题)

ST算法(RMQ区间最值问题)

ST算法(倍增)(用于解决RMQ)