ACM-数据结构-树状数组I

ACM竞赛中,树状数组,即二分索引树(BinaryIndexedTree,BIT),也是常见的一种数据结构,其应用场景如下:

给出一个长度为n的数组(a[1]-a[n]),每一次给出一个i,询问该数组的前缀和sum[i]。一般情况下,可以在O(n)时间复杂度内处理出所有的前缀和,在O(1)时间复杂度内回答询问。但是如果还存在修改操作,比如修改数组中的某一个元素的值,就无法保证只进行一次O(n)时间复杂度的处理,也就是说直接模拟的话,要么O(n)修改O(1)查询,要么O(1)修改O(n)查询。所以,如果n比较大,操作次数比较多,那么O(n)的时间复杂度就有可能变得不可接受。

树状数组可以解决这一问题,达到O(logn)修改O(logn)查询的时间复杂度,从O(n)到O(logn)可以优化很多时间。

那么树状数组是如何做到的呐?它引入一个数组c[i],代表[i-(i&(-i))+1,i]区间内元素a[i]的所有信息,比如区间内的所有元素和、区间内的所有元素的最大值等。以区间和为例,结合c数组和a数组,就可以得到一棵类似于树的结构,右边的数字代表a数组元素下标,长条代表c数组中的元素,下图反应了这两个数组之间的关系:


1、如何建立树状数组

初始状态下,a数组是空的,那么树状数组的c数组也自然设置成0了。

// build the BIT
memset(c, 0, sizeof(c));

2、如何进行单点增减

首先要介绍一种特殊的操作,叫做lowbit(x),它返回将x表示成二进制后,最后一个1的位置。

// return the last position of 1 of binary x
inline int lowbit(int x) {return x&(-x);}
这一步操作其实用来确定随着更新操作的进行,而需要进行修改的c数组元素的下标。c数组需要修改的原因很简单,看前面的那副关系图,既然a数组的元素被更新了,那么覆盖了该元素的长条,不难发现,这些长条就是从该a数组元素位置开始,所有向上走的长条,也就是c数组中相应的元素,自然也需要随着一起更新了。所以不难写出树状数组的更新操作,以单点增减为例:

inline void add1(int x,int v) {while(x<n)c[x]+=v,x+=lowbit(x);}

3、如何进行区间和查询

要计算区间和,可以先算出前缀和,相减即可。根据前面的关系图,不难发现前缀和即从该a数组元素位置开始,所有向下走的所有长条之和,即相应c数组元素之和:

inline int sum1(int x) {int res=0; while(x)res+=c[x],x-=lowbit(x); return res;}
inline int query1(int x,int y) {return sum1(y)-sum1(x-1);}

上面介绍的树状数组的操作,是其最基本的功能,即对应单点增减,查询区间和。其实树状数组也可以做到区间增减、单点查询,以及区间增减、区间查询,只需要在原来的操作上多进行几步即可。

1、区间增减,单点查询

要实现区间增减,可以利用基础的单点增减操作,即两次单点增减,将区间外多修改的值,+v-v抵消掉。最后执行基础的查询操作即可得到单点的值。

inline void update(int x,int v) {while(x<N)c[x]+=v,x+=lowbit(x);}
inline void add2(int x,int y,int v) {update(x,v); update(y+1,-v);}
inline int sum2(int x) {int res=0; while(x)res+=c[x],x-=lowbit(x); return res;}
inline int query2(int x) {return sum2(x);}

2、区间修改,区间查询

这一种操作除了需要维护原来的c[i]数组,还需要维护i*c[i]的值。

template <typename X>
struct tree_array
{
    struct tree_array_single
    {
        X arr[MAXN];
        void add(int x,X n){while(x<=N)arr[x]+=n,x+=lowbit(x);}
        X sum(int x){X sum=0;while(x)sum+=arr[x],x-=lowbit(x);return sum;}
    }T1,T2;
    void reset(){CLR(T1.arr,0); CLR(T2.arr,0);}
    void add(int x,int n){T1.add(x,n);T2.add(x,x*n);}
    void update(int L,int R,int n){add(L,n);add(R+1,-n);}
    X sum(int x){return (x+1)*T1.sum(x)-T2.sum(x);}
    X query(int L,int R){return sum(R)-sum(L-1);}
};

