树状数组(BIT,binary indexed tree)

1 lowbit运算

2 树状数组

2.1 需解决的问题

  • 设计函数getSum(x),返回前x个数之和A[1]+......+A[x]。
  • 设计函数update(x, v),实现将第x个数加上一个数v的功能,即A[x]+=v。

2.2 树状数组

        树状数组  C[i]  其实仍然是一个数组,是一个用来记录和的数组,下标必须从1开始,它存放的是在i号位之前(含i号位,下同)lowbit(i)个整数之和。(并不是前i个整数之和)

树状数组定义图

2.3 树状数组如何实现getSum(x)

记SUM(1, x) = A[1]+...+A[x]

由于C[x]的覆盖长度是lowbit(x),因此 C[x] = A[x-lowbit(x)+1]+...+A[x]

于是可以得到SUM(1, x)=SUM(1, x-lowbit(x)) + C[x]

int lowbit(int x){
    return x & (-x);
}
// getSum函数返回前x个整数之和
int getSum(int x){
    int sum=0;
    for(int i=x;i>0;i-=lowbit(i)){   // 注意边界是  i>0
        sum+=C(i);
    }
    return sum;
}

        getSum 时间复杂度分析:由于lowbit(i) 的作用是定位i的二进制中最右边的1,因此i = i - lowbit(i)事实上是不断把i的二进制中最右边的1置为0的过程。所以getSum函数的for循环执行次数为x的二进制中1的个数,也就是说,getSum函数的时间复杂度为 O(logN)。

        如果要求数组下标在区间[x, y]内的数之和,即A[x] + A[x+1] + ... + A[y],可以转换成getSum(y) - getSum(x-1)来解决。

2.4 树状数组如何实现update(x, v)

        要让A[x]加上v,就是要寻找树状数组C中能覆盖A[x]的那些元素,让它们都加上v。

树状数组定义图

        从图上直观来看,只需要总是寻找离当前的“矩形” C[x]最近的“矩形” C[y],使得C[y]能够覆盖C[x]即可。

        由于lowbit(y) > lowbit(x),问题等价于求一个尽可能小的整数a,使得lowbit(x+a) > lowbit(x)。

        显然,由于lowbit(x)是取x的二进制最右边的1的位置,因此如果lowbit(a)<lowbit(x), lowbit(x+a)就会小于lowbit(x)。为此lowbit(a)必须不小于lowbit(x)。

        当a取lowbit(x)时,由于x和a的二进制最右边的1的位置相同,因此x+a会在这个1的位置上产生进位,使得进位过程中的所有连续的1变成0,直到把它们左边第一个0置为1时结束。于是lowbit(x+a) > lowbit(x)显然成立,最小的a就是lowbit(x)

        于是update函数的做法就很明确了,只要让x不断加上lowbit(x),并让每步的C[x]都加上v,直到x超过给定的数据范围为止(因为在不给定数据范围的情况下,更新操作是无上限的)。

int lowbit(int x){
    return x & (-x);
}
// update函数将第x个整数加上v
void update(int x,int v){
    for(int i=x;i<=N;i+=lowbit(i)){   // 注意i必须能取到N
        C[i]+=v;  // 让C[i]加上v,然后让C[i+lowbit(i)]加上v
    }
}

显然,这个过程是从右至左不断定位x的二进制最右边的1的左边的0的过程,因此update函数的时间复杂度为O(logN)。

update函数二进制路径图

2.5 离散化

离散化,把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。

通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。例如:

原数据:1,999,100000,15;处理后:1,3,4,2;

原数据:{100,200},{20,50000},{1,400};处理后:{3,4},{2,6},{1,5};

  • 一般来说,离散化只适用于离线查询,因为必须知道所有出现的元素之后才能方便进行离散化。
  • 不过对于在线查询来说,可以先把所有操作都记录下来,然后对其中出现的数据进行离散化,之后再按照记录下来的操作顺序正常进行“在线”查询即可。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN=1e5+10;
