• 区间统计——ST算法


    一、引入

    先举一个小栗子。

    一数组有 nn 个元素,有 mm 次询问(n,m<=105n,m<=105)。对于每次询问给出 l,rl,r,求出 [l,r][l,r]的区间和。

    有的同学说,这很简单啊!直接前缀和不就行了吗?确实如此,示例代码如下:

    复制代码
      c++
      int n, m; cin >> n >> m;
      vector< int > sum( n + 10 );
      fill( sum.begin(), sum.end(), 0 );
      for ( int i = 1, x; i <= n; ++i ) {
      cin >> x;
      sum[ i ] = sum[ i - 1 ] + x;
      }
      while ( m-- ) {
      int l, r; cin >> l >> r;
      l = min( l, r ); r = max( l, r );
      cout << sum[ r ] - sum[ l - 1 ] << endl;
      }

      但是,我们稍稍改变一下题目,将求区间和改为求区间最大值,前缀和就行不通了。我们应该如何在 O(nlogn)O(nlogn) 的时间复杂度下求得结果呢?

      二、ST 算法介绍

      上面的问题也被称为区间最值查询。(RMQRMQ, RangeRange Maximum/MinimumMaximum/Minimum QueryQuery)在静态的区间最值查询问题中,我们可以使用 STST 算法解决。

      首先我们假定需要求解的数组为 A={10,20,30,40,50,60}A={10,20,30,40,50,60},且为了方便,数组下标从 11 开始。

      由于问题可离线,我们可以先预处理,再输出答案。
      基于倍增思想,我们可以对于每一个元素构造一个倍增数组,其内容为AA[i,i+2k1][i,i+2k1]的最大值(i([1,n]N),i+2k1n,kNi([1,n]N),i+2k1n,kN),如下图所示:

      Pre 数组感性理解

      以此类推,我们可以对于每个元素构造这么一个数组,即 PrePre 数组为一个二维数组,可定义为:

      复制代码
        c++
        int pre[ maxn ][ maxlog ]; // maxlog 为上文中 k 的最大值,一般取 25 左右

        那么,我们该如何快速求解出 pre[i][j]pre[i][j] 呢?

        三、 pre[i][j] 的求法

        我们可以将 STST 算法看作一个 DP。

        首先,pre[i][j] 本身就可以视作一个状态矩阵,存储着对应区间的最值。

        接着,其边界条件是 pre[i][0],即元素本身。这很容易理解,因为 [i,i] 的最值本身就是 i 嘛。

        其次,由于预处理是离线过程,所以对于新的区间最值求解,不会对已求出区间的最值产生影响,故满足 DP 的无后效性原则。

        最后,我们来整理状态转移方程。

        对于区间 [i,j],显然可以将其二分为 [i,i+j2](i+j2,j)。若知道这两个区间的最值 pq,显然地,整个[i,j]区间的最值必然等于max(p,q)min(p,q)。这样问题就转化为求子区间的最值。以此类推直至边界。我们可以结合下图进行理解。

        Pre数组的求法

        于是我们可以轻松写出代码:

        复制代码
          c++
          int n, m; cin >> n >> m;
          for ( int i = 1; i <= n; ++i ) cin >> pre[ i ][ 0 ];
          for ( int j = 1; j <= maxlog; ++j )
          for ( int i = 1; i + ( 1 << j ) - 1 <= n; ++i )
          pre[ i ][ j ] = max(
          pre[ i ][ j - 1 ], // [i, i+2^(j-1)-1] 即前半段区间
          pre[ i + ( 1 << ( j - 1 ) ) ][ j - 1 ] // [i+2^(j-1), i+2^j-1] 即后半段区间
          ); // 因为 2^j = 2 * 2^(j-1),所以可以这么写

          四、How to query?

          预处理完毕,该如何实现高效查询呢?

          要求的区间为 [l,r],区间长度即为 rl+1。得知了区间长度,我们就可以在 Pre 中进行查找。由于区间长度不一定为 2k,kN,我们仅取一个区间返回结果不一定准确(因为 Pre 中预处理的区间长度均为 2k)所以我们需要找到一个长度,使得其为 2k 且尽量长但不超过 [l,r] 的长度。显然地,这个长度为 floor(log2(rl+1))。这个长度可以直接用于 Pre 且尽量大。所以所取区间为 [l,l+2log2(rl+1)1],在 Pre 数组中即为 pre[l][log(rl+1)]。 对于 [l,l+2log2(rl+1)1][l,r],由于 RMQ 问题的可重复贡献性,我们可以找两段重叠的区间取最值。所以可以从 r 开始向左找长度同样为 floor(log2(rl+1)) 的区间,使这个区间右端点为 r。于是第二个区间为 [r2log2(rl+1)+1,r],对应 Pre 中即为 pre[r(1<<log(rl+1))+1][log(rl+1)] 。不难发现这两个区间的并集必为 [l,r]。即两个区间最值的 max/min 一定是整个区间的最值。通过图片进行解释:

          并集

          于是我们可得出 query 函数的代码:

          复制代码
            c++
            inline int query( int l, int r ) {
            int k = log( r - l + 1 ); // 简化代码
            return max(
            pre[ l ][ k ],
            pre[ r - ( 1 << k ) + 1 ][ k ]
            );
            }

            下面是 ST 算法的模板。用于解决洛谷 P3865

            复制代码
              c++
              #include <bits/stdc++.h>
              using namespace std;
              #define k lg2[r - l + 1]
              typedef long long ll;
              template<typename T>
              inline void read(T &x) {
              T f = 1; x = T(0); char ch = getchar();
              while (!isdigit(ch)) { if (ch == '-') f = -1; ch = getchar(); }
              while (isdigit(ch)) { x = x * 10 + ch - '0'; ch = getchar(); }
              x *= f;
              }
              namespace SparseTable {
              const int MAXN = 2e6 + 10, MAXLOG = 25;
              int n, m, f[MAXN][MAXLOG], lg2[MAXN];
              void init(void) {
              // Read components
              read(n); read(m);
              for (int i = 1; i <= n; i++)
              read(f[i][0]);
              // Sparse Table
              for (int j = 1; j <= MAXLOG; 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 ]);
              // Log2
              lg2[1] = 0; lg2[2] = 1;
              for (int i = 3; i < MAXN; i++)
              lg2[i] = lg2[i / 2] + 1;
              }
              inline int query(const int l, const int r) {
              return max(f[l][k], f[r - (1 << k) + 1][k]);
              }
              }
              int main(void) {
              int l, r;
              SparseTable::init();
              while ( (SparseTable::m) --) {
              read(l); read(r);
              printf("%d\n", SparseTable::query(min(l, r), max(l, r)));
              }
              return 0;
              }
              折叠

              六、优点与局限性

              ST 算法有一些其他算法所不具备的优点,比如:

              • 代码量小
              • 常数小,时间复杂度较低

              ST 算法的局限性很大,只能解决静态区间可重复贡献问题,局限性如下:

              • 可扩展性较弱
              • 无法处理在线修改操作

              第二点实际上是 ST 表实际应用中最大的障碍。那么,如何解决在线修改查询问题呢?需要的两大杀器分别是树状数组线段树(包括 zkw 线段树),这两种数据结构将在接下来的几篇文章中介绍。

              七、参考资料:

              1. 【朝夕的ACM笔记】数据结构-ST表
              2. OIWiki ST表
            • 相关阅读:
              大数据治理的核心是什么
              2023 年最佳开源软件
              1.Javascript-JavaScript事件
              3D激光点云霍夫变换拟合直线
              做好一个BI项目的关键是什么
              “蔚来杯“2022牛客暑期多校训练营6
              MATLAB----矩阵处理
              C++11补充:智能指针如std::unique_ptr如何添加自定义的deleter
              Python博客系统源代码,基于Django + Vue.js +MySql,毕业设计
              Vulnhub靶机:GEMINI INC_ 2
            • 原文地址:https://www.cnblogs.com/Cristime/p/16466943.html