以上所有操作,树状数组维护的都是区间和信息,其实它也可以维护极值信息,不过要实现这样的操作,就需要更改c数组本身的定义了,然后划分区间求解了。

// 单点更新+区间极值,C[x]表示区间极值
inline void init()
{
    // 找最大值初始化
    for(int i=1; i<=N; ++i)
    {
        c[i] = a[i];
        //j是以2倍的速度增长
        for(int j=1; j<lowbit(i); j<<=1)
        // 找比i小的数但又在lowbit(i)+1到i这个区间上的数更新c数组
        {
            c[i] = std::max(c[i], c[i-j]);
        }
     }
}
inline void update2(int k, int value)
{
    a[k] = value;
    while(k <= N)
    {
        if(value > c[k]) c[k] = value;
        else break;
        k += lowbit(k);
    }
}
inline int query3(int l, int r)
{
    int ans = a[r];
    while(1)
    {
        // 跟r位置上的数字比较
        ans = std::max(ans, a[r]);
        if(l == r) break;
        // r自减1,判断r-lowbit(r)和l之间的关系如果l在区间内就不能减了而是继续循环
        // 如果l比r-lowbit(r)小的话,就可以之间判断ans和p[r]的最值了
        for(r=r-1; r-l>=lowbit(r); r-=lowbit(r))
        {
            if(ans < c[r]) ans = c[r];
        }
    }
    return ans;
}

以一道例题为例,演示树状数组的使用,HDOJ:1541,时空转移(点击打开链接):

Stars

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 5788    Accepted Submission(s): 2301


Problem Description
Astronomers often examine star maps where stars are represented by points on a plane and each star has Cartesian coordinates. Let the level of a star be an amount of the stars that are not higher and not to the right of the given star. Astronomers want to know the distribution of the levels of the stars. 



