当前位置: 首页 > news >正文

ST表及数学归纳法

ST表

  • 0. 引言
  • 1. 什么是ST表
  • 2. 如何来的?

0. 引言

\;\;\;\;\;\;\;\;介绍ST表,结论先放,然后详细说明。

1. 什么是ST表

\;\;\;\;\;\;\;\;ST 表(Sparse Table)是一种基于倍增思想的数据结构,常用于高效处理静态区间查询问题,尤其是区间最值查询(RMQ)。它的预处理时间复杂度为 O(n log n),查询时间复杂度为 O(1),但不支持动态更新。

大致分为两个步骤

  1. 预处理:用二维数组 st[i][j]st[i][j]st[i][j] 表示从位置 i 开始,长度为 2j2^j2j 的区间的最值。
  2. 查询:对于任意区间 [L,R][L, R][L,R],找到最大的 k 使得 2k≤R−L+12^k ≤ R-L+12kRL+1,然后利用两个重叠子区间[L,L+2k−1][L, L+2^k-1][L,L+2k1][R−2k+1,R][R-2^k+1, R][R2k+1,R] 的预处理结果合并得到答案。

使用场景

  1. 静态区间最值查询(如区间最大值、最小值)。
  2. 不支持动态更新(数据固定,查询频繁)。

类似的数据结构

  1. 线段树(Segment Tree)
  2. 树状数组(Binary Indexed Tree, BIT)
  3. 分块(Block)
  4. 笛卡尔树(Cartesian Tree)

2. 如何来的?

第一步:明确问题导向
要解决的问题是,如何高效查询区间内的最值问题。
比如:有10000个查询,每次查询区间[Li,Ri][L_i,R_i][Li,Ri] 内的最大值。

最直接的办法就是对于每个查询,都暴力访问所有元素;

