ST算法(Sparse Table,稀疏表)

定义

ST表(Sparse Table)是一种用于高效处理区间查询的数据结构。它可以在O(1)的时间复杂度内回答某一区间的最值查询(最小值、最大值等)。ST表使用动态规划的思想,通过预处理的方式来快速计算出各个区间的最值。

概念引入

ST 表基于倍增思想,可以做到 O ( n l o g n ) O(nlogn) O(nlogn)预处理, O ( 1 ) O(1) O(1) 回答每个询问。但是不支持修改操作
基于倍增思想,我们考虑如何求出区间最大值。按照一般的倍增流程,每次跳 2 i 2^i 2i
我们发现 m a x ( x , x ) = x max(x,x)=x max(x,x)=x,也就是说,区间最大值是一个具有「可重复贡献」性质的问题。即使用来求解的预处理区间有重叠部分,只要这些区间包含所求的区间,最终计算出的答案就是正确的

应用范围

  • RMQ(英文 Range Maximum/Minimum Query 的缩写,表示区间最大(最小)值)
  • 区间按位与(或)
  • 区间 GCD

【模板】ST 表 && RMQ 问题

来源

洛谷ST表模板题

思路

考虑朴素算法,每次都遍历区间 [ l , r ] [l,r] [l,r],那么它的时间复杂度高达 O ( n m ) O(nm) O(nm)必然会超时
这时我们就需要用到 O ( n l o g n ) O(nlogn) O(nlogn)的算法,即ST倍增

我们先开一个st数组,行代表当前位置,列代表长度,它存入的值为最大值
首先我们要明白一个知识点:任何一个数都可以用二进制的数来表示
例如一个区间为30,那么它可以被分为1 2 4 8 15(不够16剩下的数拎出来)
那么这个区间可操作的次数为4次(1 2 4 8),那么我们可以用 l o g 2 log_2 log2函数求出一个区间可进行的操作次数

接着我们将小区间进行倍增,假设我们一开始的区间为1,初始化 s t [ 1 ] [ 0 ] = a [ 1 ] st[1][0]=a[1] st[1][0]=a[1]
(解释:当前位置为1,长度为 2 0 = 1 2^0=1 20=1,那么这个区间的最大值就为它本身,即 a [ 1 ] a[1] a[1]

从该位置进行倍增,每次倍增长度为 2 j 2^j 2j,那么这个区间就被划分为两个 2 j − 1 2^{j-1} 2j1
我们拿 j = 3 j=3 j=3举例

如图:
在这里插入图片描述
那么 2 3 的长度可以被划分为两个 2 2 的长度 2^3的长度可以被划分为两个2^2的长度 23的长度可以被划分为两个22的长度
我们只需要求出这两个 2 2 2^2 22长度的最大值即可,至于 2 2 2^2 22长度的最大值,则由两个 2 1 2^1 21最大值而来
因此,st表的状态转移方程就为: s t [ i ] [ j ] = m a x ( s t [ i ] [ j − 1 ] , s t [ i + ( 1 < < ( j − 1 ) ) ] [ j − 1 ] ) st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]) st[i][j]=max(st[i][j1],st[i+(1<<(j1))][j1])

以上为预处理,接下来看询问

对于每次询问,我们先算出 [ l − r ] [l-r] [lr]在二进制下的长度
那么左区间从 l 开始,长度为 2 l e n , 右区间为 r − 2 l e n + 1 ,长度也为 2 l e n 那么左区间从l开始,长度为2^{len},右区间为r-2^{len} +1,长度也为2^{len} 那么左区间从l开始,长度为2len,右区间为r2len+1,长度也为2len
(右区间保证长度为len,所以需要+1)

接下来看代码

code

const int N=1e5+5;
int a[N],lg[N];
int st[N][31];//i代表当前位置,j代表2^j(长度)
int n,m;
void init(){
	lg[1]=0;
	for(int i=2;i<=n;++i) lg[i]=lg[i>>1]+1;//log2初始化
	for(int i=1;i<=n;++i) st[i][0]=a[i];//当长度为2^0=1时,最大值为本身
	for(int j=1;j<=lg[n];++j)//以二进制优化来划分区间
	   for(int i=1;i<=n-(1<<j)+1;++i){//长度为2^j
	   	st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
        //划分两个区间,左区间从i开始,长度为2^j-1,右区间从i+2^j-1,长度也为2^j-1
	   }
}
void solve(){
	cin >> n >> m;
	for(int i=1;i<=n;++i) cin >> a[i];
	init();
	while(m--){
		int l,r;
		cin >> l >> r;
		int len=lg[r-l+1];//区间l-r在二进制下的长度
		cout << max(st[l][len],st[r-(1<<len)+1][len]) << endl;
        //左区间从l开始,长度为2^len,右区间为r-2^len +1,长度也为2^len
	}
	return ;
}

区间gcd

来源

洛谷gcd区间

思路

套RMQ模板,将max函数改为gcd函数即可
相当于每次都是求区间的最大公因数

code

const int N=1e3+5;
int a[N],lg[N];
int st[N][31];
int n,m;
int gcd(int a,int b){
	return b?gcd(b,a%b) : a;
}
void init(){
	lg[1]=0;
	for(int i=2;i<=n;++i) lg[i]=lg[i>>1]+1;
	for(int i=1;i<=n;++i) st[i][0]=a[i];
	for(int j=1;j<=lg[n];++j)
	   for(int i=1;i<=n-(1<<j)+1;++i){
	   	st[i][j]=gcd(st[i][j-1],st[i+(1<<(j-1))][j-1]);
	   }
}
void solve(){
	cin >> n >> m;
	for(int i=1;i<=n;++i) cin >> a[i];
	init();
	while(m--){
		int l,r;
		cin >> l >> r;
		int len=lg[r-l+1];
		cout << gcd(st[l][len],st[r-(1<<len)+1][len]) << endl;
	}
	return ;
}

