简介

ST 表是用于解决 可重复贡献问题 的数据结构。

什么是可重复贡献问题?

可重复贡献问题 是指对于运算 \(\operatorname{opt}\) ,满足 \(x\operatorname{opt} x=x\) ,则对应的区间询问就是一个可重复贡献问题。例如,最大值有 \(\max(x,x)=x\) ,gcd 有 \(\operatorname{gcd}(x,x)=x\) ,所以 RMQ 和区间 GCD 就是一个可重复贡献问题。像区间和就不具有这个性质,如果求区间和的时候采用的预处理区间重叠了,则会导致重叠部分被计算两次,这是我们所不愿意看到的。另外, \(\operatorname{opt}\) 还必须满足结合律才能使用 ST 表求解。

什么是RMQ?

RMQ 是英文 Range Maximum/Minimum Query 的缩写,表示区间最大(最小)值。解决 RMQ 问题有很多种方法,如 线段树 、单调栈、ST表 和 Four Russian -- 基于 ST 表的算法。

引入

ST 表模板题

题目大意:给定 \(n\) 个数,有 \(m\) 个询问,对于每个询问,你需要回答区间 \([l,r]\) 中的最大值。

考虑暴力做法。每次都对区间 \([l,r]\) 扫描一遍,求出最大值。

显然,这个算法会超时。

ST 表

ST 表基于 倍增 思想,可以做到 \(\Theta(n\log n)\) 预处理, \(\Theta(1)\) 回答每个询问。但是不支持修改操作。

基于倍增思想,我们考虑如何求出区间最大值。可以发现,如果按照一般的倍增流程,每次跳 \(2^i\) 步的话,询问时的复杂度仍旧是 \(\Theta(\log n)\) ,并没有比线段树更优,反而预处理一步还比线段树慢。

我们发现 \(\max(x,x)=x\) ,也就是说,区间最大值是一个具有“可重复贡献”性质的问题。即使用来求解的预处理区间有重叠部分,只要这些区间的并是所求的区间,最终计算出的答案就是正确的。

如果手动模拟一下,可以发现我们能使用至多两个预处理过的区间来覆盖询问区间,也就是说询问时的时间复杂度可以被降至 \(\Theta(1)\) ,在处理有大量询问的题目时十分有效。

具体实现如下:

令 \(f(i,j)\) 表示区间 \([i,i+2^j-1]\) 的最大值。

显然 \(f(i,0)=a_i\) 。

根据定义式,第二维就相当于倍增的时候“跳了 \(2^j-1\) 步”,依据倍增的思路,写出状态转移方程: \(f(i,j)=\max(f(i,j-1),f(i+2^{j-1},j-1))\) 。

以上就是预处理部分。而对于查询,可以简单实现如下:

对于每个询问 \([l,r]\) ,我们把它分成两部分: \(f[l,l+2^s-1]\) 与 \(f[r-2^s+1,r]\) 。

其中 \(s=\left\lfloor\log_2(r-l+1)\right\rfloor\) 。

根据上面对于“可重复贡献问题”的论证,由于最大值是“可重复贡献问题”,重叠并不会对区间最大值产生影响。又因为这两个区间完全覆盖了 \([l,r]\) ,可以保证答案的正确性。

模板代码

