ST表-数据结构

最近刷题有点疯狂,写篇ST表冷静一下,顺便回顾一下。

什么是ST表

傻子或大犇都会看的定义

ST表是一种用于解决区间最值问题的数据结构,它的全称是Sparse Table,意为稀疏表。它的主要思想是预处理出每个区间的最值,然后通过预处理的结果来快速求出任意区间的最值。

有点蒙? 没关系,我当初也是,下面我会十分详细的一步一步推导。

从求最大值说起…

现在有一个序列a,我们想要求出区间 [ l , r ] [l,r] [l,r]的最大值,请问该怎么做?
想必大家都会想到枚举这一优美且高效(搞笑)的方法,所以我们不妨试一试。

方法自然是从 a [ l ] a[l] a[l]开始枚举到 a [ r ] a[r] a[r],每两个数取最大值,最后得到的就是区间 [ l , r ] [l,r] [l,r]的最大值。

int ans = a[l];
for(int i = l + 1; i <= r; i++)
    ans = max(ans, a[i]);

真逝完美!
来分析一下时间复杂度,每次枚举需要 O ( 1 ) O(1) O(1),总共需要枚举 r − l + 1 r-l+1 rl+1次,所以总时间复杂度为 O ( n ) O(n) O(n)。还算过得去,对吧?但是,出题老师不可能放你暴力过的,倘若ta轻描淡写地来一句 “n组询问,每组一个[l,r]”,那就惨了,代码就被迫变成了这样:

int ans = a[l];
for(int i = 1;i <= n;i++)
    int l, r;
    cin >> l >> r;
    for(int j = l;j <= r;j++)
        ans = max(ans, a[j]);

这样的时间复杂度就是 O ( n 2 ) O(n^2) O(n2)了,显然是不可接受的。为了解决这个问题,ST表应运而生。

ST表的过程

ST表只有两个步骤,第一个是 O ( n l o g n ) O(nlogn) O(nlogn)的预处理,第二个是 O ( 1 ) O(1) O(1)的查询。显然比暴力要快得多。

预处理

预处理用到了动态规划的思想,我们定义一个二维数组 s t st st ,其中 s t i j st_{ij} stij 表示从 a i a_i ai 开始,长度为 2 j 2^j 2j 的区间的最大值。也就是区间 [ i , i + 2 j − 1 ] [i,i+2^j-1] [i,i+2j1]的最大值。

接下来便是构造动态转移方程,在构造之前,先来看看求最大值这件事的一个性质:
m a x ( a 1 , a 2 . . . a n ) = m a x ( m a x ( a 1 , a 2 . . . a k ) , m a x ( a k , a k + 1 . . . a n ) ) max(a_1,a_2...a_n)=max(max(a_1,a_2...a_k),max(a_k,a_{k+1}...a_n)) max(a1,a2...an)=max(max(a1,a2...ak),max(ak,ak+1...an))
这个性质很好理解,就是把一个区间分成两个区间,然后求两个区间的最大值,最后再求两个最大值的最大值,就是整一个大区间的最大值。这样就把一个大区间的最大值转化成了两个小区间的最大值。

这么说…撕,那转移方程不就有了吗?对于 [ i , i + 2 j − 1 ] [i,i+2^j-1] [i,i+2j1]的最大值,就有:
m a x ( [ i , i + 2 j − 1 ] ) = m a x ( m a x ( [ i , i + 2 j − 1 − 1 ] ) , m a x ( [ i + 2 j − 1 , i + 2 j − 1 ] ) ) max([i,i+2^j-1])=max(max([i,i+2^{j-1}-1]),max([i+2^{j-1},i+2^j-1])) max([i,i+2j1])=max(max([i,i+2j11]),max([i+2j1,i+2j1]))

ST表

