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; }