首页 > 技术文章 > 「ST表」学习笔记

JHTBlog 2019-08-25 23:57 原文

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));
	}
}

推荐阅读