1.一维ST表
RMQ(Range Maximum/Minimum Query)问题,即区间最值查询问题,是指对于长度为 n n n 的数列 a [ ] a[\ ] a[ ],查询数列 a [ ] a[\ ] a[ ] 中下标在 [ i , j ] [i, j] [i,j] 里的最大值或最小值。
一般解决这类区间问题可以用线段树来做,比较通用,但是线段树的代码量有点多。
这里介绍ST(Sparse Table)算法。
ST算法是一个非常有名的在线处理RMQ问题的算法,它在 O ( n log n ) O(n \log n) O(nlogn) 时间内进行预处理,然后可以在 O ( 1 ) O(1) O(1) 时间内回答每个查询,其本质是倍增与动态规划的思想。
要注意的是,ST算法只适用于静态区间求最值。如果是动态的,那还是乖乖使用线段树吧!
1.1 预处理
ST算法是基于倍增思想设计的在线算法,之所以称之为倍增,是因为算法记录从每个元素开始的连续长度为 2 k 2^k 2k 的区间中元素的最值。
类似于动态规划,规定 f [ i , j ] f[i,j] f[i,j] 表示从第 i i i 个元素起连续 2 j 2^j 2j 个数中的最大值,即记录区间 [ i , i + 2 j − 1 ] [i,\ i+2^j-1] [i, i+2j−1] 中的最大值。
由定义可知, f [ i , j ] f[i,j] f[i,j] 一定包含偶数个数字,故把 f [ i , j ] f[i,j] f[i,j] 平均分成两段,从 i i i 到 i + 2 j − 1 − 1 i+2^{j-1}-1 i+2j−1−1 为一段, i + 2 j − 1 i+2^{j-1} i+2j−1 到 i + 2 j − 1 i+2^j-1 i+2j−1 为另一段,长度均为 2 j − 1 2^{j-1} 2j−1。
于是得到状态转移方程为: f [ i , j ] = m a x { f [ i , j − 1 ] , f [ i + 2 j − 1 , j − 1 ] } f[i, j] = max\left\{ f[i,\ j-1], \ \ \ f[i+2^{j-1},\ j-1] \right\} f[i,j]=max{f[i, j−1], f[i+2j−1, j−1]},其中 f [ i , 0 ] = a [ i ] f[i,0] = a[i] f[i,0]=a[i]。
void init()
{
// 数组下标从1开始
for (int i = 1; i <= n; i++)
{
f[i][0] = a[i];
}
for (int j = 1; (1 << j) <= n; j++)
{
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]);
}
}
}
1.2 查询
对于一个查询区间 [ l , r ] [l,r] [l,r],只要找到一个或者多个 2 2 2 的整数倍长度的区间覆盖 [ l , r ] [l,r] [l,r],然后取这些区间最大值的最大值就是答案了。
如何把 [ l , r ] [l,r] [l,r] 覆盖完整?
-
一种办法是把区间的长度按照二进制分成多个 2 2 2 的整数倍区间,显然这些区间是不重叠的,这样求多次最大值就能得到答案。不过这种分割增加了算法常数,一次查询可能就要求几十次最大值。
-
还有种更好的方法,原理是:为了减少分割出的区间数量,允许区间重叠,这样所有的情况下最多只要两个区间就好了,见下图所示。
由此可见,这个算法的高明之处不是在于动态规划的建立,而是它的查询,它的查询效率是 O ( 1 ) O(1) O(1)。
假设我们要求区间 [ l , r ] [l,r] [l,r] 中序列 a [ ] a[\ ] a[ ] 的最大值,找到一个数 k k k 使得 2 k < r − l + 1 2^k < r - l + 1 2k<r−l+1,即 k = ⌊ log 2 ( r − l + 1 ) ⌋ k = \lfloor \log_2(r-l+1) \rfloor k=⌊log2(r−l+1)⌋,由换底公式得 k = ⌊ ln ( r − l + 1 ) ln ( 2 ) ⌋ k = \lfloor \frac{\ln(r-l+1)}{\ln(2)} \rfloor k=⌊ln(2)ln(r−l+1)⌋。
这样,可以把这个区间分成两个部分: [ l , l + 2 k − 1 ] [l, l+2^k-1] [l,l+2k−1] 和 [ r − 2 k + 1 , r ] [r-2^k+1, r] [r−2k+1,r]。
我们发现,这两个区间恰好是刚刚已经初始化好的,前者对应的是 f [ l , k ] f[l, k] f[l,k],后者对应的是 f [ r − 2 k + 1 , k ] f[r-2^k+1, k] f[r−2k+1,k]。
这样,只要看这两个区间的最大值,就可以知道整个区间的最大值。
int query(int l, int r)
{
int k = log(r - l + 1) / log(2);
return max(f[l][k], f[r - (1 << k) + 1][k]);
}
2.二维ST表
一维RMQ问题是求一个数列 a [ ] a[\ ] a[ ] 中某个区间的最值,而二维RMQ问题是求一个 n × m n \times m n×m 的矩阵中某个子矩阵的最值。
2.1 初始化
设 f [ i ] [ j ] [ i i ] [ j j ] f[i][j][ii][jj] f[i][j][ii][jj] 表示以 ( i , j ) (i, j) (i,j) 为左上角、以 ( i + 2 i i − 1 , j + 2 j j − 1 ) (i+2^{ii}-1, j+2^{jj}-1) (i+2ii−1,j+2jj−1) 为右下角的矩阵内的最大值。
易知: f [ i ] [ j ] [ 0 ] [ 0 ] = a [ i ] [ j ] f[i][j][0][0] = a[i][j] f[i][j][0][0]=a[i][j]。
假设 f [ i ] [ j ] [ i i ] [ j j ] f[i][j][ii][jj] f[i][j][ii][jj] 中的 i i ii ii 不为 0 0 0,那么 f [ i ] [ j ] [ i i ] [ j j ] = m a x ( f [ i ] [ j ] [ i i − 1 ] [ j j ] , f [ i + 2 i i − 1 ] [ j ] [ i i − 1 ] [ j j ] ) f[i][j][ii][jj] = max(f[i][j][ii-1][jj], f[i+2^{ii-1}][j][ii-1][jj]) f[i][j][ii][jj]=max(f[i][j][ii−1][jj],f[i+2ii−1][j][ii−1][jj])
如果 i i ii ii 为 0,那么就按 j j jj jj 来求,即 f [ i ] [ j ] [ i i ] [ j j ] = m a x ( f [ i ] [ j ] [ i i ] [ j j − 1 ] , f [ i ] [ j + 2 j j − 1 ] [ i i ] [ j j − 1 ] ) f[i][j][ii][jj] = max(f[i][j][ii][jj-1] , f[i][j+2^{jj-1}][ii][jj-1]) f[i][j][ii][jj]=max(f[i][j][ii][jj−1],f[i][j+2jj−1][ii][jj−1])
简单来说,就是将二维问题转变为一维问题求解。
void init()
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
f[i][j][0][0] = a[i][j];
}
}
for (int ii = 0; (1 << ii) <= n; ii++)
{
for (int jj = 0; (1 << jj) <= m; jj++)
{
if (ii == 0 && jj == 0) continue;
for (int i = 1; i + (1 << ii) - 1 <= n; i++)
{
for (int j = 1; j + (1 << jj) - 1 <= m; j++)
{
if (ii)
{
f[i][j][ii][jj] = max(f[i][j][ii - 1][jj], f[i + (1 << ii - 1)][j][ii - 1][jj]);
}
else
{
f[i][j][ii][jj] = max(f[i][j][ii][jj - 1], f[i][j + (1 << jj - 1)][ii][jj - 1]);
}
}
}
}
}
}
2.2 查询
假设我们要求以 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 为左上角、以 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 为右下角的矩形区域的的最大值。
-
令 k 1 = ⌊ log 2 ( x 2 − x 1 + 1 ) ⌋ k_1 = \lfloor \log_2(x_2 - x_1 + 1) \rfloor k1=⌊log2(x2−x1+1)⌋,由换底公式得 k 1 = ⌊ ln ( x 2 − x 1 + 1 ) ln ( 2 ) ⌋ k_1 = \lfloor \frac{\ln(x_2-x_1+1)}{\ln(2)} \rfloor k1=⌊ln(2)ln(x2−x1+1)⌋
-
令 k 2 = ⌊ log 2 ( y 2 − y 1 + 1 ) ⌋ k_2 = \lfloor \log_2(y_2 - y_1 + 1) \rfloor k2=⌊log2(y2−y1+1)⌋,由换底公式得 k 2 = ⌊ ln ( y 2 − y 1 + 1 ) ln ( 2 ) ⌋ k_2 = \lfloor \frac{\ln(y_2 - y_1 + 1)}{\ln(2)} \rfloor k2=⌊ln(2)ln(y2−y1+1)⌋
将目标矩形分成四小块,这四小块可能有重合部分,但它们共同构成了目标矩形:
-
f [ x 1 ] [ y 1 ] [ k 1 ] [ k 2 ] f[x_1][y_1][k_1][k_2] f[x1][y1][k1][k2]
-
f [ x 1 ] [ y 2 − 2 k 2 + 1 ] [ k 1 ] [ k 2 ] f[x_1][y_2-2^{k_2}+1][k_1][k_2] f[x1][y2−2k2+1][k1][k2]
-
f [ x 2 − 2 k 1 + 1 ] [ y 1 ] [ k 1 ] [ k 2 ] f[x_2-2^{k_1}+1][y_1][k_1][k_2] f[x2−2k1+1][y1][k1][k2]
-
f [ x 2 − 2 k 1 + 1 ] [ y 2 − 2 k 2 + 1 ] [ k 1 ] [ k 2 ] f[x_2-2^{k_1}+1][y_2-2^{k_2}+1][k_1][k_2] f[x2−2k1+1][y2−2k2+1][k1][k2]
那么,求解这四块的最大值即为最终结果。
int query(int x1, int y1, int x2, int y2)
{
int k1 = log(x2 - x1 + 1) / log(2);
int k2 = log(y2 - y1 + 1) / log(2);
int tmp1 = max(f[x1][y1][k1][k2], f[x1][y2 - (1 << k2) + 1][k1][k2]);
int tmp2 = max(f[x2 - (1 << k1) + 1][y1][k1][k2], f[x2 - (1 << k1) + 1][y2 - (1 << k2) + 1][k1][k2]);
return max(tmp1, tmp2);
}