算法模板-树状数组

树状数组

树状数组,顾名思义,就是树一样的数组(废话)。树状数组,又叫二叉索引树(Binary Indexed Tree),其发明者又称其为Fenwich Tree。其支持两种操作,时间复杂度均为 O ( l o g n ) O(logn) O(logn)单点修改区间查询
在最基础的应用中,单点修改便是修改数组中一个元素的值,区间查询便是查询数组一个区间的和。

例题

上述说到,树状数组最简单的应用中区间查询便是查询数组一个区间的和,那我们便以此来当做例题:

假设输入第一行一个数字T,为T组测试数据
每一组测试数据的第一行第一个正整数N(N<=50000),为数组长度。接下来,有N个正整数,第i个正整数为数组元素ai。
接下来每一行是一条命令,命令分为四种:
(1)A i j:数组i下标增加j。
(2)M i j:数组i下标减去j。
(3)Q i j:查询数组区间i到j的和
(4)E:结束

引入

对于普通数组而言,单点修改的时间复杂度为 O ( 1 ) O(1) O(1),但区间求和的时间复杂度为 O ( n ) O(n) O(n)。如果用前缀和的方法维护这个数组,下标i代表从数组开始到i的数的和,那么区间求和的时间复杂度便降到了 O ( 1 ) O(1) O(1),但是单点修改会影响到后面所有的元素,所以单点修改的时间复杂度上升到了 O ( n ) O(n) O(n)
所以此时,最好采用树状数组,两个操作的时间复杂度折中了一下。树状数组的每一个元素,维护的都是一段区间的区间和。其实,普通数组的元素 i i i维护的是区间 [ a i , a i ] [a_i, a_i] [ai,ai]的区间和;前缀和数组 i i i维护的是区间 [ a 0 , a i ] [a_0, a_i] [a0,ai]的区间和。
树状数组每个元素维护的区间,是依靠二进制的。我们假设需要计算前11项的和。11的二进制为 ( 1011 ) 2 (1011)_2 (1011)2,那么前11项的和便分为区间 ( 1010 , 1011 ] (1010, 1011] (1010,1011] ( 1000 , 1010 ] (1000, 1010] (1000,1010] ( 0000 , 1000 ] (0000, 1000] (0000,1000]的和,这三个区间的和分别存储在树状数组下标1011、1010和1000中。这三个下标,便是每次不断去掉二进制的最后一个0。(1011去掉最后一个0,得到1010,1010去掉最后一个0,得到1000,再去掉,就是0000了。)
树状数组中定义一个数 x x x,它最后一个1连带着这个1后面的0,成为 l o w b i t ( x ) lowbit(x) lowbit(x)。树状数组为 C C C C i C_i Ci保存着区间 ( a i − l o w b i t ( a i ) , a i ] (a_i - lowbit(a_i), a_i] (ailowbit(ai),ai]的区间和。这样显然查询前n项和时需要合并的区间数是少于 l o g 2 n log_2n log2n的。大致结构如下图:

那么更新操作,就可能会更新多个节点,如下图:

我们再以更新二进制数 ( 100110 ) 2 (100110)_2 (100110)2为例,首先其肯定在区间 ( 100100 , 100110 ] (100100, 100110] (100100,100110]中,那么它肯定也在区间 ( 100000 , 101000 ] (100000, 101000] (100000,101000]中,那么它肯定也在区间 ( 100000 , 110000 ] (100000, 110000] (100000,110000]中,那么它肯定也在区间 ( 000000 , 100000 ] (000000, 100000] (000000,100000]中。可以发现,这些区间的右端点的变化有点像进位的过程,那么细心的便宜已经发现了,每次加的都是 l o w b i t ( x ) lowbit(x) lowbit(x)。这样,我们更新的区间数不会超过 l o g 2 l e n g t h log_2length log2length

代码

单点修改

void update(int i, int x) {
    for (int pos = i; pos < MAXN; pos += lowbit(pos)) {
        tree[pos] += x;
    }
}

