ST表学习笔记
经过了树状数组的折磨学习,小蒟蒻\(hqk\)又学了一个新的结构——\(ST\)表(其实应该是\(ST\)算法,因为\(T\)本来就有\(table\)的意思,但是由于大家都这么叫,接下来的文章里也沿用这一说法。
\(ST\)表
(\(1\))区间\(RMQ\)问题
区间\(RMQ\)($ Range\space Maximum/minimum\space Query \()问题,顾名思义,就是询问某个区间的最大最小值。这种问题通常有很多种解法,比如线段树、树状数组,还有像笛卡尔树、莫队这样的神仙做法。但是我们今天要探讨的解法是一种比较好理解的算法——\)ST$表
(\(2\))啥是\(ST\)表
\(ST\)表是基于动态规划的一种算法。为啥叫表呢?是因为\(ST\)表在运行过程中是先进行预处理,然后进行查询。这种算法能够实现\(O(nlogn)\)预处理,\(O(1)\)查询。是一种比较高效的算法,但是需要注意的一点是,这种算法的空间复杂度较高,需要一些优化(或者使用其他的算法来代替)才能通过一些毒瘤题目
(\(3\))\(ST\)表的基本思想
其实,\(ST\)表的基本思想就是\(dp\)。
我们用\(a[1···n]\)表示一组数。设\(f[i][j]\)表示从\(a[i]\)加到\(a[i+2^i-1]\)这个范围内的最大值,也就是说\(f[i][j]\)表示以\(a[i]\)为起点连续\(2^i\)个数的最大值。由于元素个数为\(2^j\)个,所以我们可以考虑分治的思想,分而治之,分别求出左半边(\(2^{j-1}\))的最大值,再求右半边的最大值,即\(f[i][j]=max(f[i][j-1],f[i+2^{j-1}][j-1])\)从前往后扫描一下就可以预处理出来。
接下来我们要考虑如何进行查询
每提问一个区间\([l,r]\),一定会存在一个数\(x\),使得\(2^x\leq r-l+1\)。只要求出了这个值,我们就可以用已经与处理完毕的\(f[][]\)来进行回答了。
具体方法是:\(min(f[l][x],f[r-2^x+1][x])\)这个东西可以再\(O(1)\)的时间内求出来
等等,怎么求\(x\)呢?**
其实,求\(x\)的方法也很简单,就是\(log_2^{r-l+1}\),具体是为什么需要读者自己去思考,这里就不再赘述了
但是怎么求\(log_2^{r-l+1}\)呢?
我们可以维护一个\(log[]\),其中\(log[i]\)表示\(log_2^i\)。至于\(log[i]\)的计算,我们可以用下面一个递推式:\(log[i]=log[i/2]+1\);不过如果再懒一点的话可以调用\(cmath\)库里的\(log2\)函数
(\(4\))\(ST\)表的例题
其实就是\(ST\)表的实现
例题(\(1\))
题目背景
这是一道\(ST\)表经典题——静态区间最大值
请注意最大数据时限只有\(0.8s\),数据强度不低,请务必保证你的每次查询复杂度为$ O(1)$
题目描述
给定一个长度为$ N \(的数列,和\) M $次询问,求出每一次询问的区间内数字的最大值。
输入输出格式
输入格式:
第一行包含两个整数$ N, M$,分别表示数列的长度和询问的个数。
第二行包含\(N\)个整数(记为 \(a_i\)),依次表示数列的第$ i$项。
接下来$ M$行,每行包含两个整数 \(l_i, r_i\),表示查询的区间为$ l_i, r_i$
输出格式:
输出包含$ M$行,每行一个整数,依次表示每一次询问的结果。
题解
这就是一道板子题啊!莫慌莫慌,都在代码里了——
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e6+5,logn=20;
int log[N],f[N][logn+5],a[N];
int n,m,x,y;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
log[0]=-1;//一定要注意这个小细节,才能使log[1]=0
for(int i=1;i<=n;++i)
{
f[i][0]=a[i];//将形如[i,i]的都标作a[i],作为dp的边界条件
log[i]=log[i>>1]+1;//对log的处理
}
for(int j=1;j<=logn;++j)//外循环是1~logn
for(int i=1;i+(1<<j)-1<=n;++i)//内循环是一直到出界
f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1]);//套进刚才的式子中
while(m--)//循环读入数据
{
scanf("%d%d",&x,&y);
int s=log[y-x+1];
printf("%d\n",max(f[x][s],f[y-(1<<s)+1][s]));//这些式子我们都进行过说明,这里就不说了
}
return 0;
}
例题(\(2\))
题目背景
无
题目描述
为了检测生产流水线上总共\(N\)件产品的质量,我们首先给每一件产品打一个分数\(A_i\)表示其品质,然后统计前M件产品中质量最差的产品的分值\(Q_m = min(A_1, A_2, ... A_m)\),以及第\(2\)至第\(M + 1\)件的\(Q_{m + 1}, Q_{m + 2} ...\) 最后统计第\(N - M + 1\)至第\(N\)件的\(Q_n\)。根据\(Q\)再做进一步评估。
请你尽快求出\(Q\)序列。
输入输出格式
输入格式:
输入共两行。
第一行共两个数\(N、M\),由空格隔开。含义如前述。
第二行共\(N\)个数,表示\(N\)件产品的质量。
输出格式:
输出共\(N - M + 1\)行。
第\(1\)至\(N - M + 1\)行每行一个数,第\(i\)行的数\(Q_{i + M - 1}\)。含义如前述。
题解
其实这道题也算一道模板题,只不过是将询问变成了让你自己循环跑一边,不过要注意的是,一定要将\(m-1\)后再进行循环
代码实现:
#include<cstdio>
#include<iostream>
using namespace std;
const int maxn=2000010;
const int logn=20;
int log[maxn],f[maxn][logn],a[maxn];
int n,m;
void init()
{
scanf("%d%d",&n,&m);
log[0]=-1;
for(int i=1;i<=n;++i)
{
scanf("%d",&a[i]);
f[i][0]=a[i];
log[i]=log[i>>1]+1;
}
for(int j=1;j<=logn;++j)
for(int i=1;i+(1<<j)-1<=n;++i)
f[i][j]=min(f[i][j-1],f[i+(1<<j-1)][j-1]);
}
void work()
{
m-=1;
for(int i=1;i+m<=n;++i)
{
int s=log[m+1];
printf("%d\n",min(f[i][s],f[i+m-(1<<s)+1][s]));
}
}
int main()
{
init();
work();
return 0;
}