而:
m a x ( [ i , i + 2 j − 1 ] ) = s t i , j max([i,i+2^{j}-1])=st_{i,j} max([i,i+2j1])=sti,j
m a x ( [ i , i + 2 j − 1 − 1 ] ) = s t i , j − 1 max([i,i+2^{j-1}-1])=st_{i,j-1} max([i,i+2j11])=sti,j1
m a x ( [ i + 2 j − 1 , i + 2 j − 1 ] ) = s t i + 2 j − 1 , j − 1 max([i+2^{j-1},i+2^j-1])=st_{i+2^{j-1},j-1} max([i+2j1,i+2j1])=sti+2j1,j1
所以:
s t i , j = m a x ( s t i , j − 1 , s t i + 2 j − 1 , j − 1 ) st_{i,j}=max(st_{i,j-1},st_{i+2^{j-1},j-1}) sti,j=max(sti,j1,sti+2j1,j1)
这样转移方程不就出来了么?

for(int i = 1;i <= n;i++)//枚举起点
    st[i][0] = a[i];
for(int j = 1;j <= log2(n);j++)//枚举区间长度
    for(int i = 1;i + (1 << j) - 1 <= n;i++)//枚举起点
        st[i][j] = max(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);//(1 << j) = 2^j

查询

查询的时间复杂度度是 O ( 1 ) O(1) O(1)的。既然我们已经预处理出所有起点的所有长度为 2 j 2^j 2j的区间的最大值,如果我们要查询 m a x ( [ l , r ] ) max([l,r]) max([l,r]),其实只需要找到这段区间所对应的 s t l x st_{lx} stlx就行了。在区间和 s t st st表中, l l l i i i的意义都是一样的,所以 i i i就是 l l l,而 x x x也不难推导,我们知道 s t i x st_{ix} stix就是 m a x ( [ i , i + 2 x − 1 ] ) max([i,i+2^x-1]) max([i,i+2x1]),而我们要查询 m a x ( [ l , r ] ) max([l,r]) max([l,r]),所以
r = i + 2 x − 1 r=i+2^x-1 r=i+2x1
因为 i = l i=l i=l,所以
r = l + 2 x − 1 2 x = r − l + 1 x = l o g ( r − l + 1 ) \begin{align} r&=l+2^x-1\\ 2^x&=r-l+1\\ x&=log(r-l+1) \end{align} r2xx=l+2x1=rl+1=log(rl+1)

确实

由于 l o g log log函数的值是浮点数,向下取整可能导致查询的区间长度不够,向上取整又可能多查了,这怎么办?
在预处理的时候提出了一个性质:

m a x ( a 1 , a 2 . . . a n ) = m a x ( m a x ( a 1 , a 2 . . . a k ) , m a x ( a k , a k + 1 . . . a n ) ) max(a_1,a_2...a_n)=max(max(a_1,a_2...a_k),max(a_k,a_{k+1}...a_n)) max(a1,a2...an)=max(max(a1,a2...ak),max(ak,ak+1...an))
把它稍微改一下:
m a x ( a 1 , a 2 . . . a n ) = m a x ( m a x ( a 1 , a 2 . . . a m ) , m a x ( a k , a k + 1 . . . a n ) ) 其中, m ≤ n , k ≥ 1 max(a_1,a_2...a_n)=max(max(a_1,a_2...a_m),max(a_{k},a_{k+1}...a_n))\\其中,m{\le}n,k{\ge}1 max(a1,a2...an)=max(max(a1,a2...am),max(ak,ak+1...an))其中,mn,k1

ST表2

也就是说,就算被拆成的两个区间有重叠,只要它俩能填满整个区间,那么这整个区间的最大值就是这两个区间的最大值的最大值。这样就可以解决上面的问题了,我们只需要对 x = l o g ( r − l + 1 ) x=log(r-l+1) x=log(rl+1)向下取整,即
x = ⌊ l o g ( r − l + 1 ) ⌋ x=\lfloor log(r-l+1) \rfloor x=log(rl+1)⌋
然后用 r − ( 2 x − 1 ) r-(2^x-1) r(2x1),记为 d d d,就是这一段

ST表3

