用树状数组解决区间查询问题

转自http://blog.csdn.net/zsc09_leaf/article/details/6530345


转至ROBA大神博客:http://roba.rushcj.com/?p=510

本文扩写自郭神的《树状数组新应用》,在此表示膜拜。

树状数组的学名貌似叫做Binary Index Tree,关于它的基本应用可参考Topcoder上的这篇Tutorial.

树状数组可以看作一个受限制的线段树,它维护一个数组,最经典的树状数组支持的基本操作有两个:(1)改变某一个元素的值 (2)查询某一个区间内所有元素的和。在此基础上,经过简单的变形可以变成支持另一组操作:(1)把一个区间内所有元素都加上一个值 (2)查询某一个元素的值。这两个都是已经泛滥了的东西了,在此不赘述。

简单的树状数组模型是不支持这样一组操作的:(1)把某一个区间内所有元素都加上一个值 (2)查询某一个区间内所有元素的和。当然,这个东西可以用线段树完成,但是线段树占内存比较大,写起来也比较繁(对我这种不会数据结构的人而言)。下面我们用一个改进版的树状数组完成这个任务。

首先一个观察是区间操作总可以变成从最左端开始,比如把区间[3..6]都加10,可以变成[1..6]加10, [1..2]减10。查询也类似。于是下面只关心从最左端开始的情况。定义Insert(p, d)表示把区间[1..p]都加d,Query(p)表示查询区间[1..p]之和。

我们考虑调用一次Insert(p, d)对以后的某次查询Query(q)的影响:

(1) 如果p<=q,总的结果会加上p*d (2) 如果p>q,总的结果会加上q*d

也就是说,Query(q)的结果来源可分为两部分,一部分是Insert(p1,d) (p1<=q),一部分是Insert(p2,d) (p2 > q)。我们用两个数组B[], C[]分别维护这两部分信息,B[i]表示区间右端点恰好是i的所有区间的影响之和,C[i]表示区间右端点大于i的所有区间的影响之和。每当遇到 Insert时,考虑当前的Insert会对以后的Query产生什么影响,更新B和C数组;当遇到Query时,把两部分的结果累加起来。

具体来说,当我们遇到Insert(p, d)时,把B[p]增加p*d,把C[1], C[2], …, C[p-1]都增加d。当遇到Query(p)时,查询B[1]+B[2]+…+B[p]+C[p]*p即可。可以发现对B数组是修改单个元素,查询区间和;对C数组是修改区间,查询单个元素,这恰好对应于一开始说的树状数组支持的基本操作。于是我们用两个树状数组漂亮地完成了任务。:)


#include <cstdio>  
   
const int MAXN = 1024;  
int B[MAXN], C[MAXN];  
   
#define LOWBIT(x) ((x)&(-(x)))  
   
void bit_update(int *a, int p, int d) {  
    for ( ; p && p < MAXN ; p += LOWBIT(p))  
        a[p] += d;  
}  
   
int bit_query(int *a, int p) {  
    int s = 0;  
    for ( ; p ; p -= LOWBIT(p))  
        s += a[p];  
    return s;  
}  
   
void bit_update2(int *a, int p, int d) {  
    for ( ; p ; p -= LOWBIT(p))  
        a[p] += d;  
}  
   
int bit_query2(int *a, int p) {  
    int s = 0;  
    for ( ; p && p < MAXN ; p += LOWBIT(p))  
        s += a[p];  
    return s;  
}  
   
inline void _insert(int p, int d) {  
    bit_update(B, p, p*d);  
    bit_update2(C, p-1, d);  
}  
   
inline int _query(int p) {  
    return bit_query(B, p) + bit_query2(C, p) * p;  
}  
   
inline void insert_seg(int a, int b, int d) {  
    _insert(a-1, -d);  
    _insert(b, d);  
}  
   
inline int query_seg(int a, int b) {  
    return _query(b) - _query(a-1);  
}  
   
int main() {  
    int com, a, b, c;  
    while (scanf("%d%d%d",&com,&a,&b) != EOF) {  
        a += 2; b += 2;     //防止出现负数  
        if (com == 0) {     //更新  
            scanf("%d",&c);  
            insert_seg(a, b, c);  
        } else {            //查询  
            printf("%d/n",query_seg(a,b));  
        }  
    }  
    return 0;  
}  

另一篇  树状数组求最值 转载自: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

另外,该方法还有一个减枝, 很容易想到,在求最大值时,当某个值更新的值大于原值的时候是没有必要再去查询儿子节点的,所以内部循环可加一个判断来判定是否需要扫描儿子节点,可能是数据问题,该题时间并没有大的变化


你可能感兴趣的:(用树状数组解决区间查询问题)