浅谈 ST 表
ST 表(Sparse Table,稀疏表)是用于解决可重复贡献问题的数据结构。——OI Wiki
一维 RMQ
例题
P3865 【模板】ST 表 & RMQ 问题
显然,单次查询的时间复杂度应为
ST 表基于倍增思想。我们令
:::info[为什么是
在
在
:::
预处理
初始值方面显然
作为倍增的一种实现,ST 表的思想和倍增可谓是一模一样。我们可以把
查询
对于每个询问
代码实现
请注意,如果你要求 log2(n),因为该函数常数较大,而且有潜在的精度误差风险,所以建议使用 __lg(n)。
:::success[AC Code]
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&st[0][i]);
for(int j=1;j<=__lg(n);j++)
for(int i=1;i+(1<<j)-1<=n;i++)
st[j][i]=max(st[j-1][i],st[j-1][i+(1<<j-1)]);
while(m--)
{
int l,r;
scanf("%d%d",&l,&r);
int k=__lg(r-l+1);
printf("%d\n",max(st[k][l],st[k][r-(1<<k)+1]));
}
return 0;
}
:::
时空复杂度
预处理时间复杂度
空间复杂度
习题
P1890 gcd 区间
因为满足
P1886 【模板】单调队列 / 滑动窗口
你说得对,这道题正解应该是单调队列,但是也没人拦我用 ST 表啊!
二维 RMQ
:::info[关于二维 RMQ 的实现方法]{open}
其实二维 RMQ 有两种实现:一种是分步合并(先算行再合并列),一种是四项合并。理论上是分步合并更好,但是我选择的是四项合并。两者的优缺点显示如下:
| 分步合并 | 四项合并 | |
|---|---|---|
| 比较次数 | 较少。 |
较多。 |
| 代码实现 | 极其简洁。逻辑线性,不需要处理复杂的边界。 | 较为繁琐。必须手动处理边界的情况。 |
| 逻辑直观度 | 略显抽象。先处理宽再处理长。 | 非常直观。四个小方块拼成大方块,符合几何直觉。 |
| 鲁棒性 | 高。不容易出现数组越界或逻辑遗漏。 | 中。初始化边界条件时极易出错。 |
:::
例题(正方形)
P2216 [HAOI2007] 理想的正方形
令
预处理
我们可以分别把长和宽拆成
查询
假设要求左上角
代码实现
:::success[AC Code]
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int a,b,n;
int stmax[15][N][N],stmin[15][N][N];
int ans=INT_MAX;
int main()
{
scanf("%d%d%d",&a,&b,&n);
for(int i=1;i<=a;i++)
for(int j=1;j<=b;j++)
{
int x;
scanf("%d",&x);
stmax[0][i][j]=stmin[0][i][j]=x;
}
for(int k=1;k<=__lg(min(a,b));k++)
for(int i=1;i+(1<<k)-1<=a;i++)
for(int j=1;j+(1<<k)-1<=b;j++)
{
stmax[k][i][j]=max({stmax[k-1][i][j],stmax[k-1][i][j+(1<<k-1)],stmax[k-1][i+(1<<k-1)][j],stmax[k-1][i+(1<<k-1)][j+(1<<k-1)]});
stmin[k][i][j]=min({stmin[k-1][i][j],stmin[k-1][i][j+(1<<k-1)],stmin[k-1][i+(1<<k-1)][j],stmin[k-1][i+(1<<k-1)][j+(1<<k-1)]});
}
for(int i=1;i+n-1<=a;i++)
for(int j=1;j+n-1<=b;j++)
{
int k=__lg(n);
int maxn=max({stmax[k][i][j],stmax[k][i][j+n-(1<<k)],stmax[k][i+n-(1<<k)][j],stmax[k][i+n-(1<<k)][j+n-(1<<k)]});
int minn=min({stmin[k][i][j],stmin[k][i][j+n-(1<<k)],stmin[k][i+n-(1<<k)][j],stmin[k][i+n-(1<<k)][j+n-(1<<k)]});
ans=min(ans,maxn-minn);
}
printf("%d\n",ans);
return 0;
}
:::
不得不说,虽然 ST 表相较于正解单调队列多了一个
其实这段代码是可以用滚动数组优化的(即把第一维舍掉),读者可以自行了解。其实也就是空间复杂度少了个
习题(正方形)
P2701 [USACO5.3] 巨大的牛棚 Big Barn
发现边长满足单调性,因此考虑二分答案结合 ST 表。
时间复杂度较正解 DP 多了一个
:::success[AC Code]
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m;
bool st[15][N][N];
bool check(int mid)
{
for(int i=1;i+mid-1<=n;i++)
for(int j=1;j+mid-1<=n;j++)
{
int k=__lg(mid);
bool flag=st[k][i][j]||st[k][i+mid-(1<<k)][j]||st[k][i][j+mid-(1<<k)]||st[k][i+mid-(1<<k)][j+mid-(1<<k)];
if(!flag)
return true;
}
return false;
}
int main()
{
scanf("%d%d",&n,&m);
while(m--)
{
int x,y;
scanf("%d%d",&x,&y);
st[0][x][y]=true;
}
for(int k=1;k<=10;k++)
for(int i=1;i+(1<<k)-1<=n;i++)
for(int j=1;j+(1<<k)-1<=n;j++)
{
st[k][i][j]|=st[k-1][i][j];
st[k][i][j]|=st[k-1][i+(1<<k-1)][j];
st[k][i][j]|=st[k-1][i][j+(1<<k-1)];
st[k][i][j]|=st[k-1][i+(1<<k-1)][j+(1<<k-1)];
}
int l=1,r=n,mid;
while(l<r)
{
mid=l+r+1>>1;
if(check(mid))
l=mid;
else
r=mid-1;
}
printf("%d\n",l);
return 0;
}
:::
例题(长方形)
Check Corners
令
预处理
把长拆成
还有注意处理边界情况:
查询
假设要求左上角
代码实现
注意 HDU 非常坑。多组数据,不能用 __lg(),也不能用万能头。
:::success[AC Code]
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=305;
int n,m,q;
int lg[N];
int st[10][10][N][N];
int main()
{
lg[1]=0;
for(int i=2;i<=300;i++)
lg[i]=lg[i>>1]+1;
while(~scanf("%d%d",&n,&m))
{
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&st[0][0][i][j]);
//处理1*2^kj
for(int kj=1;kj<=lg[m];kj++)
for(int i=1;i<=n;i++)
for(int j=1;j+(1<<kj)-1<=m;j++)
st[0][kj][i][j]=max(st[0][kj-1][i][j],st[0][kj-1][i][j+(1<<kj-1)]);
//处理2^ki*1
for(int ki=1;ki<=lg[n];ki++)
for(int i=1;i+(1<<ki)-1<=n;i++)
for(int j=1;j<=m;j++)
st[ki][0][i][j]=max(st[ki-1][0][i][j],st[ki-1][0][i+(1<<ki-1)][j]);
//处理正常情况
for(int ki=1;ki<=lg[n];ki++)
for(int kj=1;kj<=lg[m];kj++)
for(int i=1;i+(1<<ki)-1<=n;i++)
for(int j=1;j+(1<<kj)-1<=m;j++)
st[ki][kj][i][j]=max({st[ki-1][kj-1][i][j],st[ki-1][kj-1][i+(1<<ki-1)][j],st[ki-1][kj-1][i][j+(1<<kj-1)],st[ki-1][kj-1][i+(1<<ki-1)][j+(1<<kj-1)]});
scanf("%d",&q);
while(q--)
{
int x1,y1,x2,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
int ki=lg[x2-x1+1],kj=lg[y2-y1+1];
int maxn=max({st[ki][kj][x1][y1],st[ki][kj][x2-(1<<ki)+1][y1],st[ki][kj][x1][y2-(1<<kj)+1],st[ki][kj][x2-(1<<ki)+1][y2-(1<<kj)+1]});
printf("%d %s\n",maxn,max({st[0][0][x1][y1],st[0][0][x1][y2],st[0][0][x2][y1],st[0][0][x2][y2]})==maxn?"yes":"no");
}
}
return 0;
}
:::
时空复杂度
正方形:预处理时间复杂度
长方形:预处理时间复杂度
局限性
ST 表的优点是好写而且单次
顺便安利一下自己的树状数组。