int lowbit(int i) {
    return i & (-i);
}
struct Node { // 辅助离散化的实现
    int val;  // 序列元素的值
    int pos;  // 原始序号
    bool operator< (const Node& y) const {
        return val < y.val;
    }
} temp[MAXN]; // temp数组临时存放输入数据
int A[MAXN];  // 离散化后的原始数组
int c[MAXN];  // 树状数组
// update函数将第x个整数加上v
void update(int x,int v) {
    for(int i=x; i<MAXN; i+=lowbit(i)) {
        c[i]+=v;
    }
}
// getSum函数返回前x个整数之和
int getSum(int x) {
    int sum=0;
    for(int i=x; i>0; i-=lowbit(i)) {
        sum+=c[i];
    }
    return sum;  // 返回和
}
int main() {
    int n,x;
    cin>>n;
    memset(c,0,sizeof(c));  // 树状数组初始值为0
    for(int i=0; i<n; i++) {
        cin>>temp[i].val;  // 输入序列元素
        temp[i].pos = i;  // 原始序号
        // update(x,1);   // x的出现次数加1
        // cout<<getSum(x-1)<<endl;  // 查询当前小于x的数的个数
    }
    // 离散化
    sort(temp,temp+n);  // 按val从小到大排序
    for(int i=0; i<n; i++) {
        // 与上一个元素值不同时,赋值为元素个数
        if(i==0 || temp[i].val!=temp[i-1].val)
            A[temp[i].pos] = i+1;  // [注意]这里必须从1开始
        else  // 与上一个元素值相同时,直接继承
            A[temp[i].pos] = A[temp[i-1].pos];
    }
    // 正式进入更新、求和操作
    for(int i=0; i<n; i++) {
        update(A[i],1);   // A[i]的出现次数加1
        cout<<getSum(A[i]-1)<<endl;  // 查询当前小于A[i]的数的个数
    }
    return 0;
}

3 树状数组的应用

3.1 统计序列中在元素左边比该元素小的元素个数

问题描述:给定一个有N个正整数的序列A(N<=10^5,A[i]<=10^5),对序列中的每个数,求出序列中它左边比它小的个数。(其中“小”的定义根据题目而定,并不一定必须是数值的大小)

问题分析

        先来看使用hash数组的做法,其中hash[x]记录整数x当前出现的次数。接着,从左到右遍历序列A,假设当前访问的是A[i],那么就令hash[A[i]]加1,表示当前整数A[i]的出现次数增加了一次;同时,序列中在A[i]左边比A[i]小的数的个数等于hash[1]+hash[2]+...+hash[A[i]-1],这个和需要输出。但是很显然,这两个工作可以通过树状数组的update(A[i], 1)和getSum(A[i]-1)来解决。

        使用树状数组时,不必真的建一个hash数组,因为它只存在于解法的逻辑中,并不需要真的用到,只需用一个树状数组来代替它即可。

#include <iostream>
#include <cstring>
using namespace std;
const int MAXN=1e5+10;
int lowbit(int i) {
    return i & (-i);
}
int c[MAXN];  // 树状数组
void update(int x,int v) {
    for(int i=x; i<MAXN; i+=lowbit(i)) {
        c[i]+=v;
    }
}
int getSum(int x) {
    int sum=0;
    for(int i=x; i>0; i-=lowbit(i)) {
        sum+=c[i];
    }
    return sum;  // 返回和
}
int main() {
    int n,x;
    cin>>n;
    memset(c,0,sizeof(c));  // 树状数组初始值为0
    for(int i=0; i<n; i++) {
        cin>>x;  // 输入序列元素
        update(x,1);   // x的出现次数加1
        cout<<getSum(x-1)<<endl;  // 查询当前小于x的数的个数
    }
    return 0;
}

3.2 求序列第K大

这个问题等价于寻找第一个满足条件“getSum(i)>=K”的i(即i值最小)

        针对这个问题,由于getSum(i)值随着i递增,可以令left=1、right=MAXN,然后在[left, right]范围内进行二分,对当前的mid,判断getSum(mid)>=K是否成立:如果成立,说明所求位置不超过mid,因此令right=mid;如果不成立,说明所求位置大于mid,因此令left=mid+1。如此二分,直到left<right不成立为止。显然二分的时间复杂度是O(logn),求和的时间复杂度也是O(logn),因此总复杂度是O(logn*logn)。