pair<int,int> query[100010];   //假设初始化好了
vector<int>ans;
for(int i=0;i<100010;i++)
{int L=query[i].first,R=query[i].second;int Mx=0;for(int j=L;j<=R;j++){	Mx=max(Mx,nums[j];}ans.push_back(Mx);
}

这样的时间复杂度是O(n2)O(n^2)O(n2)

优化: 使用分块思想。即将数组分为一个个大小固定的区间(块),每个块保存这个区间的最大值。先预处理每个区间最大值。那么查询某个区间的时候,就可以更快找到区间的最大值。

如图,找[l,r][l,r][l,r]的最大值
在这里插入图片描述

假设

  • 数组总大小为n,分成k个块,每个块的大小为b(则k=n/bk = n/bk=n/b,简化起见假设n能被b整除);
  • 单次 “块内操作” 的成本与块大小成正比(比如块内遍历,复杂度为O(b)O(b)O(b)
  • 单次 “块间操作” 的成本与块数量成正比(比如遍历所有块,复杂度为O(k)O(k)O(k)

因此,总复杂度:O(块内成本+块间成本)=O(b+k)O(块内成本 + 块间成本) = O(b + k)O(块内成本+块间成本)=O(b+k)。但因为k=n/bk = n/bk=n/b(总大小 ÷ 块大小 = 块数量),所以总复杂度可以写成:总复杂度 = O(b+n/b)O(b + n/b)O(b+n/b)

但是问题是,如何选取块大小?问题等价于,如何选取b,使得(b+n/b)(b + n/b)(b+n/b)最小,这是一个很简单的数学问题。

  1. f(x)=x+nx(视x为连续变量)f(x) =x+\frac{n}{x}\quad \text{(视x为连续变量)}f(x)=x+xn(视x为连续变量)
  2. 求导得:f′(x)=1−nx2f'(x)=1-\frac{n}{x^2}f(x)=1x2n,令f′(x)=0f'(x)=0f(x)=0x=nx=\sqrt{n}x=n,且当x=nx=\sqrt{n}x=n的时候,f(x)f(x)f(x)取最小值。

因此当b=nb=\sqrt{n}b=n的时候,时间复杂度最低。

继续优化
很显然,块优化的方法还是有局限性,即块不能完全覆盖查找区间。那么如何才能让块覆盖查找区间呢?

  • 不定块大小

这就是ST表的核心改进。使用2幂次长度来分块。这样使得所有区间都可以被若干个块完全覆盖。为什么呢?因为任意一个整数可以拆解成若干个2的幂次相加。

那么

  • 预处理所有长度为20,21,22,...,2k2^0, 2^1, 2^2, ..., 2^k20,21,22,...,2k 的区间的最小值(其中2k≤n)2^k ≤ n)2kn
  • 查询时,用两个重叠的 2k2^k2k长度区间覆盖任意区间 [L,R][L, R][L,R],时间复杂度 O(1)O(1)O(1)

首先说查询,对于任意一个区间[L,R][L,R][L,R],都一定可以被若个长度为2ki2^{k_i}2ki次幂的区间覆盖。而且覆盖方式不止一种。但是为了让查询效率最高,最好是让两个长度为2的k次幂的区间覆盖,而且2的k次幂是小于区间长度的最大值。

在这里插入图片描述

接着说预处理,所有长度为20,21,22,...,2k2^0, 2^1, 2^2, ..., 2^k20,21,22,...,2k 的区间都需要预处理出来。如何做呢?首先暴力是可以的,但是时间复杂度是O(n2)O(n^2)O(n2)。为了优化这个过程,采用动态规划的方法。(动态规划的过程中采用倍增思想,这样就可以优化掉第一个for循环的n复杂度为logn)

首先发现,任意一个区间都可以用两个更小的区间更新,这一点就直击动态规划的核心,那就是最优子结构。

设要求最值的数列为A,设F(i,j)表示从第i个数开始连续2j个数中的最大值。设要求最值的数列为A,设F(i, j)表示从第i个数开始连续2^j个数中的最大值。设要求最值的数列为A,F(i,j)表示从第i个数开始连续2j个数中的最大值。

  • 初始化:当j=0时,20=1j = 0时,2^0 = 1j=0时,20=1,即从第i个数开始连续1个数,所以F(i,0)=A(i)F(i, 0)=A(i)F(i,0)=A(i),这就是动态规划的初值。
  • 状态转移:对于j>0j>0j>0的情况,可将F(i,j)F(i, j)F(i,j)表示的区间平均分成两段。从i到i+2j−1−1为一段,i+2j−1到i+2j−1i + 2^{j - 1}-1为一段,i + 2^{j - 1}到i + 2^{j}-1i+2j11为一段,i+2j1i+2j1为一段,这两段长度都为2j−12^{j - 1}2j1。根据定义,可得状态转移方程为F(i,j)=max⁡(F(i,j−1),F(i+2j−1,j−1))F(i, j)=\max(F(i, j - 1), F(i + 2^{j - 1}, j - 1))F(i,j)=max(F(i,j1),F(i+2j1,j1))

使用数学归纳法证明:对任意 i,ji, ji,jF(i,j)F(i,j)F(i,j) 确实表示从 iii 开始长度为 2j2^j2j 的区间最值

  1. 初始值:当 j=0j=0j=0 时,F(i,0)=1F(i,0)=1F(i,0)=1 显然成立。
  2. 归纳假设:对所有的 k<jk<jk<j(也就是长度为2k2^k2k的区间),F(i,k)F(i,k)F(i,k) 都能表示对应区间的最值。
  3. 归纳:那么对于长度为2j2^j2j的区间。首先它可以拆分为两个长度为2j−12^{j-1}2j1的区间。即F[i,j−1]以及F[i+2j−1,j−1]F[i,j-1]以及F[i+2^{j-1},j-1]F[i,j1]以及F[i+2j1,j1]。根据归纳假设,这两个子区间都满足假设的条件,那么整个区间的最大值也是两个子区间的最大值。假设成立。
#include <iostream>
#include <cmath>
using namespace std;const int MAXN = 10005;
const int MAXLOG = 20;
int f[MAXN][MAXLOG];
int a[MAXN];void buildST(int n) 
{for (int i = 1; i <= n; ++i) {f[i][0] = a[i];}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]);}}
}