ST 表模板题

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int logn = 21;
  4. const int maxn = 2000001;
  5. int Logn[maxn], f[maxn][logn];
  6. int n, m;
  7. inline int read(){
  8. int x = 0, f = 1; char ch = getchar();
  9. while (!isdigit(ch)) { if (ch == '-') f = -1; ch = getchar(); }
  10. while (isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
  11. return x * f;
  12. }
  13. void pre() {
  14. Logn[1] = 0, Logn[2] = 1;
  15. for (int i = 3; i < maxn; ++i)
  16. Logn[i] = Logn[i / 2] + 1;
  17. }
  18. int main() {
  19. //freopen("in.txt", "r", stdin);
  20. //ios::sync_with_stdio(false), cin.tie(0);
  21. n = read(), m = read();
  22. for (int i = 1; i <= n; ++i)f[i][0] = read();
  23. pre();
  24. //f(i,j) = max(f(i,j - 1),f(i + 1 << (j - 1),j - 1))
  25. for (int j = 1; j <= logn; j++)
  26. for (int i = 1; i + (1 << j) - 1 <= n; i++)
  27. f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
  28. int x, y;
  29. while (m--) {
  30. x = read(), y = read();
  31. int s = Logn[y - x + 1];
  32. printf("%d\n", max(f[x][s], f[y - (1 << s) + 1][s]));
  33. }
  34. }

注意点

  1. 输入输出数据一般很多,建议开启输入输出优化。

  2. 每次用 std::log 重新计算 log 函数值并不值得,建议进行如下的预处理:

\[\left\{\begin{aligned}
Logn[1] &=0, \\
Logn\left[i\right] &=Logn[\frac{i}{2}] + 1.
\end{aligned}\right.
\]

ST 表维护其他信息

除 RMQ 以外,还有其它的“可重复贡献问题”。例如“区间按位和”、“区间按位或”、“区间 GCD”,ST 表都能高效地解决。

需要注意的是,对于“区间 GCD”,ST 表的查询复杂度并没有比线段树更优(令值域为 \(w\) ,ST 表的查询复杂度为 \(\Theta(\log w)\) ,而线段树为 \(\Theta(\log n+\log w)\) ,且值域一般是大于 \(n\) 的),但是 ST 表的预处理复杂度也没有比线段树更劣,而编程复杂度方面 ST 表比线段树简单很多。

如果分析一下,“可重复贡献问题”一般都带有某种类似 RMQ 的成分。例如“区间按位与”就是每一位取最小值,而“区间 GCD”则是每一个质因数的指数取最小值。

总结

ST 表能较好的维护“可重复贡献”的区间信息(同时也应满足结合律),时间复杂度较低,代码量相对其他算法很小。但是,ST 表能维护的信息非常有限,不能较好地扩展,并且不支持修改操作。

练习

RMQ 模板题

SCOI2007」降雨量

平衡的阵容 Balanced Lineup


以下摘自网络,仅作为学习算法使用,侵权删。

附录:ST 表求区间 GCD 的时间复杂度分析

在算法运行的时候,可能要经过 \(\Theta(\log n)\) 次迭代。每一次迭代都可能会使用 GCD 函数进行递归,令值域为 \(w\) ,GCD 函数的时间复杂度最高是 \(\Omega(\log w)\) 的,所以总时间复杂度看似有 \(O(n\log n\log w)\) 。

但是,在 GCD 的过程中,每一次递归(除最后一次递归之外)都会使数列中的某个数至少减半,而数列中的数最多减半的次数为 \(\log_2 (w^n)=\Theta(n\log w)\) ,所以,GCD 的递归部分最多只会运行 \(O(n\log w)\) 次。再加上循环部分(以及最后一层递归)的 \(\Theta(n\log n)\) ,最终时间复杂度则是 \(O(n(\log w+\log x))\) ,由于可以构造数据使得时间复杂度为 \(\Omega(n(\log w+\log x))\) ,所以最终的时间复杂度即为 \(\Theta(n(\log w+\log x))\) 。

而查询部分的时间复杂度很好分析,考虑最劣情况,即每次询问都询问最劣的一对数,时间复杂度为 \(\Theta(\log w)\) 。因此,ST 表维护“区间 GCD”的时间复杂度为预处理 \(\Theta(n(\log n+\log w))\) ,单次查询 \(\Theta(\log w)\) 。

线段树的相应操作是预处理 \(\Theta(n\log x)\) ,查询 \(\Theta(n(\log n+\log x))\) 。

这并不是一个严谨的数学论证,更为严谨的附在下方:

更严谨的证明

理解本段,可能需要具备 时间复杂度 的关于“势能分析法”的知识。

先分析预处理部分的时间复杂度:

设“待考虑数列”为在预处理 ST 表的时候当前层循环的数列。例如,第零层的数列就是原数列,第一层的数列就是第零层的数列经过一次迭代之后的数列,即 st[1..n][1] ,我们将其记为 \(A\) 。

而势能函数就定义为“待考虑数列”中所有数的累乘的以二为底的对数。即: \(\Phi(A)=\log_2\left(\prod\limits_{i=1}^n A_i\right)\) 。

在一次迭代中,所花费的时间相当于迭代循环所花费的时间与 GCD 所花费的时间之和。其中,GCD 花费的时间有长有短。最短可能只有两次甚至一次递归,而最长可能有 \(O(\log w)\) 次递归。但是,GCD 过程中,除最开头一层与最末一层以外,每次递归都会使“待考虑数列”中的某个结果至少减半。即, \(\Phi(A)\) 会减少至少 \(1\) ,该层递归所用的时间可以被势能函数均摊。

同时,我们可以看到, \(\Phi(A)\) 的初值最大为 \(\log_2 (w^n)=\Theta(n\log w)\) ,而 \(\Phi(A)\) 不增。所以,ST 表预处理部分的时间复杂度为 \(O(n(\log w+\log n))\) 。

其它

文章开源在 Github - blog-articles,点击 Watch 即可订阅本博客。 若文章有错误,请在 Issues 中提出,我会及时回复,谢谢。

如果您觉得文章不错,或者在生活和工作中帮助到了您,不妨给个 Star,谢谢。

(文章完)

数据结构进阶:ST表的更多相关文章

  1. 数据结构:ST表

    BZOJ1699 在经历了树套树和主席树的洗礼之后,所有的数据结构都显得格外地亲切,和自然.. ST算法能够实现O(nlogn)的预处理的情况下完成O(1)的区间最值查询 虽然这要求区间是静态的,也就 ...

  2. COJ 1003 WZJ的数据结构(三)ST表

    WZJ的数据结构(三) 难度级别:B: 运行时间限制:3000ms: 运行空间限制:51200KB: 代码长度限制:2000000B 试题描述 请你设计一个数据结构,完成以下功能: 给定一个大小为N的 ...

  3. 数据结构-ST表

    数据结构-ST表 不可修改,在线查询的 RMQ 问题. 其中 \(f[i][j]\) 表示 \(i\sim i+(1<<j)-1\) 这段的 RMQ 值. 时间复杂度 \(O(n\log ...

  4. 模板 - 数据结构 - ST表/SparseTable

    SparseTable,俗称ST表,其功能,就是静态的RMQ(区间最值查询)问题的解决.注意传入查询的时候两个参数的合法性,或者可以进行一次全部初始化来使得越界值不产生负面影响.不过访问越界是写程序的 ...

  5. 区间最值的优秀数据结构---ST表

    ST表,听起来高大上,实际上限制非常多,仅仅可以求最值问题: 为什么?先从原理看起: st表运用了倍增的思想:st[i][j] = min(st[i][j - 1],st[i + 2^(j - 1)) ...

  6. [数据结构与算法-13]ST表

    ST表 主要用来快速查询静态数据区间最大值 思路 数组\(A[i][j]\)存储数列\(\{a_i\}\)中区间\(i \in [i, i+2^j)\)的最大值 查询时只需要查询\(max\{A[i] ...

  7. RMQ求解->ST表

    ST表 这是一种神奇的数据结构,用nlogn的空间与nlongn的预处理得出O(1)的区间最大最小值(无修) 那么来看看这个核心数组:ST[][] ST[i][j]表示从i到i+(1<<j ...

  8. 【笔记】自学ST表笔记

    自学ST表笔记 说实话原先QBXT学的ST表忘的差不多了吧...... 我重新自学巩固一下(回忆一下) 顺便把原先一些思想来源的原博发上来 一.ST表简介 ST表,建表时间\(O(n\cdot log ...

  9. BZOJ 4569 [Scoi2016]萌萌哒 | ST表 并查集

    传送门 BZOJ 4569 题解 ST表和并查集是我认为最优雅(其实是最好写--)的两个数据结构. 然鹅!他俩加一起的这道题,我却--没有做出来-- 咳咳. 正解是这样的: 类似ST表有\(\log ...

随机推荐

  1. Iphone上对于动态生成的html元素绑定点击事件$(document).click()失效解决办法

    在Iphone上,新生成的DOM元素不支持$(document).click的绑定方法,该怎么办呢? 百度了N久都没找到解决办法,在快要走投无路之时,试了试Google,我去,还真找到了,歪国人就是牛 ...

  2. (4)webpack中配置css,scss,less第三方loader

    为什么要使用第三方loader 一般引入样式文件,我们会在html中引入样式标签. 很明显,这样就跟之前在script中引入js文件一样,会导致二次请求.我们希望webpack像处理js文件一样处理样 ...

  3. Python 爬取 42 年高考数据,告诉你高考为什么这么难?

    作者 | 徐麟 历年录取率 可能很多经历过高考的人都不知道高考的全称,高考实际上是普通高等学校招生全国统一考试的简称.从1977年国家恢复高考制度至今,高考经历了许多的改革,其中最为显著的变化就是录取 ...

  4. 深入了解PHP的生成器

    在驾驶方面,速度并不会决定一切.但是在网络上,速度至关重要.你的应用程序越快,用户体验就越好.好吧,这时候有人就奇怪了,本文是关于PHP 生成器的,那么为什么我们要谈论速度呢?很快你就会发现,生成器在 ...

  5. Java中使用断言

    由于断言在Java程序中用于开发和测试阶段,考虑到以后很有可能会用到,在此先记类一下. 在Java语言中,给出了3种处理系统错误的机制: 1.抛出一个异常 2.日志 3.使用断言 什么时候使用断言呢? ...

  6. CentOS 7配置网卡信息,并设置yum为阿里云的镜像源

    一.问题场景 使用virtualbox创建的centos 7版本的linux虚拟机,使用ping mirrors.aliyun.com有返回,但是wget -O /etc/yum.repos.d/ep ...

  7. 关于IDEA的一些快捷键操作

    shift+F6修改实体类中的属性会重构代码

  8. 将音频文件转二进制分包存储到Redis(奇淫技巧操作)

    功能需求: 一.获取本地音频文件,进行解析成二进制数据音频流 二.将音频流转化成byte[]数组,按指定大小字节数进行分包 三.将音频流分成若干个包,以List列表形式缓存到redis数据库中 四.从 ...

  9. set自动排序去重 stringstream流分割字符

    链接:https://vjudge.net/problem/UVA-10815#author=0 题意:给几段句子,按字典序筛选出单词. 题解:用C的话太麻烦,不如用自动去重并排序的set容器.有个地 ...

  10. netcore RabbitMQ入门--win10开发环境

    安装 1.进入rabbitMQ官网下载安装包 2.点击安装包安装的时候会提示需要先装erlang 点击是会自动跳转到erlang的下载界面如果没有跳转可以直接点击这里下载,根据系统选择下载包 下载完之 ...