一、引入

先举一个小栗子。

一数组有 \(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. 二叉查找树速通攻略 图文代码精心编写(Java实现)

    说在前面 如题目所言 这篇文章为了给下一篇二叉查找数做铺垫和前期知识准备,以便大家有良好的阅读体验,本来想合在一起的,但觉得有些长,所以就拆开了哈哈哈,还是新手向,两篇文章有些长,但如果能认真看下去, ...

  2. ansible的roles使用

    1.创建roles文件夹 mkdir roles 2.在roles文件夹里面创建文件夹 cd roles/ mkdir {nginx,uwsgi,redis,mysql} 3.cd nginx 4.m ...

  3. Java 对象头那点事

    概览 对象头 存放:关于堆对象的布局.类型.GC状态.同步状态和标识哈希码的基本信息.Java对象和vm内部对象都有一个共同的对象头格式. (后面做详细介绍) 实例数据 存放:类的数据信息,父类的信息 ...

  4. 实验:Python图形图像处理

    1. 准备一张照片,编写Python程序将该照片进行图像处理,分别输出以下效果的图片:(a)灰度图:(b)轮廓图: (c)变换RGB通道图:(d)旋转45度图. 2. 假设当前文件夹中data.csv ...

  5. Install Ubuntu on Windows Subsystem for Linux

    安装参考 ubuntu.com/wsl microsoft/wsl/install-manual microsoft/terminal 错误解决方案 github/启动 WSL 2时警告"参 ...

  6. 645. Set Mismatch - LeetCode

    Question 645. Set Mismatch Solution 思路: 遍历每个数字,然后将其应该出现的位置上的数字变为其相反数,这样如果我们再变为其相反数之前已经成负数了,说明该数字是重复数 ...

  7. Fail2ban 命令详解 fail2ban-regex

    fail2ban-regex是fail2ban提供的用来测试正则表达式的一个小工具,我们可以用它来测试正则表达式是否能够匹配到日志文件中的要禁止的IP行. fail2ban-regex默认情况下自动匹 ...

  8. Dockerfile 使用 SSH

    如果在书写 Dockerfile 时,有些命令需要使用到 SSH 连接,比如从私有仓库下载文件等,那么我们应该怎么做呢? Dockerfile 使用 SSH Dockerfile 文件配置 为了使得 ...

  9. Spring大事务到底如何优化?

    所谓的大事务就是耗时比较长的事务. Spring有两种方式实现事务,分别是编程式和声明式两种. 不手动开启事务,mysql 默认自动提交事务,一条语句执行完自动提交. 一.大事务产生的原因 操作的数据 ...

  10. Python Flask项目步骤

    构建flask项目步骤 步骤一:构建基础项目框架 创建manage.py文件 from flask import Flask app = Flask(__name__) ""&qu ...