// 求序列元素第K大
int findKthElement(int K) {
    int left=1,right=MAXN,mid;
    while(left<right) { // 循环,直到[left, right]能锁定单一元素
        mid = (left+right)/2;
        if(getSum(mid)>=K)
            right = mid;
        else
            left = mid+1;
    }
    return left;  // 返回二分夹出来的元素
}

4 拓展——高维树状数组

        如果给定一个二维整数矩阵A,怎样求A[1][1] ~ A[x][y]这个子矩阵中所有元素之和,以及怎样给单点A[x][y]加上整数v?

        事实上只需把树状数组推广到二维即可。具体做法是,直接把update函数和getSum函数中的for循环改为两重。

        如果想求A[a][b] ~ A[x][y]这个子矩阵的元素之和,只需计算getSum(x, y) - getSum(x-1, y) - getSum(x, y-1) + getSum(x-1, y-1)即可。

        更高维的情况只需把for循环改为相应的重数即可。

int c[MAXN][MAXN];  // 二维树状数组
// 二维update函数位置为(x, y)的整数加上v
void update(int x,int y,int v){
    for(int i=x;i<MAXN;i+=lowbit(i)){
        for(int j=y;j<MAXN;j+=lowbit(j)){
            c[i][j] += v;
        }
    }
}
// 二维getSum函数返回(1, 1)到(x, y)的子矩阵中元素之和
int getSum(int x,int y){
    int sum=0;
    for(int i=x; i>0; i-=lowbit(i)) {
        for(int j=y;j>0;j-=lowbit(j)){
            sum+=c[i][j];
        }
    }
    return sum;  // 返回和
}

5 拓展——区间更新、单点查询

        到这里为止,前面都是在对树状数组进行单点更新、区间查询,下面将讨论区间更新、单点查询的问题。

5.1 需解决的问题

  • 设计函数getSum(x),返回A[x]
  • 设计函数update(x, v),将A[1] ~ A[x]的每个数都加上一个数v

5.2 解题思路

        首先,树状数组C中每个“矩形”C[i]仍然保持和之前一样的长度,即lowbit(i)。只不过C[i]不再表示这段区间的元素之和,而是表示这段区间每个数当前被加了多少

        如下图,C[16] = 0表示A[1] ~ A[16]都被加了0,C[8] = 5表示A[1] ~ A[8]都被加了5,C[6]=3表示A[5] ~ A[6]都被加了3,C[5]=6表示A[5]被加了6。

        显然,对A[5]来说,它被C[5]加了6,被C[6]加了3,被C[8]加了5,因此实际上的A[5]的值应当是C[5]+C[6]+C[8] = 14

        很快就会发现,A[x]的值实际就是覆盖它的若干个“矩形”C[i]的值之和,而这显然是之前“单点更新、区间查询”问题中update函数的做法。

单点查询示意图

 

// getSum函数返回第x个整数的值
int getSum(int x){
    int sum=0;
    for(int i=x;i<MAXN;i+=lowbit(i))
        sum+=c[i];
    return sum;
}

         接着来看update函数。此处的update需要把A[1] ~ A[x]的每个数都加上v,它等价于让C[x]加上v,然后执行UPDATE(1, x-lowbit(x))

         如下图,让A[1] ~ A[14]的每个数都加上6,等价于让C[8]、C[12]、C[14]加上6

区间更新示意图

 

// update函数将前x个整数都加上v
void update(int x,int v){
    for(int i=x;i>0;i-=lowbit(i))
        c[i]+=v;
}

         显然,如果需要让A[x] ~ A[y]的每个数加上v,只要先让 A[1] ~ A[y]的每个数加上v,然后让A[1] ~ A[x-1]的每个数加上(-v)即可,即先后执行update(y, v)、update(x-1, -v)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值