预处理Log值

在查询区间最值时,需要根据区间长度计算对应的j值,即需要计算以 2 为底的对数。为了避免每次查询时都计算对数,可提前预处理 log 值。通常可以使用如下递推公式预处理:

Log2(i)=Log2(⌊2i​⌋)+1Log2(i)=Log2(⌊2i​⌋)+1Log2(i)=Log2(⌊2i⌋)+1

int Log2[MAXN];
void initLog(int n) 
{Log2[1] = 0;for (int i = 2; i <= n; ++i) {Log2[i] = Log2[i / 2] + 1;}
}

所有代码:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;struct ST
{vector<vector<int>> st;vector<int> log2;  // 预处理log值// 预处理ST表void build(const vector<int>& arr) {int n = arr.size();int maxLog = 32 - __builtin_clz(n);  // 计算最大log级别st.assign(n, vector<int>(maxLog));log2.resize(n + 1);// 预处理log2数组for (int i = 2; i <= n; i++) {log2[i] = log2[i / 2] + 1;}// 初始化j=0的情况for (int i = 0; i < n; i++) {st[i][0] = arr[i];}// 动态规划预处理for (int j = 1; (1 << j) <= n; j++)   //倍增思想,每次*2{for (int i = 0; i + (1 << j) - 1 < n; i++) {st[i][j] = max(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);}}}// 查询区间最大值int query(int l, int r) {int len = r - l + 1;int j = log2[len];return max(st[l][j], st[r - (1 << j) + 1][j]);}
};// 示例用法
int main() {vector<int> arr = {4, 6, 1, 5, 7, 3};ST st;st.build(arr);cout << "区间[1, 3]的最大值: " << st.query(1, 3) << endl;  // 输出6cout << "区间[3, 5]的最大值: " << st.query(3, 5) << endl;  // 输出7return 0;
}

以上就是ST表的基本介绍。
预处理时间复杂度 O(nlogn)O (nlogn)O(nlogn)

查询时间复杂度 O(1)O (1)O(1)

http://www.dtcms.com/a/283548.html

相关文章:

  • 洛谷 P11247 [GESP202409 六级] 算法学习-普及/提高-
  • IIS网站间歇性打不开暴力解决方法
  • 基于多源时序特征卷积网络(MSTFCN)的光伏功率预测模型
  • 对称二叉树、二叉树直径
  • 云测试提前定位和解决问题 萤火故事屋上架流程
  • LLM指纹底层技术——模型架构
  • IPD核心思想之一:业务与能力并重
  • 专业文档盖章软件,批量处理高效
  • Qt开发环境搭建指南:从零开始构建跨平台应用基石
  • MySQL InnoDB 存储引擎
  • 算法穿上隐身衣:数据交易中数据黑箱与算法透明性的法律义务边界
  • Kiro:亚马逊云发布,革命性AI编程工具!以“规范驱动开发“重塑软件构建范式!
  • 识别装甲板
  • C#语法基础总结(超级全面)(二)
  • 理解大模型的对话和检索能力
  • 车载诊断架构 --- 由于产线节拍引起的OEM EOL刷写问题
  • 标准化装配检测解决方案发布,可快速部署,换型仅需一分钟!
  • 多维傅里叶变换性质与计算
  • 权限隔离设计中实现字段级别的动态隐藏
  • [特殊字符]️ Snort 与 Suricata 入侵检测系统详解
  • Scratch竞赛题目
  • Python 多线程
  • Effective Python 第9条: 建议避免在for和while循环后使用else块
  • Scrapy无缝集成Splash:轻量级动态渲染爬虫终极解决方案
  • 轻量级消息总线实现 (C++)
  • 适用于高性能封装的TGV视觉检测方案
  • 版本更新 | 华望M-Design 4400版本功能更新详解(下篇)
  • [特殊字符] Electron 中的 `global` 变量
  • LVGL 列表表格控件颜色修改
  • 【时时三省】(C语言基础)通过指针引用多维数组2