「ST表」学习笔记
ST表
ST表是解决RMQ (区间最值) 问题的经典算法。
时间复杂度上 \(O(nlog(n))\) 预处理,\(O(1)\) 处理询问。
但是ST表只能用于求静态区间最值,动态区间还是需要线段树等算法。
基本原理
ST表基于倍增思想,实质是DP
考虑DP
如果用 \(f[i][j]\) 表示区间 \([i,j]\) 中的最值,显然 \(f[i][j]=max/min(f[i][j-1],a_j)\) ,这样有 \(O(n^2)\) 的复杂度,但仍不够优秀。
那么如何优化呢?
接下来就是ST表了,区间最值有一个重要的性质——max操作允许两个区间重叠,即可以用两个覆盖大区间的小区间推出一个大的区间的最值,这里就要利用倍增的思想。
我们用 \(f[i][j]\:(n*\log_2(n))\) 表示从第 \(i\) 个数开始,连续 \(2^j\) 个数中的最值,即区间 \([i,i+2^j-1]\) 中的最值。
有:
\(f[i][0]=a_i\)
\(f[i][j]=max/min(f[i][j-1],f[i+2^{j-1}][j-1])\)
那么DP转移方程是怎么来的呢?
根据定义,\(f[i][j]\) 表示的是 \([i,i+2^j-1]\) 的最值,\([i,i+2^j-1]\) 就要用 \([i,i+2^{j-1}-1]\) 和 \([i+2^{j-1},i+2^j-1]\) 来覆盖。对应到 \(f\) 数组上,就分别是 \(f[i][j-1]\:,\:f[i+2^{j-1}][j-1]\) 。
然后是如何处理询问:
对于询问的区间 \([x,y]\) ,我们要用 \(f\) 数组覆盖它:
从 \(x\) 开始向右取 \(2^{\lfloor\log_2(y-x+1)\rfloor}\) 个数作为左区间,从 \(y\) 开始也取 \(2^{\lfloor\log_2(y-x+1)\rfloor}\) 个数作为右区间。
表示层区间分别是 \([x,x+2^{\lfloor\log_2(y-x+1)\rfloor}-1]\) 和 \([y-2^{\lfloor\log_2(y-x+1)\rfloor}+1,y]\) 。
对应到 \(f\) 数组:
设 \(j=\lfloor\log_2(y-x+1)\rfloor\) ,\(Query(x,y)=max/min(f[x][j],f[y-2^j+1][j])\)
附上代码
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,m;
int a[100001];
int f[100001][50];
void init(int n) {
for(int i=1;i<=n;i++) f[i][0]=a[i];
for(int j=1;(1<<j)<=n;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]);
}
int query(int x,int y) {
int j=log(y-x+1)/log(2);
return max(f[x][j],f[y-(1<<j)+1][j]);
}
int main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
init(n);
for(int i=1;i<=m;i++) {
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",query(a,b));
}
}