当前位置: 首页 > 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.lryc.cn/news/590326.html

相关文章:

  • LLM OCR vs 传统 OCR:解锁文档处理的未来
  • 统一日志格式规范与 Filebeat+Logstash 实践落地
  • LeetCode 3201.找出有效子序列的最大长度 I:分类统计+贪心(一次遍历)
  • 跟着Carl学算法--回溯【2】
  • Python高级编程技巧探讨:装饰器、Patch与语法糖详解
  • Android动态获取当前应用占用的内存PSS,Java
  • x86版Ubuntu的容器中运行ARM版Ubuntu
  • 消息中间件(Kafka VS RocketMQ)
  • AQS(AbstractQueuedSynchronizer)抽象队列同步器
  • 开源Web播放器推荐与选型指南
  • 开源一体化协作平台Colanode
  • uniapp小程序实现地图多个标记点
  • 数据结构与算法学习(一)
  • Java大厂面试实录:从Spring Boot到AI微服务架构的全栈挑战
  • PyCharm高效入门指南大纲
  • 图机器学习(8)——经典监督图嵌入算法
  • 浅析BLE/MQTT协议的区别
  • Web3.0与元宇宙:重构数字文明的技术范式与社会变革
  • 创客匠人解析:系统化工具如何重构知识变现效率
  • AI Agent:重构智能边界的终极形态——从技术内核到未来图景全景解析
  • UDP和TCP的主要区别是什么?
  • 智能呼叫中心系统:重构客户服务的核心引擎
  • 【保姆级喂饭教程】Idea中配置类注释模板
  • C++---emplace_back与push_back
  • Java接口:小白如何初步认识Java接口?
  • C语言 个人总结1
  • 【SF顺丰】顺丰开放平台API对接(Java对接篇)
  • AI Agent开发学习系列 - langchain之LCEL(2):LCEL 链式表达解析
  • Nand2Tetris(计算机系统要素)学习笔记 Project 0
  • 单片机学习笔记.IIC通信协议(根据数据手册写IIC驱动程序,这里以普中开发板上的AT24C02为例)