转载出处:http://www.cnblogs.com/ambition/archive/2011/04/06/bit_rmq.html
树状数组(Binary Index Tree)利用二进制的一些性质巧妙的划分区间,是一种编程,时间和空间上都十分理想的求区间和的算法,同样我们可以利用树状数组优美的区间划分方法来求一个序列的最值
约定以 num[] 表示原数组, 以 idx[] 表示索引数组, Lowbit(x)=x&(-x)
树状数组求和时通过构造数组 idx[] 使 idx[k]=sum(num[tk]), tk [k-Lowbit(k)+1,k], 使用同样的方法构造最值索引数组:
以最大值为例, 先讨论询问过程中不对数组做任何修改的情况, 用 idx[k] 记录 [k-Lowbit(k)+1,k] 区间内的最大值, 可以仿照求和时的方法得到:
1
2
3
4
5
6
7
|
void Init( int n){ for ( int i=1;i<=n;i++){ for ( int j=i;j<=n;j+=Lowbit(j)){ idx[j]=MAX(idx[j],num[i]); } } } |
这种方法在每次调用该函数前都必须对数组进行初始化, 这样对于数据范围比较大的时候不是很优美, 这样我们可以改为:
1
2
3
4
5
6
7
8
|
void Init( int n){ for ( int i=1;i<=n;i++){ idx[i]=num[i]; for ( int j=1;j<Lowbit(i);j<<=1){ idx[i]=MAX(idx[i],idx[i-j]); } } } |
这样, 在更新到第k个数时, 所有 t(t<k) 都已经是正确的值了, 不存在上面那个函数的情况了
然后再来看查询的问题, 对于区间 [l,r] 把该区间转化为多个的小区间再进行求最值, 方法是从后往前对每一个索引数的范围进行判断, 如在进行到第k项时,该数控制的范围是 [k-Lowbit(k)+1,k], 如果k-Lowbit(k)+1在所求的范围内的话则将该区间的最值加入最值的判断,然后转至地k-Lowbit(k),否则的话就只对第k个数进行最值判断,然后转至k-1,具体实现如下:
1
2
3
4
5
6
7
8
9
10
11
|
int Query( int l, int r){ int ans=num[r]; while ( true ){ ans=MAX(ans,num[r]); if (r==l) break ; for (r-=1;r-l>=Lowbit(r);r-=Lowbit(r)){ ans=MAX(ans,idx[r]); } } return ans; } |
该查询的复杂度为log(n)
st算法的复杂度 O(nlog(n)) / O(1) , 线段树为 O(nlog(n)) / (log(n)),树状数组 O(<nlog(n)) / O(log(n))
空间复杂度 st 为 O(nlog(n)), 线段树 O(n),常数较大 , 树状数组是 O(n)
编程上 st 和 树状数组 都比较容易实现,线段树代码较长
另外线段树灵活性较大
PKU 3264 题:
st Memory: 6372K Time: 1250MS 964B
BIT Memory: 716K Time: 1282MS 933B
SegTree 未测,要比st更大
然后我们可以进一步扩展到边查询边修改的情况
每次直接去更新父亲节点自然是不行的, 为了维护索引数组的正确性,我们在对每个父亲节点进行更新时都要查询他的所有儿子节点,在其中取最优值, 得到代码如下:
1
2
3
4
5
6
7
8
9
|
void Modify( int p, int v, int n){ num[p]=v; for ( int i=p;i<=n;i+=Lowbit(i)){ idx[i]=v; for ( int j=1;j<Lowbit(i);j<<=1){ idx[i]=MAX(idx[i],idx[i-j]); } } } |
复杂度为 O(log^2(n)), HDU 1754 I hate it 437MS 1776K
另外,该方法还有一个减枝, 很容易想到,在求最大值时,当某个值更新的值大于原值的时候是没有必要再去查询儿子节点的,所以内部循环可加一个判断来判定是否需要扫描儿子节点,可能是数据问题,该题时间并没有大的变化