【数据结构】—— 树状数组


树状数组 

一个正整数 x 的二进制表示为 a_{k-1}a_{k-2}...a_2a_1a_0 ,其中等于1的位是 \begin{Bmatrix} a_{i1},a_{i2},a_{i3}...a_{im} \end{Bmatrix}

则 x 可以被二进制表示为 x=2^{i1}+2^{i2}+..+2^{im}

不妨设 i1>i2>...im,进一步的,区间[1, x] 可以分成 O(logx) 个小区间

这些小区间的共同特点是:若区间结尾为R,则区间长度就是等于R的“二进制分解”下最小的2的次幂,即 lowbit(R).

例如:x=7=2^2+2^1+2^0,区间 [1, 7] 可以分成 [1, 4] [5, 6] [7, 7]

长度分别是 lowbit(4) = 4, lowbit(6) = 2, lowbit(7) = 1 

〔manim | 算法 | 数据结构〕 完全理解并深入应用树状数组 | 支持多种动态维护区间操作_哔哩哔哩_bilibili


树状数组(Binary Indexed Tree)是一种 基于上述思想的数据结构,其基本用途就是维护序列的前缀和。对于给定的序列 a ,我们建立一个数组 c, 其中 c[x] 保存 a 的区间

[x - lowbit(x) + 1, x] 中所有数的和, \large \sum_{i=x-lowbit(x)+1)}^{x} a[i] 


 

黑色数组代表原来的数组(下面用A[i]代替),红色结构代表我们的树状数组(下面用C[i]代替),发现没有,每个位置只有一个方框,令每个位置存的就是子节点的值的和,则有

  • C[1] = A[1];
  • C[2] = A[1] + A[2];
  • C[3] = A[3];
  • C[4] = A[1] + A[2] + A[3] + A[4];
  • C[5] = A[5];
  • C[6] = A[5] + A[6];
  • C[7] = A[7];
  • C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];

可以发现,这颗树是有规律的

C[i] = A[i - 2k+1] + A[i - 2k+2] + ... + A[i];   //k为i的二进制中从最低位到高位连续零的长度


例如i = 8(1000)时候,k = 3,可自行验证。

这个怎么实现求和呢,比如我们要找前7项和,那么应该是SUM = C[7] + C[6] + C[4];

而根据上面的式子,容易得出 

\large sumi = c[i] + c[i - 2^{k1}] + c[(i-2^{k1}) - 2^{k2}]+...

其实树状数组就是一个二进制上面的应用。

树状数组(BIT)—— 一篇就够了 - Last_Whisper - 博客园

树状数组详解 - Xenny - 博客园


AcWing 241. 楼兰图腾

输入样例:

5
1 5 3 2 4

输出样例:

3 4

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

using namespace std;

const int N = 200010;

typedef long long LL;

int n;
int a[N];
int tr[N];
int greaterr[N], lower[N];

//返回非负整数x在二进制表示下最低位1及其后面的0构成的数值
int lowbit(int x)
{
    return x & -x;
}

//将序列中第x个数加上k。
void add(int x, int c)
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

//查询序列前x个数的和
int sum(int x)
{
    int res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    cin >> n;

    for(int i = 1; i <= n; i ++ ) scanf("%d",&a[i]);

    //从左向右,依次统计每个位置左边比第i个数y小的数的个数、以及大的数的个数
    for(int i = 1; i <= n; i ++ )
    {
        int y = a[i];

        //在前面已加入树状数组的所有数中统计在区间[1, y - 1]的数字的出现次数
        greaterr[i] = sum(n) - sum(y);

        //在前面已加入树状数组的所有数中统计在区间[y + 1, n]的数字的出现次数
        lower[i] = sum(y - 1);

        //将y加入树状数组,即数字y出现1次
        add(y,1);
    }
    
    //清空树状数组,从右往左统计每个位置右边比第i个数y小的数的个数、以及大的数的个数
    memset(tr, 0, sizeof tr);

    LL res1 = 0, res2 = 0;
    for(int i = n; i; i --)
    {
        int y = a[i];
        res1 += greaterr[i] * (LL)(sum(n) - sum(y));
        res2 += lower[i] * (LL)(sum(y - 1));

        //将y加入树状数组,即数字y出现1次
        add(y,1);
    }

    cout << res1 << " " << res2;
    return 0;
}

AcWing 242. 一个简单的整数问题

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
Q 4
Q 1
Q 2
C 1 6 3
Q 2

输出样例:

4
1
2
5

树状数组 + 差分 

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

using namespace std;
typedef long long LL;

const int N = 100010;

int n, m;
int a[N];
LL tr[N];

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

void add(int x, int t)
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += t;
}

LL sum(int x)
{
    LL res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    
    for(int i = 1; i <= n; i ++ ) add(i, a[i] - a[i - 1]);
    
    while(m -- )
    {
        char op[2];
        int l, r, d;
        scanf("%s%d", op, &l);
        if(*op == 'C')
        {
            scanf("%d%d", &r, &d);
            add(l, d), add(r + 1, -d);
        }
        else
        {
            printf("%lld\n", sum(l));
        }
    }
    return 0;
}

AcWing 243. 一个简单的整数问题2 

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4

输出样例:

4
55
9
15

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

using namespace std;

typedef long long LL;

const int N = 100010;

int n,m;
int a[N];
LL tr1[N]; //维护差分数组b[i]的前缀和
LL tr2[N]; //维护b[i] * i 的前缀和

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

void add(LL tr[], int x, LL c)
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

LL sum(LL tr[], int x)
{
    LL res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

LL prefix_sum(int x)
{
    return sum(tr1, x) * (x + 1) - sum(tr2, x);
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ ) scanf("%d",&a[i]);
    for(int i = 1; i <= n; i ++ )
    {
        int b = a[i] - a[i - 1];
        add(tr1, i, b);
        add(tr2, i, (LL)i * b);
    }

    while(m --)
    {
        char op[2];
        int l,r,d;
        scanf("%s%d%d", op, &l, &r);
        if(*op == 'Q')
        {
            printf("%lld\n",prefix_sum(r) - prefix_sum(l - 1));
        }
        else
        {
            scanf("%d",&d);
            add(tr1, l, d), add(tr1, r + 1, -d);
            add(tr2, l, l * d), add(tr2, r + 1, (r + 1) * -d);
        }
    }
    return 0;
}

 AcWing 244. 谜一样的牛 

输入样例:

5
1
2
1
0

输出样例:

2
4
5
3
1

 

// 找到一个最小的x是sum(x) = k
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int h[N];
int ans[N];
int tr[N];

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

void add(int x, int c)
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

int sum(int x)
{
    int res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    cin >> n;
    for(int i = 2; i <= n; i ++ ) scanf("%d", &h[i]);
    
    for(int i = 1; i <= n; i ++ ) tr[i] = lowbit(i);
    
    for(int i = n; i; i -- )
    {
        int k = h[i] + 1;
        int l = 1, r = n;
        while(l < r)
        {
            int mid = l + r >> 1;
            if(sum(mid) >= k) r = mid;
            else l = mid + 1;
        }
        ans[i] = r;
        add(r, -1);
    }
    
    for(int i = 1; i <= n; i ++) printf("%d\n", ans[i]);
    
    return 0;
}
  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玄澈_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值