其实这个方法我去年11月就发现了,但当时觉得比赛已经结束,发布也没有意义了,就权当私下研究。不过转眼间几个月就去了,想想在这个圈子呆了一些时候,也得到了他人的帮助,想做些有意思的事情回赠,觉得这个还不错,所以就翻翻账。
实际上,想想Sparse Table(简称ST)算法和线段树算法(Segment Tree),就会发现,问题的解决之道就是对区间采取长短不同的覆盖策略,然后统计、合并子区间的结果。那么,Binary Index Tree(简称BIT,点击下载参考文献1)策略其实也是一种不错的区间划分方法,为什么不利用呢?
先约定下后续的符号。T是记录数据的数组,C是BIT的索引数组,用于存储与计算的子区间结果。现在的问题是求Range Minimum Query。
C[k]表示BIT中的第k个节点,它的值是[k – low(k) + 1, k]这个区间中所有元素的最小值,T[k]表示数组的第k个元素,low(k) = k & -k。那么k为奇数时就有C[k] = T[k];k为偶数时,C[k]可以再被细分为lg(low(k)) 个子区间和T[k]元素本身。下面的代码就可以求得C数组的值:
[code:cpp]
void preprocess()
{
int i;
int y, k;
for ( i = 1; i <= n; ++i ) {
y = LOW(i);
C[i] = T[i];
for ( k = 1; k < y; k <<= 1 ) C[i] <?= C[i-k];
}
}
[/code]
代码非常简洁,同时很容易证明该预处理的时间复杂度是O(n),常数是2,并且没有过多的操作,所以速度非常快。
查询也很简单。对一个查询区间[A, B],如果B–low(B) < A,我们就访问T[B]的值,然后转移到B所管辖的子区间中去,即令B = B–1;否则访问C[B],令B = B-low(B),如此反复,直到B < A为止,代码如下:
[code:cpp]
int query( int A, int B )
{
int answer = 0x7fffffff;
A = A – 1;
while ( 1 ) {
for (; B – low(B) >= A; B -= low(B) ) answer <?= C[B];
if ( B == A ) break;
anwser <?= T[B];
B = B - 1;
}
return answer;
}
[/code]
代码很简洁,但是这个操作的复杂度却比较高,最坏情况为O(lg^2(n)),系数也是2。
这个算法有个常数级优化,观察到一个区间k的后半段一般都是零散的小区间,所以如果A < B – low(B) / 2,那么,实际上我们花了很多时间去访问后面的那些小区间,所以预处理时额外用个数组作记录,这样可以加快操作。代价是多花费了空间,以及代码变得不是很清晰。
总结一下。这个算法的时间是O(n) – O(lg^2(n)),空间是O(n),而且时空上的常数都是2,代码也一如BIT其它操作一样简练,所以在实际中使用比起ST(内存量太高)和Segment Tree(编程较复杂,空间也比较多)在某些情况下是有优势的(部分测试数据见下表),大家不妨尝试一下。
本文算法和ST算法性能对比
硬件环境:
CPU: Centrino Mobile 1.6G
Memory: 512MB
Hard Drive: 5400RPM
符号:
n = 数组中的数据量
m = 查询数量
BIT = 本文算法
ST = Sparse Table算法
Time = 耗时
Memory = 内存用量
结果:
n = 100000, m = 10000
BIT: Time = 77.6ms, Memory = 1890.4KB
ST: Time = 96ms, Memory = 7812KB
n = 100000, m = 10000000
BIT: Time = 20589ms, Memory = 1892.8KB
ST: Time = 11852.8ms, Memory = 7812.8KB
n = 1000000, m = 100000
BIT: Time = 1003.2ms, Memory = 8920KB
ST: Time = 1148.8ms, Memory = 79308KB
n = 1000000, m = 1000000
BIT: Time = 4915.2ms, Memory = 8932KB
ST: Time = 2415.2ms, Memory = 79308KB
n = 3000000, m = 1000000
BIT: Time = 6733.6ms, Memory = 24552KB
ST: Time = 4376.8ms, Memory = 254296KB