ST算法(Sparse Table,又称稀疏表、ST表)是用来解决RMQ问题的一种算法。
RMQ(Range Minimum/Maximum Query)问题是询问区间最值的一种问题。
ST表的实质是利用倍增思想的动态规划。相较于朴素的暴力\(O(n)\)做法,ST表可以做到\(O(n\log n)\)时间的预处理,\(O(1)\)时间的查询,是解决这类问题最快的算法,但不支持在线修改。
ST表利用一个二维数组记录答案(典型的以空间换时间),在这个数组中\(st[i][j]\)表示从第\(i\)个数开始连续\(2^j\)个数中的最值(包括第\(i\)个数),因此查询的区间范围为\([i,i+2^j-1]\),并且初始条件为\(st[i][0]=\)第\(i\)个数。在构造的过程中,我们有如下转移方程(以最大值为例)
\[st[i][j]=\max(st[i][j-1],st[i+2^{j-1}][j-1]) \]需特别注意构造时的细节。第一,第\(i+2^j-1\)个数(右边界)不能出界(即不超过最后一个数的序号);第二,两层循环的外层应是\(j\),这可以感性地理解一下,因为每次构造ST表时都是先让区间长度固定将序列全部遍历完后再修改区间长度,而这里的区间长度就对应循环里的\(j\)。
构造说完后,再说查询。设读入区间范围为\([l,r]\),那么它的长度为\(r-l+1\),由于有可能区间的长度不能严格等于\(2\)的幂次,所以我们会把它分成两段分别查询然后求最值,这并不影响结果。具体做法是,先求出\(k=\left\lfloor {{{\log }_2}(r - l + 1)} \right\rfloor\) ,然后将它分成\([l,l+2^k-1]\)与\([r-2^k+1,r]\)两段,即在查询时分别查询\(st[l][k]\)和\(st[r-2^k+1][k]\),注意到这两段有重合的地方,且长度均为\(2^k-1\)。至于为什么这么分是正确的,是基于一个公式
\[2^{\lfloor{\log_2 N}\rfloor}>\frac{N}{2}\]因此我们取对数总能保证两段将整个区间完全覆盖。
最后,还有一点小插曲值得一提,在我看到的求对数的写法中,基本上都是int k = (int)(log((double)(r-l+1))/log(2.0))
,由于数学库中没有提供以任意底求对数的函数,于是就用了换底公式进行了转换(让我对换底公式有了深刻的记忆2333),换底公式为
\[\log_aN=\frac{\log_bN}{\log_ba}\]但由于这里要求\(2\)为底的对数,所以不用这么麻烦,直接用库里提供的log2()
就好啦!
说完了这些,刚好附一道裸的模板题的代码,就当板子用啦qwq
(题目:[P2880]Balanced Lineup)
#include <iostream>
#include <cmath>
#define MAX(a,b) a>b?a:b
#define MIN(a,b) a<b?a:b
using namespace std;
int n,q,STmin[50005][20],STmax[50005][20];
void RMQbyST()
{
int k=log2(n);
for(int j=1;j<=k;j++)
for(int i=1;i+(1<<j)-1<=n;i++) //构造时需特别注意
STmax[i][j]=MAX(STmax[i][j-1],STmax[i+(1<<(j-1))][j-1]),
STmin[i][j]=MIN(STmin[i][j-1],STmin[i+(1<<(j-1))][j-1]);
}
int main()
{
ios::sync_with_stdio(false);
cin >> n >> q;
for(int i=1;i<=n;i++)
{
cin >> STmin[i][0];
STmax[i][0]=STmin[i][0]; //初始条件不能忘!
}
RMQbyST();
while(q--)
{
int l,r;
cin >> l >> r;
int k=log2(r-l+1);
int Max=MAX(STmax[l][k],STmax[r-(1<<k)+1][k]),
Min=MIN(STmin[l][k],STmin[r-(1<<k)+1][k]);
cout << Max-Min << endl;
}
return 0;
}