For example, look at the map shown on the figure above. Level of the star number 5 is equal to 3 (it's formed by three stars with a numbers 1, 2 and 4). And the levels of the stars numbered by 2 and 4 are 1. At this map there are only one star of the level 0, two stars of the level 1, one star of the level 2, and one star of the level 3. 

You are to write a program that will count the amounts of the stars of each level on a given map.
 

Input
The first line of the input file contains a number of stars N (1<=N<=15000). The following N lines describe coordinates of stars (two integers X and Y per line separated by a space, 0<=X,Y<=32000). There can be only one star at one point of the plane. Stars are listed in ascending order of Y coordinate. Stars with equal Y coordinates are listed in ascending order of X coordinate.
 

Output
The output should contain N lines, one number per line. The first line contains amount of stars of the level 0, the second does amount of stars of the level 1 and so on, the last line contains amount of stars of the level N-1.
 

Sample Input
  
  
5 1 1 5 1 7 1 3 3 5 5
 

Sample Output
  
  
1 2 1 1 0
 

Source
 

Recommend
LL   |   We have carefully selected several similar problems for you:   1166  1394  3450  1255  1540 
 

题意:

给出一些星星的坐标,问每一个级别的星星有多少个,星星的级别即在它左下边星星的数量。

分析:

首先,题目保证了给出的星星坐标,是先按y坐标递增排序,再按x坐标递增排序的,也就是说星星是从下往上、从左往右,一层一层给出的。其实,不管怎样,也应该按这样进行排序。

然后,先看y坐标相同的星星,也就是最低一层星星,再从左往右分析,假设c[i]代表的是x坐标为i的级别,所以每有一个星星,它后面星星的级别都得加一,这正好符合了树状数组的单点增减操作,因为这个操作就是将覆盖了当前位置往后的树状数组元素进行增减。

最后,还剩下对y坐标的处理,考虑第二层星星,由于第一层的星星的级别已经处理完了,所以很明显当前x坐标为i的星星的级别等于c[1]+c[2]+...+c[i],这也正好符合树状数组的区间求和操作。

从下到上,从左到右,其余层的星星也是相同的处理方法。

源代码:

#include <cstdio>
#include <cstring>
#include <algorithm>

// if MAXN is too large, must use discrete
const int MAXN = 1e5 + 5;
int c[MAXN], N;
int a[MAXN];
int level[MAXN];

// return the last position of 1 of binary x
inline int lowbit(int x) {return x&(-x);}

// 单点增减+区间求和,C[x]表示区间和,sum(x)=C[1]+C[2]+……C[x]
inline void add1(int x,int v) {while(x<=N)c[x]+=v,x+=lowbit(x);}
inline int sum1(int x) {int res=0; while(x>0)res+=c[x],x-=lowbit(x); return res;}
inline int query1(int x,int y) {return sum1(y)-sum1(x-1);}

// 区间增减+单点查询,C[x]表示区间和,两次单点增减操作实现区间增减,操作sum[x]=C[x]
inline void update1(int x,int v) {while(x<=N)c[x]+=v,x+=lowbit(x);}
inline void add2(int x,int y,int v) {update1(x,v); update1(y+1,-v);}
inline int sum2(int x) {int res=0; while(x>0)res+=c[x],x-=lowbit(x); return res;}
inline int query2(int x) {return sum2(x);}

// 区间增减+区间查询,C1[x]表示区间和C[x],C2[x]表示的是x*C[x]
template <typename X>
struct tree_array
{
    struct tree_array_single
    {
        X arr[MAXN];
        void add(int x,X n){while(x<=N)arr[x]+=n,x+=lowbit(x);}
        X sum(int x){X sum=0;while(x>0)sum+=arr[x],x-=lowbit(x);return sum;}
    }T1,T2;
    void reset(){CLR(T1.arr,0); CLR(T2.arr,0);}
    void add(int x,int n){T1.add(x,n);T2.add(x,x*n);}
    void update(int L,int R,int n){add(L,n);add(R+1,-n);}
    X sum(int x){return (x+1)*T1.sum(x)-T2.sum(x);}
    X query(int L,int R){return sum(R)-sum(L-1);}
};

// 单点更新+区间极值,C[x]表示区间极值
inline void init()
{
    // 找最大值初始化
    for(int i=1; i<=N; ++i)
    {
        c[i] = a[i];
        //j是以2倍的速度增长
        for(int j=1; j<lowbit(i); j<<=1)
        // 找比i小的数但又在lowbit(i)+1到i这个区间上的数更新c数组
        {
            c[i] = std::max(c[i], c[i-j]);
        }
     }
}
inline void update2(int k, int value)
{
    a[k] = value;
    while(k <= N)
    {
        if(value > c[k]) c[k] = value;
        else break;
        k += lowbit(k);
    }
}
inline int query3(int l, int r)
{
    int ans = a[r];
    while(1)
    {
        // 跟r位置上的数字比较
        ans = std::max(ans, a[r]);
        if(l == r) break;
        // r自减1,判断r-lowbit(r)和l之间的关系如果l在区间内就不能减了而是继续循环
        // 如果l比r-lowbit(r)小的话,就可以之间判断ans和p[r]的最值了
        for(r=r-1; r-l>=lowbit(r); r-=lowbit(r))
        {
            if(ans < c[r]) ans = c[r];
        }
    }
    return ans;
}

int main()
{//freopen("sample.txt", "r", stdin);
    int n;
    while(~scanf("%d", &n))
    {
        // build the BIT
        memset(c, 0, sizeof(c));
        memset(level, 0, sizeof(level));
        // N的最大值是坐标最大值
        N = MAXN;
        for(int i=0; i<n; ++i)
        {
            int x, y;
            scanf("%d%d", &x, &y);
            // 树状数组的下标是从1开始的
            ++level[query1(1,x+1)];
            add1(x+1, 1);
        }
        for(int i=0; i<n; ++i) printf("%d\n", level[i]);
    }
    return 0;
}

这里讨论的树状数组是一维的,其实也可以将其扩展到二维,详细的方法可以去这里了解()。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值