一、引入

先举一个小栗子。

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

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

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)\) 的时间复杂度下求得结果呢?

二、ST 算法介绍

上面的问题也被称为区间最值查询。(\(RMQ\), $Range $ \(Maximum/Minimum\) \(Query\))在静态的区间最值查询问题中,我们可以使用 \(ST\) 算法解决。

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

由于问题可离线,我们可以先预处理,再输出答案。

基于倍增思想,我们可以对于每一个元素构造一个倍增数组,其内容为\(A\) 中 \([i, i+2^k-1]\)的最大值(\(i\in( [1,n]\cap\N), i+2^k-1\leq n, k\in \N\)),如下图所示:

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

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

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

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

我们可以将 \(ST\) 算法看作一个 DP。

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

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

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

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

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

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

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

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

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

#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表

区间统计——ST算法的更多相关文章

  1. 求解区间最值 - RMQ - ST 算法介绍

    解析 ST 算法是 RMQ(Range Minimum/Maximum Query)中一个很经典的算法,它天生用来求得一个区间的最值,但却不能维护最值,也就是说,过程中不能改变区间中的某个元素的值.O ...

  2. 51NOD1174 区间最大数 && RMQ问题(ST算法)

    RMQ问题(区间最值问题Range Minimum/Maximum Query) ST算法 RMQ(Range Minimum/Maximum Query),即区间最值查询,是指这样一个问题:对于长度 ...

  3. HDU 5289 Assignment (ST算法区间最值+二分)

    题目链接:pid=5289">http://acm.hdu.edu.cn/showproblem.php?pid=5289 题面: Assignment Time Limit: 400 ...

  4. AcWing ST算法(区间求最值)打卡

    一,介绍 ST算法是一个用倍增来求区间最值的算法,倍增是一个与二分类似的思想的一个东西,倍增简而言之也就是区间长度按1,2,4,8..... 我们先用nlog(n)的复杂度打出一个最大值表,后面我们可 ...

  5. HDU 5289 Assignment (数字序列,ST算法)

    题意: 给一个整数序列,多达10万个,问:有多少个区间满足“区间最大元素与最小元素之差不超过k”.k是给定的. 思路: 如果穷举,有O(n*n)复杂度.可以用ST算法先预处理每个区间最大和最小,O(n ...

  6. [CF 191C]Fools and Roads[LCA Tarjan算法][LCA 与 RMQ问题的转化][LCA ST算法]

    参考: 1. 郭华阳 - 算法合集之<RMQ与LCA问题>. 讲得很清楚! 2. http://www.cnblogs.com/lazycal/archive/2012/08/11/263 ...

  7. ST算法

    作用:ST算法是用来求解给定区间RMQ的最值,本文以最小值为例 举例: 给出一数组A[0~5] = {5,4,6,10,1,12},则区间[2,5]之间的最值为1. 方法:ST算法分成两部分:离线预处 ...

  8. RMQ问题之ST算法

    RMQ问题之ST算法 RMQ(Range Minimum/Maximum Query)问题,即区间最值问题.给你n个数,a1 , a2 , a3 , ... ,an,求出区间 [ l , r ]的最大 ...

  9. RMQ之ST算法模板

    #include<stdio.h> #include<string.h> #include<iostream> using namespace std; ; ],M ...

随机推荐

  1. LCA的离线快速求法

    最常见的LCA(树上公共祖先)都是在线算法,往往带了一个log.有一种办法是转化为"+-1最值问题"得到O(n)+O(1)的复杂度,但是原理复杂,常数大.今天介绍一种允许离线时接近 ...

  2. Redis为什么变慢了?透彻解读如何排查Redis性能问题

    Redis 作为优秀的内存数据库,其拥有非常高的性能,单个实例的 OPS 能够达到 10W 左右.但也正因此如此,当我们在使用 Redis 时,如果发现操作延迟变大的情况,就会与我们的预期不符. 你也 ...

  3. GO语言学习——切片一

    切片(slice) 数组的长度的固定的.是声明之后不能变的.是类型的一部分 切片是一个引用类型 切片的定义 声明切片类型的基本语法如下: var name []T 其中, name:表示变量名 T:表 ...

  4. Python自定义排序及我实际遇到的一些题目实例

    写在前面,本文主要介绍Python基础排序和自定义排序的一些规则,如果都比较熟悉,可以直接翻到第三节,看下实际的笔试面试题中关于自定义排序的应用. 一.基础排序 排序是比较基础的算法,与很多语言一样, ...

  5. Python生成GIF动态图

    python生成摸头GIF 本篇教程演示了如何使用python的PIL库生成GIF图片 源码已经贴在文中,自行取用 效果演示 运行代码,会让你选择要制作的图片 运行完成后,会在同路径下生成dem.gi ...

  6. 手脱PESpin壳【06.exe】

    1.查壳 2.LoradPE工具检查 一方面可以用LoradPE工具查看重定位,另一方面也可获取一些详细信息 3.查找OEP ①未发现pushad 开始未发现pushad,进行单步步入,很快就能找到p ...

  7. [数学基础] 4 欧几里得算法&扩展欧几里得算法

    欧几里得算法 欧几里得算法基于的性质: 若\(d|a, a|b\),则\(d|(ax+by)\) \((a,b)=(b,a~mod~b)\) 第二条性质证明: \(\because a~mod~b=a ...

  8. vue - Vue路由(扩展)

    忙里偷闲,还在学校,趁机把后面的路由多出来的知识点学完 十.缓存路由组件 让不展示的路由组件保持挂载,不被销毁 在我们的前面案例有一个问题,都知道vue的路由当我们切换一个路由后,另一个路由就会被销毁 ...

  9. 解决windows server 2008r2服务器自动关机

    问题 具体表现就是系统自动关机,网上说是开机后2小时就会自动关机 系统版本: 解决 PsTools下载 解压:PSTools.zipg,如解压到C:\PSTools目录下 执行如下命令,打开注册表 W ...

  10. 安装Zookeeper到Linux

    系统版本:Ubuntu 16.04.5 LTS 软件版本:apache-zookeeper-3.5.8 硬件要求:无 1.安装依赖 Zookeeper需要JDK的支持. 注:需要先去JDK官网下载安装 ...