• ST表 求 RMQ(区间最值)


    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

  • 相关阅读:
    c# 测试篇之Linq性能测试
    F# 笔记
    c# DataSource和BindingSource
    .net中配置的保存格式笔记
    泛型约束(转)
    c# 调用showDialog后需要Dispose
    c# 实现ComboBox自动模糊匹配
    c# 二进制或算法实现枚举的HasFlag函数
    C# WinForm自定义控件整理
    微软中文MSDN上的一些文章链接
  • 原文地址:https://www.cnblogs.com/wozaixuexi/p/8411984.html
Copyright © 2020-2023  润新知