st表的应用题

来源

P7167 [eJOI2020 Day1] Fountain

思路

考点:栈+st表

如果水超出盘子的容量,会溢出到往下一个直径比所在盘子要大的盘子里面
“后面第一个比自己大的元素”,这不就是单调栈的用法吗

因此我们可以开2个二维数组 n x t , s u m nxt,sum nxt,sum ,nxt数组用于存下标,sum数组用于存容量
(为什么开2个二维数组呢?方便我们接下来进行倍增操作)
若当前下标的直径比栈首元素的直径大,那么当前 n x t [ s . t o p ( ) ] nxt[s.top()] nxt[s.top()]存的是当前下标i,sum存的是当前下标的容量,即
n x t [ s . t o p ( ) ] [ 0 ] = i , s u m [ s . t o p ( ) ] [ 0 ] = c [ i ] nxt[s.top()][0]=i,sum[s.top()][0]=c[i] nxt[s.top()][0]=isum[s.top()][0]=c[i]
和上面模板题一样,列存的是长度,那么该点在长度 2 0 = 1 2^0=1 20=1,它的下一个元素为当前下标 i i i
存完之后将栈首出队,最后若栈还剩余,那么这些元素的下一个元素必然是水池,将这些元素都标记为0,即
n x t [ s . t o p ( ) ] [ 0 ] = 0 nxt[s.top()][0]=0 nxt[s.top()][0]=0

以上是单调栈的预处理,接下来讲st倍增的预处理
对于 l o g 2 log2 log2函数的用法,我们也可以换一种思路,让长度 j j j不断乘以2,当 j j j超出n时,停止操作

对于nxt和sum函数,我们有一个很神奇的操作
n x t [ i ] [ j ] = n x t [ n x t [ i ] [ j − 1 ] ] [ j − 1 ] nxt[i][j]=nxt[nxt[i][j-1]][j-1] nxt[i][j]=nxt[nxt[i][j1]][j1] s u m [ i ] [ j ] = s u m [ i ] [ j − 1 ] + s u m [ n x t [ i ] [ j − 1 ] ] [ j − 1 ] sum[i][j]=sum[i][j-1]+sum[nxt[i][j-1]][j-1] sum[i][j]=sum[i][j1]+sum[nxt[i][j1]][j1]
他们的状态转移方程怎么和模板不一样?它其实就是基于st倍增的思想,转换一下思路

我们拿nxt函数的状态转移方程来说明
假设i等于1
首先 n x t [ 1 ] [ 0 ] = 下一个元素 ( 假设这个元素为 2 ) nxt[1][0]=下一个元素(假设这个元素为2) nxt[1][0]=下一个元素(假设这个元素为2)
那么 n x t [ n x t [ 1 ] [ 0 ] ] [ 0 ] = n x t [ 2 ] [ 0 ] nxt[nxt[1][0]][0]=nxt[2][0] nxt[nxt[1][0]][0]=nxt[2][0]为下下个元素,即在下一个元素的基础上,在往下一个元素
它的长度为 2 1 = 2 2^1=2 21=2,这不就是st倍增的思想吗,以一个元素为踏板,在这个元素的基础上往下接着跳
sum数组存的就是这些元素的容量总和,每次先加上原来的容量,然后在加上新踏板的容量

以上为预处理,接下来看询问
若当前容量 r r r容的下所放的水,直接输出当前下标 r r r
否则,就减去当前容量,进去循环
我们每次循环都从后往前遍历,由于N的范围在 1 0 5 10^5 105,那么它所对于的二进制数大概为 2 17 2^{17} 217次方
判断当前容量是否比 s u m [ r ] [ i ] sum[r][i] sum[r][i]大,若是,则减去 s u m [ r ] [ i ] sum[r][i] sum[r][i],将r更新为当前下标,即 n x t [ r ] [ i ] nxt[r][i] nxt[r][i]
最后输出当前下标的下一个元素,即 n x t [ r ] [ 0 ] nxt[r][0] nxt[r][0]

接下来看代码

code

const int N=1e5+5;
int d[N],c[N];
int nxt[N][30],sum[N][30];
int n,q;
void init(){
	stack<int> s;
	for(int i=1;i<=n;++i){
		while(!s.empty() && d[i]>d[s.top()]){
			nxt[s.top()][0]=i;
			sum[s.top()][0]=c[i];
			s.pop();
		}
		s.push(i);
	}
	while(!s.empty()){
		nxt[s.top()][0]=0;
		s.pop();
	}
	for(int j=1;(1<<j)<=n;++j)
	   for(int i=1;i<=n-(1<<j);++i){
	   	  nxt[i][j]=nxt[nxt[i][j-1]][j-1];
	   	  sum[i][j]=sum[i][j-1]+sum[nxt[i][j-1]][j-1];
	   }
}
void solve(){
	cin >> n >> q;
	for(int i=1;i<=n;++i){
		cin >> d[i] >> c[i];
	}
	init();
	while(q--){
		int r,v;
		cin >> r >> v;
		if(c[r]>=v){
			cout << r << endl;
			continue;
		}
		v-=c[r];
		for(int i=17;i>=0;--i){
			if(nxt[r][i] && v>sum[r][i]){
				v-=sum[r][i];
				r=nxt[r][i];
			}
		}
		cout << nxt[r][0] << endl;
	}
	return ;
}
  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值