ST表
本文仅发布于此博客和作者的洛谷博客,不允许任何人以任何形式转载,无论是否标明出处及作者。
0x00 概述
ST表(Sparse Table)是一种基于倍增思想的数据结构,可用于区间查询。
0x10 ST表
0x11 模板
给定一个长度为 的数列,和 次询问,求出每一次询问的区间内数字的最大值。
对于 的数据,满足 ,。
可以发现,询问的次数明显大于数列长度,线段树每次查询是的,TLE,无法通过。
0x12 正解: ST表。
我们可以维护这样一个数据结构:
-
建立:设 为数列长度,原数列为
a[i]
,.建立一个表格g[n][m]
,g[i][j]
表示区间 中的最大值,即 . -
初始化:显然,当 时,
g[i][j]=a[i]
。 时,g[i][j]=max(g[i][j-1],g[i+pow(2,j-1)][j-1])
如图。
-
查询:设查询区间左右端点为 ,长度 。我们在表里找到两个区间 ,满足:可重但是不能漏地覆盖 。
显然, 的左端点是 , 的右端点是 。
两个区间的一个端点已经分别确定,只需要一个合适的区间长度。
设区间长度 ,则区间长度显然必须满足这些条件:
- ,不然没有办法全部覆盖。
- ,不然 会超过查询区间的边界。
- ,其中 ,不然 在表格里面找不到。
并且两个区间的长度都为 时,可以满足上面的条件。
时,符合条件。
此时, ,在
g[n][m]
里就分别是t1=g[l][x]
和t2=g[r-pow(2,x)+1][x]
。最后的结果就是
max(t1,t2)
。
0x13 复杂度分析
空间复杂度:一个 行列的表,。
时间复杂度:初始化时每次填入一个g[i][j]
,表格里有 个数需要计算,所以是 的。查询时得到答案。总复杂度是 。
可以通过模板题。
0x14 代码
题目:link
#include<bits/stdc++.h>
using namespace std;
int g[100005][17];//log 1e5=16.6
int main(){
int n,m,q;
cin>>n>>q;
m=__lg(n)+1;//st表的行数,__lg(x)可以在O(1)内返回floor(log2(x)).
for(int i=1;i<=n;i++){
cin>>g[i][0];
}
for(int j=1;j<=m;j++){
for(int i=1;i+(1<<j)-1<=n;i++){//区间不能超
g[i][j]=max(g[i][j-1],g[i+(int)pow(2,j-1)][j-1]);
}
}
for(int i=1;i<=q;i++){
int l,r;
cin>>l>>r;
int len=r-l+1;
int x=__lg(len);
int ans=max(g[l][x],g[r-(int)pow(2,x)+1][x]);
cout<<ans<<endl;
}
return 0;
}
轻微卡常(快读,\n
,pow
改成位运算等)即可通过。
注意:这里初始化最好就这么写,最好不要进行什么额外的操作,不然容易出锅。
0x20 优缺点分析
我们把它和也可以完成区间RMQ的线段树相比。
0x21 优点
-
显然,三个数据结构中,只有ST表能做到O(1)查询,适合在很大的时候使用。
-
而且,ST表的码量比线段树少很多,甚至比树状数组还小一点。
0x22 缺点
-
只能计算可重复贡献问题,比如说像最大值这种,。 被重复计算但是对结果没有影响。可重复贡献问题还有区间按位与,按位或,GCD等。
连一个区间求和都做不成 -
不能做带修的。
0x30 习题
0x31 Problem 1
link:P7333 [JRKSJ R1]JFCA 名字好评
Solution:
首先进行一个破环为链,变成一个长度为的链(经典操作),只存 , 不管。
显然,区间 的最大值在 递增时,是单调递增的,于是我们可以有以下操作:
对于每一个点 ,如果链上的区间 的最大值 (等价于“存在点 满足 ”),那么就在此区间上二分,找到一个满足 的最大值 且编号尽可能最小的点 ,算一下 和 的距离即可。
对于区间 进行相似的操作,所得结果和上面那个区间的结果取个 就好。(注意进行无解的判断)
区间最大值显然直接使用ST表,解决。
另外, 可能是负数,遇到负数直接把区间往右平移 就可以。
#include<bits/stdc++.h>
using namespace std;
struct pt{
int a;
int b;
}a[100007];
int n;
int chain[200014];//链
int st[200014][18];
int query(int l,int r){//查询,l可以不>r,也可以是负数
if(l>r){
swap(l,r);
}
int len=r-l+1;
if(l<=0){
l+=n;
r+=n;
}
int x=__lg(len);
return max(st[l][x],st[r-(1<<x)+1][x]);//经典公式
}
int bs(int k,int l,int r){//[i+1,i+n/2]的二分
if(l==r){
return r-k;
}
int mid=(l+r)/2;
if(query(k+1,mid)<a[k].b){//查询[k+1,mid]有没有解
return bs(k,mid+1,r);
}else{
return bs(k,l,mid);
}
}
int rev_bs(int k,int l,int r){//[i-n/2,i-1]的二分
if(l==r){
return k-r;
}
int mid=(l+r+1)/2;//不+1有死循环(其他方式避免也可以
if(query(k-1,mid)<a[k].b){
return rev_bs(k,l,mid-1);
}else{
return rev_bs(k,mid,r);
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i].a;
}
for(int i=1;i<=n;i++){
cin>>a[i].b;
}
if(n==1){//n=1要特判
cout<<-1;
return 0;
}
for(int i=1;i<=n;i++){
chain[i]=a[i].a;
}
for(int i=1;i<=n;i++){//再复制一遍,长度变成2n
chain[i+n]=a[i].a;
}
for(int i=1;i<=2*n;i++){
st[i][0]=chain[i];
}
for(int j=1;j<=18;j++){//初始化
for(int i=1;i+(1<<j)-1<=2*n;i++){
st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
for(int i=1;i<=n;i++){
int ans=min((query(i+1,i+n/2)<a[i].b?0x3f3f3f3f:bs(i,i+1,i+n/2)),(query(i-1,i-n/2)<a[i].b?0x3f3f3f3f:rev_bs(i,i-n/2,i-1)));
//重点长难句 包括对区间里有没有解的预先判断,两个解取min等
cout<<(ans==0x3f3f3f3f?-1:ans)<<' ';//无解判断
}
}
0x32 Problem 2
link:P8818 [CSP-S 2022] 策略游戏 进行鞭尸
Solution:我觉得这道题应该不需要Solution了。
#include<bits/stdc++.h>
using namespace std;
int a_max[100005][17];
int a_min[100005][17];
int b_max[100005][17];
int b_min[100005][17];
int a_pos[100005][17];
int a_neg[100005][17];
int a[100005];
int b[100005];
int main(){
int n,m,q;
cin>>n>>m>>q;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
cin>>b[i];
}
for(int i=1;i<=n;i++){
a_max[i][0]=a[i];
a_min[i][0]=a[i];
if(a[i]>=0){
a_pos[i][0]=a[i];
}else{
a_pos[i][0]=0x3f3f3f3f;
}
if(a[i]<=0){
a_neg[i][0]=a[i];
}else{
a_neg[i][0]=-0x3f3f3f3f;
}
}
for(int i=1;i<=n;i++){
b_max[i][0]=b[i];
b_min[i][0]=b[i];
}
for(int j=1;j<=17;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
a_max[i][j]=max(a_max[i][j-1],a_max[i+(1<<(j-1))][j-1]);
a_min[i][j]=min(a_min[i][j-1],a_min[i+(1<<(j-1))][j-1]);
b_max[i][j]=max(b_max[i][j-1],b_max[i+(1<<(j-1))][j-1]);
b_min[i][j]=min(b_min[i][j-1],b_min[i+(1<<(j-1))][j-1]);
a_pos[i][j]=min(a_pos[i][j-1],a_pos[i+(1<<(j-1))][j-1]);
a_neg[i][j]=max(a_neg[i][j-1],a_neg[i+(1<<(j-1))][j-1]);
}
}
#define int long long
for(int i=1;i<=q;i++){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
int amax=max(a_max[l1][__lg(r1-l1+1)],a_max[r1-(1<<(__lg(r1-l1+1)))+1][__lg(r1-l1+1)]);
int amin=min(a_min[l1][__lg(r1-l1+1)],a_min[r1-(1<<(__lg(r1-l1+1)))+1][__lg(r1-l1+1)]);
int bmax=max(b_max[l2][__lg(r2-l2+1)],b_max[r2-(1<<(__lg(r2-l2+1)))+1][__lg(r2-l2+1)]);
int bmin=min(b_min[l2][__lg(r2-l2+1)],b_min[r2-(1<<(__lg(r2-l2+1)))+1][__lg(r2-l2+1)]);
int apos=min(a_pos[l1][__lg(r1-l1+1)],a_pos[r1-(1<<(__lg(r1-l1+1)))+1][__lg(r1-l1+1)]);
int aneg=max(a_neg[l1][__lg(r1-l1+1)],a_neg[r1-(1<<(__lg(r1-l1+1)))+1][__lg(r1-l1+1)]);
if(amin>=0&&bmin>=0){
cout<<amax*bmin<<endl;
continue;
}
if(amin>=0&&bmin<=0){
cout<<amin*bmin<<endl;
continue;
}
if(amax<=0&&bmax<=0){
cout<<amin*bmax<<endl;
continue;
}
if(amax<=0&&bmax>=0){
cout<<amax*bmax<<endl;
continue;
}
if(bmin>=0){
cout<<amax*bmin<<endl;
continue;
}
if(bmax<=0){
cout<<amin*bmax<<endl;
continue;
}
cout<<max(apos*bmin,aneg*bmax)<<endl;
continue;
}
}
0x33 Problem 3
link:P7974 [KSN2021] Delivering Balls 阴间的
不要看他的题面,翻译的很不好。 可以看这个。
Solution:实为阴间题。
我们把题目里面的蓝线叫做山(很形象对不对),起点设为,终点设为。
首先我们先进行一个贪心:
-
绝不会出现掉头(往远离终点的方向走)的情况:显然。
-
能斜着走就不直上直下:看图,黑字是每一段消耗的体力。
- 如图红线和绿线的花费是一样的,所以不如直接找到使得 最大的 ,在 上原地升天 这么多。(不知道这个柿子是什么意思?就是相当于一个“经过 的斜率为1的直线(在图中用黑线表示)在 上的截距”,就是红线和绿线在起点直上直下的部分)。
- 如图,绿线顶个尖尖出来完全没有必要,路径一定是尽可能地“平”。
经过这一番贪心,我们可以得出最终路径的大概模样:
设“使得 最大的 ”为 ,“使得 最大的 ”为 。
第一部分: 到 。(图中为 )
第二部分: 到 。(图中为 )
第三部分: 到 。(图中为 ,不存在)
我们算一下每一部分的体力消耗。
第一部分:显然就是 (四倍高度差)。
第二部分:设 中的最高峰为 。
第二部分可以再分成三小段:上升段,平行段,下降段。
上升段为 (也是四倍高度差)
下降段为 (一倍高度差)
平行段为 (总长度减去上升段和下降段就是平行段)
第三部分: (一倍高度差)
最后相加就是总体力消耗了。
下面,考虑一下 怎么求。
不必多说。ST表,在初始化的时候顺带维护最大值的位置即可。
至于 ,看一下定义: 最大的 。 和 是定值,对于每个山都一样。把这两个东西从柿子里扔掉,我们发现,第 个山获得了 的加成。所以。对于所有 ,我们直接把 ,此时 就是区间 的最大值。ST表维护。
区别不大,改成 即可。
最后,我们就完成了这道题。
还有一些细节:
-
有可能 。
swap
一下然后把四倍高度差一倍高度差对调一下就行。 -
山的高度 ,会爆
int
,开long long
。
阴间代码警告!!
#include<bits/stdc++.h>
#define int long long
using namespace std;
struct ST{
int val;
int pos;//最大值的位置也需要维护
};
int a[200005];
int l_a[200005];
int r_a[200005];
ST st_l[200005][19];
ST st_r[200005][19];
ST st_n[200005][19];
signed main(){
int n,q;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
l_a[i]=a[i]-i;//为了维护lhigh,处理原数据
r_a[i]=a[i]-(n-i+1);//rhigh
}
for(int i=1;i<=n;i++){
st_l[i][0].val=l_a[i];
st_r[i][0].val=r_a[i];
st_n[i][0].val=a[i];
st_l[i][0].pos=i;
st_r[i][0].pos=i;
st_n[i][0].pos=i;
}
for(int j=1;j<=19;j++){//大型初始化现场
for(int i=1;i+(1<<j)-1<=n;i++){
if(st_l[i][j-1].val>st_l[i+(1<<(j-1))][j-1].val){
st_l[i][j].val=st_l[i][j-1].val;
st_l[i][j].pos=st_l[i][j-1].pos;
}else{
st_l[i][j].val=st_l[i+(1<<(j-1))][j-1].val;
st_l[i][j].pos=st_l[i+(1<<(j-1))][j-1].pos;
}
if(st_r[i][j-1].val>st_r[i+(1<<(j-1))][j-1].val){
st_r[i][j].val=st_r[i][j-1].val;
st_r[i][j].pos=st_r[i][j-1].pos;
}else{
st_r[i][j].val=st_r[i+(1<<(j-1))][j-1].val;
st_r[i][j].pos=st_r[i+(1<<(j-1))][j-1].pos;
}
if(st_n[i][j-1].val>st_n[i+(1<<(j-1))][j-1].val){
st_n[i][j].val=st_n[i][j-1].val;
st_n[i][j].pos=st_n[i][j-1].pos;
}else{
st_n[i][j].val=st_n[i+(1<<(j-1))][j-1].val;
st_n[i][j].pos=st_n[i+(1<<(j-1))][j-1].pos;
}
}
}
cin>>q;
for(int i=1;i<=q;i++){
bool rev=false;
int l,r;
cin>>l>>r;
if(l>r){//如果l>r,交换,标记reverse用来调整体力倍率
rev=true;
swap(l,r);
}
ST lhigh=(st_l[l][__lg(r-l+1)].val>st_l[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)].val?
st_l[l][__lg(r-l+1)]:st_l[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)]);
ST rhigh=(st_r[l][__lg(r-l+1)].val>st_r[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)].val?
st_r[l][__lg(r-l+1)]:st_r[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)]);
ST nhigh=(st_n[l][__lg(r-l+1)].val>st_n[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)].val?
st_n[l][__lg(r-l+1)]:st_n[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)]);
//阴间的......
cout<<
(a[lhigh.pos]-a[l])*(rev?1:4)
+ (a[nhigh.pos]-a[lhigh.pos])*(rev?1:4)
+ (rhigh.pos-lhigh.pos-(a[nhigh.pos]-a[lhigh.pos])-(a[nhigh.pos]-a[rhigh.pos]))*2
+ (a[nhigh.pos]-a[rhigh.pos])*(rev?4:1)
+ (a[rhigh.pos]-a[r])*(rev?4:1)
<<endl;
}
}
闲话:其实这道题除了代码毒瘤一点还是很可以的,把题目中“行变换计算体力”改成“列变换计算体力”也能做思路差不多而且会简单很多(不要问我为什么知道 问就是读错题了...)。另外,用线段树做一个带修的版本也很好,甚至可以再加上山的删除(类似链表那种删除,删掉以后位置也被挤掉那种),用线段树应该也可以进行解决(没仔细想,应该没问题),再毒瘤一点可以再塞一个在末尾增加山,好像也能做......总之扩展一下还能玩出很多新花样,然后在毒瘤的路上越走越远[doge]
0x40 额外题单
P2880 [USACO07JAN] Balanced Lineup G:板子。
P7809 [JRKSJ R2] 01 序列:维护的东西很有意思
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】