树状数组基础版解析+区间修改

代码参考博客:https://www.cnblogs.com/RabbitHu/p/BIT.html

先来一张图

我觉得把这张图理解了,树状数组也就差不多理解了。

树状数组采用的是二进制的形式,功能跟线状树差不多,但比线状树要简洁很多。

下面来一一讲解下上图内容。

对于数组a[1],a[2]...a[n],有他们相应的树状数组sum[1],sum[2]...sum[n]来表示它们的和

a[1],1的二进制是0001,最后的1在倒数第1,其树状数组sum[1]表示a[1]          

a[2],2的二进制是0010,最后的1在倒数第2,其树状数组sum[2]表示a[1]+a[2]  

a[3],3的二进制是0011,最后的1在倒数第1,其树状数组sum[3]表示a[3]                  

a[4],4的二进制是0100,最后的1在倒数第3,其树状数组sum[4]表示a[1]+a[2]+a[3]+a[4]

a[5],5的二进制是0101,最后的1在倒数第1,其树状数组sum[5]表示a[5]          

a[6],6的二进制是0110,最后的1在倒数第2,其树状数组sum[6]表示a[5]+a[6]  

a[7],7的二进制是0111,最后的1在倒数第1,其树状数组sum[7]表示a[7]                  

a[8],8的二进制是1000,最后的1在倒数第4,其树状数组sum[8]表示a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]

 

比如我们要求a[1]到a[7]的和,就需要先求sum[7],即a[7],之后7的二进制0111减去最后一个1,得0110,即6,加上sum[6],得a[5]+a[6],0110再减去最后一个1,得0100,即sum[4],最后加上a[1],a[2],a[3],a[4],得到答案。

 

解释一下查找的原理

7的二进制是0111,即2^0+2^1+2^2,只需要遍历三次,第一次加2^0 = 1个数组的值,即a[7],第二次加2^1 = 2个数组的值,即a[5]和a[6],第三次加2^3 = 4个数组的值,即a[1],a[2],a[3],a[4]。

同理,8的二进制是1000,即2^3,只需要遍历一次,第一次就加了2^3 = 8个数组的值。

 

总而言之,树状数组关键就在于利用二进制的最后一个1,查找就是向下加,修改就是向上加。

 

单点修改+区间查询模板:

void add(int p, int x){ //给位置p增加x
    while(p <= n) sum[p] += x, p += p & -p;
}
int ask(int p){ //求位置p的前缀和
    int res = 0;
    while(p) res += sum[p], p -= p & -p;
    return res;
}
int range_ask(int l, int r){ //区间求和
    return ask(r) - ask(l - 1);
}

区间修改+单点查询模板:

void add(int p, int x){ //这个函数用来在树状数组中直接修改
    while(p <= n) sum[p] += x, p += p & -p;
}
void range_add(int l, int r, int x){ //给区间[l, r]加上x
    add(l, x), add(r + 1, -x);
}
int ask(int p){ //单点查询
    int res = 0;
    while(p) res += sum[p], p -= p & -p;
    return res;
}

区间修改+区间查询模板:

void add(ll p, ll x){
    for(int i = p; i <= n; i += i & -i)
        sum1[i] += x, sum2[i] += x * p;
}
void range_add(ll l, ll r, ll x){
    add(l, x), add(r + 1, -x);
}
ll ask(ll p){
    ll res = 0;
    for(int i = p; i; i -= i & -i)
        res += (p + 1) * sum1[i] - sum2[i];
    return res;
}
ll range_ask(ll l, ll r){
    return ask(r) - ask(l - 1);
}

顺便讲一下树状数组的逆序数应用,也就是求一个数组中有多少对逆序对

逆序数

简而言之就是位置与数值相对的一对数,比如2和1,其中2在1前面,但2比1大,那2和1就是一堆逆序数

思路

正常数据范围内,我们可以按顺序查找一个数组,每次查找后树状数组中对应的该值加1,数组下表就是该数值,并向上遍历更新,此后,我们再查找该值在树状数组的前缀和,显然,只要出现过一个比该值小的数,前缀和就会加1,这样我们就可以算出前i个数中比第i个数小的数量,i减去找到的数量就是逆序数,循环一遍即可。

变态数据范围内,比如ai的值很大很大,这样再开树状数组就会爆掉,于是我们可以用离散化来解。

建立一个结构体,里面有数组值和数组位置,然后数组值按从小到大排序,循环一遍数组,每次循环都先将树状数组中,下标对应该值位置的数组加1,然后向上遍历更新前缀和。最后查找数组数组中的该位置,其结果就是位置和值都在第i个数前面的数,i减去该值就是逆序数的数量

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
树状数组(Binary Indexed Tree,BIT)是一种数据结构,用于维护序列的前缀和。它可以支持单点修改区间查询,但是不支持区间修改。不过可以通过巧妙的方式将区间修改转化为单点修改,从而实现区间修改区间修改:我们可以将区间修改转化为单点修改,具体方法是将区间修改拆成两次单点修改:先将区间左端点减一,再将区间右端点加一。这样的话,查询时只需要查询右端点的前缀和,再减去左端点的前缀和即可得到区间和。 区间查询:查询区间和可以通过前缀和相减得到。具体方法是查询右端点的前缀和,再减去左端点减一的前缀和即可得到区间和。 具体实现可以参考下面的代码: ```C++ #include <iostream> #include <vector> using namespace std; class BIT { public: BIT(int n) : sums(n + 1) {} void update(int i, int delta) { while (i < sums.size()) { sums[i] += delta; i += lowbit(i); } } int query(int i) { int sum = 0; while (i > 0) { sum += sums[i]; i -= lowbit(i); } return sum; } private: vector<int> sums; int lowbit(int x) { return x & (-x); } }; class RangeBIT { public: RangeBIT(int n) : bit1(n), bit2(n) {} void update(int i, int j, int delta) { bit1.update(i, delta); bit1.update(j + 1, -delta); bit2.update(i, delta * (i - 1)); bit2.update(j + 1, -delta * j); } int query(int i, int j) { return prefix_sum(j) - prefix_sum(i - 1); } private: BIT bit1, bit2; int prefix_sum(int i) { return bit1.query(i) * i - bit2.query(i); } }; ``` 注意,这里的 BIT 是用于维护前缀和的,而 RangeBIT 是用于维护区间和的。区间修改时,bit1 用于维护前缀和,bit2 用于维护前缀和乘以下标的和。查询时,先计算出区间的前缀和,再减去左端点减一的前缀和乘以左端点即可得到区间和。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值