ST算法

  ST算法用于解决RMQ(Range Minimum/Maximum Query,区间最小/最大值问题)的问题。解决RMQ有三种实现的方法: 1.基于分治的树状数组  2.基于分治的线段树  3.动态规划下的ST表算法。点这里查看它们的复杂度和区别。ST算法无法修改、O(1)的查询、O(nlogn)的预处理。

  附上两个洛谷的模板题:P1816 忠诚    P2251 质量检测

  下面分析数组a的区间最小值。约定数组 a1~10 :4 7 9 6 3 2 8 5 1 6

O( nlogn )的预处理

  我们用 dp[ i ] [ j ] 存储 表示从 i 位置开始,长度为 2 ^ j (下文也称步长)这一段的最小值。dp [ 3 ] [ 2 ]=min(a3~a3+4-1),dp [ 2 ] [ 3 ]=min(a2~a2+8-1)。通俗地说 dp[ 4 ] [ 2 ] 表示从 4 位置(即数字 6)开始,长度为 2 ^ 2( 步长4 )的最小值,即 6 3 2 8 的最小值,显然 dp[ 4 ] [ 2 ] =2;

  求dp[ ] [ ] 数组的过程是:

  先求 dp[ i ] [ 0 ] ,再求 dp [ i ] [ 1 ],再求 dp [ i ] [ 2 ],再求 dp [ i ] [ 3 ] …… 这里的 i 定会满足 i+2 ^ j -1 <= 10(上述约定的数组长度)【1】,显然 dp[ i ] [ 0 ]=a i 

  对于 dp [ 1 ] [ 2 ] ,该如何求?由于长度2 ^ j (j>=1)总是可 一半一半 的,所以对于步长2 ^ j 的一段来说,我们将整段分成两等长段,两段中各自的最小值小的一个,就是整段的最小值

  不难证明   dp [ 1 ] [ 2 ] = min(dp [ 1 ] [ 1 ],dp [ 1+2 ] [ 1 ] )

    即:min(a1~a4)= min( min(a1~a2),min(a3~a4))

  推到一般情况,再巧用位运算(和乘除运算同级,写的时候要格外注意)dp [ i ] [ j ] = min(dp [ i ] [ j-1 ],dp [ i+ 1<<(j-1) ] [ j-1 ])

  前面说到 先求 dp [ i ] [ 1 ],再求 dp [ i ] [ 2 ],再求 dp [ i ] [ 3 ]……因此 每一个 i 循环结束,才会进行 j 循环。从式子也能看出,正是得到了 每个 dp[ i ] [ j-1 ],我们才能得到 dp [ i ] [ j ]。因此 j 循环是外层循环,i 是内层。实在书写不惯反的话也是对调字母。 

void pre_set()
{
    for(int i=1;i<=n;i++)
        dp[i][0]=a[i];
    for(int j=1;j<=20;j++)// 应是 (1<<j) <=n,但数据规模20以内最大了
    {
        for(int i=1;i+(1<<j)-1<=n;i++)
        {
            dp[i][j]=min(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
        }
    }
}
pre_set()

O(1)的查询

  ST算法不能修改,但有O(1)的查询。现有查询 query( l , r ) (区间长度 r - l + 1 ),如何使用dp [ ] [ ] 数组,快速得到答案?——计算这样的 k ,2 ^ k= ( r - l + 1 ),k 的值有两种情况,一为整数,二是个小数。如果 k 刚好为整数,那么dp [ l ] [ k ] 就是答案,比如 query( 3 , 6 ),则 k = 2,dp [ 3 ] [ 2 ] =min(a3~a3+4-1)= 2 就是答案;可数据随机的话,肯定大部分的 k 都不是整数,那么就没有 dp [ l ] [ x ] 刚好对应 r - l + 1 这个长度,但是可以从区间左右端点开始找相同步长的两个 dp 值,再比较这两个 dp 值,比如 query ( 2 , 10 ) = min( a2 ~ a10 ),找不到整数 k 满足那个式子,但是 min( a2 ~ a10 )= min( min( a2 ~ a9 ),min( a3 ~ a10 )),这样两段都有确切的 dp 值了。此时的步长需要非整数 k 向下取整(此情况要保证这两段相交、k 要尽可能大,取 k = floor ( log2( r - l + 1 ) )),因为满足 2 ^ k > ( r - l + 1 ) 的 k ,dp [ l ] [ k ] 的值牵扯到 [ l , r ] 之外的 ai 值,是不可以的。所以向下取整,取 k = floor ( log2( r - l + 1 ) ),当 k 为整数时,分的两段是相同的,不影响结果,可一并考虑。

void query(int l,int r)
{
    int k=0;
    while(1<<(k+1)<=r-l+1)
        k++;
    return min(dp[l][k],dp[r-(1<<k)+1][k]);
}
query()

  当数据规模非常大时,每次这样找 k 是不行的,浪费很多时间,所以就可以打表预处理出每个 可能的k ,代码如下:

void lg_init()
{
    lg[0]=-1;
    for(int i=1;i<M;i++)
        lg[i]=lg[i>>1]+1;
}
lg_init()

 最后,整个算法

const int M=1e5+5;

int n;
int a[n];
int lg[M];
int dp[M][20];

void pre_set()
{
    for(int i=1;i<=n;i++)
        dp[i][0]=a[i];
    for(int j=1;j<20;j++)
    {
        for(int i=1;i+(1<<j)-1<=n;i++)
        {
            dp[i][j]=min(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
        }
    }
}

void lg_init()
{
    lg[0]=-1;
    for(int i=1;i<M;i++)
        lg[i]=lg[i>>1]+1;
}

void query(int l,int r)
{
    int k=lg[r-l+1];
    return min(dp[l][k],dp[r-(1<<k)+1][k]);
}
RMQ

转载于:https://www.cnblogs.com/Ycourage/p/9402199.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值