ST是什么?
ST的全称是Sparse Table。 稀疏表概念用于对一组静态数据进行快速查询(元素不会更改)。它进行预处理,以便有效地回答查询。
为什么要用ST稀疏表?
ST算法可以可以在O(1)的时间复杂度内查询任意区间的结果,而只需要花费O(nlogn)的预处理时间。因此基于ST稀疏表的算法就是在线算法。
离线和在线算法?
在计算机科学中,一个在线算法是指它可以以序列化的方式一个个的处理输入,也就是说在开始时并不需要已经知道所有的输入。相对的,对于一个离线算法,在开始时就需要知道问题的所有输入数据,而且在解决一个问题后就要立即输出结果。例如,选择排序在排序前就需要知道所有待排序元素,然而插入排序就不必。
ST稀疏表可以解决什么问题?
RMQ问题和RGQ问题
RMQ:Range Minimum Query 区间最大最小值问题 RGQ: Range GCD Query 区间最大公约数问题
为什么这两个问题不用线段树解决?
如果需要动态维护,需要添加或者删除,那么就必须用到线段树,线段树的单次查询时间为O(logn),如果查询量大还是很浪费时间。 在不需要动态维护的时候,ST算法的查询速度可以到达O(1)
什么样的问题时候ST算法?
正如标题所说,RMQ问题和RGQ问题适合ST算法,那么这两个问题有什么特性呢,以RMQ问题举例。 求{2, 3, 1, 5, 4}的最小值,可以通过求出a = min{2, 3, 1, 5}, b = min{3, 1, 5, 4}, ans = min{a, b}求出 可以看到,求一个大区间的最小值,可以通过求两个小区间的最小值来求得,前提是这两个小区间的并集是这个大区间,至于有没有交集,对结果没有影响。所以求{2, 3, 1, 5, 4}的和,就不能这么做了。 因此,总结一下:如果一个大区间问题可以由两个小区间(这两个小区间的并集是这个大区间)得到,这两个小区间是否有重叠对大区间的结果没有任何影响。那么这类问题就可以用ST算法来做。
ST算法原理
STST算法是在线算法,在线算法都要预处理,因此,原理分两部分,预处理和查询。 上面加粗的文字其实就是STST算法的核心思想,它讲了两个点: 要把大区间分成小区间**(预处理)** 利用重叠小区间得出大区间的结果**(查询)**
预处理
预处理预处理是核心,正如上面所说的,要把大区间分成小区间,那么这些小区间的长度为多少呢? 答案是2的幂次。 因为2i=2∗2 (i−1),求出一个2的幂次,可以用这个结果继续推出下一个2的幂次,而且如果用2的幂次做区间长度,对于一个长度为n的区间,最多只会有
l
o
g
n
logn
l o g n 种不同长度的小区间,它们的区间长度依次是
2
0
,
2
1
,
⋯
,
2
l
o
g
n
2^0,2^1,⋯,2^{logn}
2 0 , 2 1 , ⋯ , 2 l o g n 。 可以看到,长度为1的区间有5个,长度为2的区间有4个,长度为4的区间有2个。我们的任务求出这5+4+2=11个区间的结果,那么如果长度为n的区间,要求出多少个这样的子区间呢? 从图上可以看出,会有logn层不同长度的子区间,每层的个数取最大值n,那么就有
n
l
o
g
n
nlogn
n l o g n 个子区间是要通过预处理求出来的。 同时,看下图中的箭头,下一层的子区间结果可以借用上一层的结果得出,即
2
i
=
2
i
−
1
+
2
i
−
1
2^i = 2^{i−1}+2^{i−1}
2 i = 2 i − 1 + 2 i − 1 。 其实观察这个预处理的本质,就是区间DP,ST算法就是用区间DP来预处理的,动态规划的思想,只不过这个区间DP是建立在二进制(待会要用到,这里先提出来)上的,即100=10+10, 1000=100+100等等。
查询
查询就是利用重叠小区间得出大区间的结果,那么给出一个大区间如何找到符合规则的两个小区间呢? 比如:对于序列{2,3,1,5,4},你找到了{2,3}和{3,1}这两个小区间是肯定不行的,因为这两个小区间没有完全覆盖大区间。 我们可以换个角度,要是我们先决定长度,两个区间的长度加起来一定要大于原来的长度,为方便起见,这两个区间的长度一样: 我们要找一个区间,这个区间不能覆盖大区间,但是这个区间的两倍要覆盖大区间,确保这个区间是小于大区间的最大区间。(没有完全覆盖我们可以平移) 我们发现长度为4的区间满足这个条件,但是两倍后整个区间超过了元区间,解决办法就是对齐。 在这里我们发现中间会有重叠,但是重叠对结果毫无影响。
ST算法代码实现
预处理
定义:dp[left][i]:左端点为left,且区间长度为2的i次幂的区间结果。 那么这个状态转移方程就是:dp[left][i] = func(dp[left][i-1],dp[left+(1<<(i-1))][i-1]); func是一个二元函数binary_function。可以是max,min或gcd。
查询
查询的原理上面已经说的很清楚了,现在的问题是如何找到那个不大也不小的区间是的这个区间不能覆盖大区间,但是这个区间的两倍要覆盖大区间。 我们把这个区间的长度写成二进制,比如len = 7(1011)
2
_2
2 ,我们每次去二进制中最高以为的1就可以满足这个要求了,(1000)
2
_2
2 <(1011)
2
_2
2 ,且两倍1000等于(1000)
2
_2
2 <<1=(10000)>(1011)
2
_2
2 获取最高位1的位置可以用以下代码:log(len)/log(2)
不过更推荐预处理,这样更快:
void init ( )
{
for ( int i = 1 ; i< MAX_SIZE; ++ i)
Log2[ i] = Log2[ i>> 1 ] + 1 ;
}
查询区间[l,r)的代码如下:
int query ( int l, int r, BF func)
{
return func ( dp[ l] [ Log2[ r- l] - 1 ] , dp[ r- ( 1 << Log2[ r- l] - 1 ) ] [ Log2[ r- l] - 1 ] ) ;
}
整体测试代码:
#include <bits/stdc++.h>
using namespace std;
#define MAX_SIZE (1<<10)
#define TOP (int)(log(MAX_SIZE)/log(2)) + 1
int arr[ MAX_SIZE] ;
int dp[ MAX_SIZE] [ TOP] ;
int Log2[ MAX_SIZE] ;
int n;
int min ( int a, int b)
{
return a< b? a: b;
}
int max ( int a, int b)
{
return a> b? a: b;
}
int gcd ( int a, int b)
{
return b== 0 ? a: gcd ( b, a% b) ;
}
typedef int ( * BF) ( int , int ) ;
void init ( )
{
for ( int i = 1 ; i < MAX_SIZE; ++ i)
Log2[ i] = Log2[ i>> 1 ] + 1 ;
}
void pretreat ( BF func)
{
for ( int left = 0 ; left < n; dp[ left] [ 0 ] = arr[ left] , ++ left) { }
for ( int i = 1 ; i < Log2[ n] ; ++ i)
for ( int left = 0 ; left+ ( 1 << i) <= n; ++ left)
dp[ left] [ i] = func ( dp[ left] [ i- 1 ] , dp[ left+ ( 1 << ( i- 1 ) ) ] [ i- 1 ] ) ;
}
int query ( int l, int r, BF func)
{
return func ( dp[ l] [ Log2[ r- l] - 1 ] , dp[ r- ( 1 << Log2[ r- l] - 1 ) ] [ Log2[ r- l] - 1 ] ) ;
}
int main ( )
{
init ( ) ;
for ( ; EOF != scanf ( "%d" , & n) ; ) {
for ( int i = 0 ; i < n; scanf ( "%d" , & arr[ i++ ] ) ) { }
BF pFunc = min;
pretreat ( pFunc) ;
for ( int l, r; ; ) {
scanf ( "%d%d" , & l, & r) ;
assert ( l< r) ;
assert ( 0 <= l&& l<= n) ;
assert ( 0 <= r&& r<= n) ;
printf ( "[%d,%d):%d\n" , l, r, query ( l, r, pFunc) ) ;
}
}
return 0 ;
}