RMQ问题之ST算法

RMQ(Range Minimum/Maximum Query)问题,即区间最值问题。也就是给出一个数列A1到An,同时给出若干次查询,query(l,r),要求我们快速准确的给出该区间内数列的最值。

显然,对于这种在线查询的问题,暴力情况下,n次查询,每次都要比较所给区间的元素以求出最值,复杂度是相当高的。为了优化解法,一般采用线段树区间查询或者ST算法来解决。最初接触RMQ之ST算法,是学完线段树后,紧接着后面一节就是RMQ之ST算法,开始还以为就是线段树的经典应用呢。一方面是因为区间最值问题的确可以用线段树来解决,另一方面,线段树的英文是Segment Tree。正好缩写就是ST。后来才发现,只是巧合而已。RMQ之ST算法,这里的ST是指Sparse Table,即稀疏表。一般对ST算法的描述就是:采用倍增的思想,动态规划的策略,一方面在O(nlogn)的时间内对数列进行预处理,将区间最值制表备查,另一方面可以在O(1)的时间内应对每次查询,快速给出区间最值。

首先分析下暴力做法为什么浪费时间,比如第一次查询编号为2-9中数列的最大值,第二次查询编号为3 - 12的最大值,这其中的3-9这段区域的最值便重复的计算了。很容易想到的一种策略是,某区间最大值等于该区间左边元素最大值和右边区间最大值的较大者。退化情况是,区间长度为1,则元素本身就是最值。然后区间长度为2的最值等于两个区间长度为1的最值较大者,区间长度为4的最值等于两个区间长度为2的最值较大者,以此类推。注意,为什么区间长度不是线性递增,而是倍增呢?因为线性递增太过缓慢,所有状态都存下来大可不必,我们只需要记录下区间长度为2的整数次幂的区间即可一次比较求出任意区间的最值,比如我们想知道区间2-6上的最值只需要比较下区间2 - 5上的最值和区间3-6上的最值即可。所以ST算法的第一个思想就是倍增策略提高速度。

按照上面的思路,先求出区间长度为1的区间的最值,再依次求出区间长度为2,4,8,16....的最值就是我们的目标。仔细思考,这正是DP中状态空间转移的的过程。

观察上图,我们在求解规模为2^j的问题时需要求解两个规模是原问题一半的子问题,即分而治之。设 f ( i , j ) 表示从i开始,长度为 2^j 的一段区间中的最大值。很容易得到状态转移方程:f ( i , j ) = max { f ( i , j-1 )  , f ( i+(1<<j-1) , j - 1 )  }。

下面正式介绍ST算法的具体实现。

初始化部分:构建稀疏表。

dp边界毋庸置疑是区间长度为1的情况,也就是f[i][0] = a[i]。(f[i][j]中2^j表示区间长度) 。下面就是如何构建循环来实现状态空间从边界扩散到整个状态空间。我们需要一步步实行区间长度为1,2,4...的状态转移,所以最外层循环必然是j,控制区间长度的,内层循环就是i,枚举区间起点的。

整个状态扩散的过程是从f[1][0],...f[n][0]扩散到f[1][1],...f[n-1][1],再到f[1][2],...f[n-1<< 2 + 1][2]...一直到最后的f[n - (1<<t) + 1][t]。

void ST_prework(){
	for(int i = 1;i <= n;i++)	f[i][0] = a[i];
	int t = log(n) / log(2) + 1;
	for(int j = 1;j < t;j++)
		for(int i = 1;i <= n - (1<<j) + 1;i++)
			f[i][j] = max(f[i][j - 1],f[i + (1<<(j - 1))][j - 1]);
}

解释下上面的代码, t = log(n) / log(2) + 1;由换底公式可知lgn / lg2 = log2 n。然后加上1是为了在循环时取下整,比如n = 7,t = 3,到最后一层状态转移得到的是f[1][2],f[2][2],f[3][2],f[4][2],准确说,我们的j要倍增到log2n取下整为止,i则要遍历到n - (1<<j) + 1为止确保覆盖到最后的元素且不会越界。

在线查询部分:

我们不能确保每次查询的区间长度都是2的整数次幂,但是可以通过比较长度为2的整数次幂的两个区间最值来求得所求区间的最值。这里采用k = log(r - l + 1) / log(2)来对log2n取下整。可以发现1 << k的值必然是大于区间长度的一半的,极端情况下,r - l + 1是2的整数次幂,max(f[l][k],f[r - (1 << k) + 1][k]也就是正好求区间长度为2的整数次幂的最值。一般情况下,比如l = 1,r = 7,区间长度为7,k = 2,我们比较下标为1 2 3 4和 4 5 6 7的最值即可求出该区间的最值。区间可以有重叠,但绝不能有遗漏。

int ST_query(int l,int r){
	int k = log(r - l + 1) / log(2);
	return max(f[l][k],f[r - (1 << k) + 1][k]);
}

可以发现,ST算法在解决RMQ问题时有着可以媲美线段树的效率,并且代码极为简洁的优点,如果简洁的特性不够鲜明,那试着写个线段树来解决RMQ问题,之后会感叹,还是ST算法简洁。

猜你喜欢

转载自blog.csdn.net/qq_30277239/article/details/88908090