求前n项和

int query(int x) {
    int res = 0;
    for (int pos = x; pos >= 1; pos -= lowbit(pos)) {
        res += tree[pos];
    }
    return res;
}

求区间和

int query(int x, int y) {
    return query(y) - query(x - 1);
}

整体代码

#include <iostream>
using namespace std;
const int MAXN = 50005;
int tree[MAXN];  //题目中说的N最大为50000,为了方便,申请全局变量数组

int lowbit(int x) { return x & (-x); }

void update(int i, int x) {
    for (int pos = i; pos < MAXN; pos += lowbit(pos)) {
        tree[pos] += x; // 
    }
}

int query(int x) {
    int res = 0;
    for (int pos = x; pos >= 1; pos -= lowbit(pos)) {
        res += tree[pos];
    }
    return res;
}

int query(int x, int y) {
    return query(y) - query(x - 1);
}

void main()
{
    int T;
    cin >> T;
    for (int t = 0; t < T; ++t) {
        memset(tree, 0, sizeof(tree));
        int N, temp;
        cin >> N;
        for (int i = 1; i <= N; ++i) {
            cin >> temp;
            update(i, temp); //初始化的时候,就是更新每一个i位置的值。
        }
        char ch;
        cout << "case: " << t + 1 << endl;
        while (true) {
            cin >> ch;
            bool end = false;
            int i, j;
            switch (ch)
            {
            case 'E':
                end = true;
                break;
            case 'A':
                cin >> i >> j;
                update(i, j);
                break;
            case 'M':
                cin >> i >> j;
                update(i, -j); //减法就是加负数
                break;
            case 'Q':
                cin >> i >> j;
                cout << "[" << i << ", " << j << "]: " << query(i, j) << endl;
            }
            if (end) {
                break;
            }
        }

    }
    system("pause");
}

逆序对

当然,上述的只是树状数组最基本的应用。下面再介绍一个较为广泛的树状数组的应用,逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例:
输入: [7,5,6,4]
输出: 5

代码如下:

class BIT{
private:
    int n;
    vector<int> tree;
public:
    BIT(int _n):n(_n), tree(_n + 1){}
    static int lowbit(int x){return x & (-x);}
    void update(int i, int x){
        for (int pos = i; pos < tree.size(); pos += lowbit(pos)){
            tree[pos] += x;
        }
    }
    int query(int i){
        int res = 0;
        for (int pos = i; pos >= 1; pos -= lowbit(pos)){
            res += tree[pos];
        }
        return res;
    }

};

class Solution {
public:
    int reversePairs(vector<int>& nums) {
        int n = nums.size();
        vector<int> temp = nums;
        sort(temp.begin(), temp.end());
        for (int& num:nums){
        	// 由于数字可能比较多,这里采用离散化,因为在逆序对中,我们只关心元素的相对大小,所以可以先进行排序,找到其在排序中的位置,来节省空间
            num = lower_bound(temp.begin(), temp.end(), num) - temp.begin() + 1;
        }
        BIT bit(n);
        int ans = 0;
        for (int i = n - 1; i >= 0; --i){
        	//因为后面的数大于前面的数才算逆序对,所以加就要查找已经出现的小于等于新nums[i]-1的数字的个数
            ans += bit.query(nums[i] - 1);
            bit.update(nums[i], 1);
        }
        return ans;
    }
};

树状数组和线段树

树状数组和线段树都是一种擅长处理区间的数据结构。它们最大的区别之一就是线段树是一颗完美二叉树,而树状数组(BIT)相当于是线段树中每个节点的右儿子去掉
用树状数组能够解决的问题,用线段树肯定能够解决,反之则不一定。但是树状数组有一个明显的好处就是较为节省空间,实现要比线段树要容易得多,而且在处理某些问题的时候使用树状数组效率反而会高得多。

水平有限,只介绍了两种最简单的应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值