定义
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 问题
来源
思路
考虑朴素算法,每次都遍历区间
[
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}
2j−1
我们拿
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][j−1],st[i+(1<<(j−1))][j−1])
以上为预处理,接下来看询问
对于每次询问,我们先算出
[
l
−
r
]
[l-r]
[l−r]在二进制下的长度
那么左区间从
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,右区间为r−2len+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
来源
思路
套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]=i,sum[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][j−1]][j−1] 和
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][j−1]+sum[nxt[i][j−1]][j−1]
他们的状态转移方程怎么和模板不一样?它其实就是基于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 ;
}