如图3所示, [ l , x ] [l,x] [l,x] [ d , r ] [d,r] [d,r]的长度都是 2 x 2^x 2x,所以我们可以直接用 s t l x st_{lx} stlx s t d r st_{dr} stdr来代替 m a x ( [ l , r ] ) max([l,r]) max([l,r]),也就是
m a x ( [ l , r ] ) = m a x ( s t l x , s t d r ) max([l,r])=max(st_{lx},st_{dr})\\ max([l,r])=max(stlx,stdr)

代码:

int find(int l,int r){
    int x=Log2[r-l+1];
    return max(st[l][x],st[r-(1<<x)+1][x]);
}

小优化

上面的代码还是有点慢,因为 l o g log log函数是 O ( l o g n ) O(logn) O(logn)的,所以我们可以用一个数组来存储 l o g log log函数的值,这样就不用每次都要调用 l o g log log函数了。

for(int i = 2; i <= n; i++)
    Log2[i] = Log2[i >> 1] + 1;

ST表的应用

通过上面的例子,很容易看出ST表在求最大值的时候的优越性,但是ST表不仅仅可以求最大值,还可以求最小值,甚至是求区间和,区间异或和等等。
其实,这一类问题都被称为可重复贡献问题,定义就是对于运算 o p t opt opt,有 x x x o p t opt opt x = x x=x x=x,像 m a x ( x , x ) = x , m i n ( x , x ) = x max(x,x)=x,min(x,x)=x max(x,x)=xmin(x,x)=x等。

但ST表也有它的不足,比如它只能维护少量信息,而且不支持修改。

几道破题

P3865 【模板】ST表

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+1;
int n,m,st[N][100],Log2[N];

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

void initST(){
    for(int j=1;j<=21;j++){
        for(int i=1;i+(1<<j)-1<=n;i++){
			st[i][j]=max(st[i][j-1],st[i+(1<<j-1)][j-1]);
        }
    }

}
void initLOG2(){
    for (int i=2;i<=n;i++)
    Log2[i]=Log2[i/2]+1;
}
int find(int l,int r){
    int x=Log2[r-l+1];
    return max(st[l][x],st[r-(1<<x)+1][x]);
}

int main(){ 
	n=read(),m=read();
	
	for(int i=1;i<=n;i++){
	    st[i][0]=read();
	} 
    initST();
    initLOG2();
    for(int i=1;i<=m;i++){
        int l=read(),r=read();
        printf("%d\n",find(l,r));
    }
	
	
	return 0;
} 

没啥好说的,就是模板题,不过这题的数据强度不低,记得用快速读入。

P2880 [USACO07JAN] Balanced Lineup G

#include<bits/stdc++.h>
using namespace std;
const int N=5e4+1;
int n,q,stMax[N][23],stMin[N][23],Log2[N];
void initLOG2(){
	for(int i=2;i<=n;i++)Log2[i]=Log2[i/2]+1;
}
void initST(){
	for(int j=1;j<23;j++){
		for(int i=1;i+(1<<j)-1<=n;i++){
			stMax[i][j]=max(stMax[i][j-1],stMax[i+(1<<j-1)][j-1]);
			stMin[i][j]=min(stMin[i][j-1],stMin[i+(1<<j-1)][j-1]);
		}
	}
}
int find(int l,int r){
	int x=Log2[r-l+1];
	int maxx=max(stMax[l][x],stMax[r-(1<<x)+1][x]);
	int minn=min(stMin[l][x],stMin[r-(1<<x)+1][x]);
	return (maxx-minn);
}
int main(){
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++){
		scanf("%d",&stMax[i][0]);
		stMin[i][0]=stMax[i][0];
	}
	initST();
	initLOG2();
	for(int i=1;i<=q;i++){
		int a,b;
		scanf("%d%d",&a,&b);
		printf("%d\n",find(a,b));
	}
	return 0;
} 

同样没啥好说的,最大值和最小值同时求就好。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值