树状数组

树状数组:

1.单点修改,区间求和
 1.快速求前缀和 o(logn)
 2.修改某一个数 o(logn)
2.拓展-区间修改,求单点和(结合差分)
3.拓展-区间修改,区间求和

lowbit函数用于取x二进制最低位的1与后面的0组成的数字
原理:二进制的负数是正数取反加一

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


原理

假设x = 2ik + 2ik-1 +…+ 2i1
ik >= ik-1 >= … i1
将区间划分成(左开右闭):
(x - 2i1, x]
包含2i1个数,2i1是右边界二进制表示的最后一位1与后面的0构成的数
(x - 2i1 - 2i2, x - 2i1]
包含2i2个数,2i2是右边界二进制表示的最后一位1与后面的0构成的数
(x - 2i1 - 2i2 - 2i3, x - 2i1 - 2i2]
 …
(0, x - 2i1 - 2i2 -…- 2ik-1)
包含2ik个数,包含2ik个数,2ik是右边界二进制表示的最后一位1与后面的0构成的数

 -> 对于其中任意区间 (L,R] 长度是R的二进制表示的最后一位1所对应的数
 -> 则对于其中的每一个区间,可以表示为 [R - lowbit(R) + 1, R] (左闭右闭)
 -> 可以用数组c[R]表示这个区间总和
   -> c[x - lowbit(x) + 1, x] 表示以x为右端点长度为lowbit(x)+1的区间的所有数的和
   -> 对于1~x所有数的和,最多可以拆成logx个c[x]的和

树如图所示

  ---------------------------------------------------------------------c16
  ------------------------------------c8
  ----------------c4                      -------------c12
  ------c2            ------c6            -----c10        -----c14    
  -c1       -c3       -c5       -c7       -c9     -c11    -c13    -c15
a 1    2    3    4    5    6    7    8    9   10  11  12  13  14  15  16

c16  =  a16  +  c15  +  c14  +  c12  +  c8
10000           01111   01110   01100   01000

tr[i]表示,以i结尾,长度是lowbit(i)的区间内元素的和!!!

求x的子节点
先将x减去1(eg 10000变成01111),每个1对应一个儿子(eg 01111 01110 01100 01000)
c[x] = a[x] + c[x-1] + c[x-1-lowbit(x-1)] + c[x-1-lowbit(x-1)-lowbit(x-2)] +…+ 0
对于x-1-lowbit(x-1)表示将x-1的最后一个一去掉,即实现01111 -> 01110

===========================

代码实现

1.通过父节点找子节点

对于x-1,有k个儿子,即循环k次,每次减掉最后的1即找到每一个儿子( eg求前缀和)

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

2.通过子节点找父节点

更改x的值,即更改(上图)包含x的所区间
假如x 0011100 -> x父节点p 0100000(唯一的) 0011100 + 0000100 = 0100000
-> p = x + lowbit(x) -> p2 = p + lowbit§ -> p3 = … -> 直到等于n
最多持续logx次(eg某个数加c)

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

3.初始化树状数组

for ( int i = 1; i <= n; i ++ ) add(i, a[i])

->时间复杂度o(nlogn)

========================================================

例题

1.简单的整数问题l

区间修改,单点求值


题目
给定长度为 N 的数列 A,然后输入 M 行操作指令。
 C l r d,表示把数列中第 l∼r 个数都加 d
 Q x,表示询问数列中第 x 个数的值

对于每个询问,输出一个整数表示答案。


思路
树状数组的拓展
将某一区间的每个数加上c,求a[x] -> 差分
将问题变成单点修改,区间求和

树状数组中存储a[i]的差分

构造差分:

b[i] = a[i] - a[i - 1]


代码

#include <iostream>
#include <cstring>
using namespace std;

typedef long long LL;
const int N = 1e5 + 10;
int a[N], tr[N], n, m;

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;
}

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]); //树状数组中存a[i]的差分
    while ( m -- )
    {
        char op[2];
        scanf("%s", op);
        if ( *op == 'C' ) 
        {
            int l, r, d;
            scanf("%d%d%d", &l, &r, &d);
            add(l, d), add(r + 1, -d);  //差分日常操作
        }
        else 
        {
            int x;
            scanf("%d",&x);
            printf("%lld\n", sum(x));     //差分数组的前缀和便是a[i]
        }
    }
    return 0;
}

~~~~~~~~~~~~~~~~~

2.简单的整数问题ll

(树状数组+差分+巧妙运算)区间修改,区间求和**


题目
给定长度为 N 的数列 A,然后输入 M 行操作指令
 C l r d,表示把数列中第 l∼r 个数都加 d
 Q l r,表示询问区间 [l, r] 内元素的和

对于每个询问,输出一个整数表示答案。


思路
构造a数组的差分数组b
区间求和:求a1 + a2 +…+ ax (ax = b1 + b2 +…+ bx)

//朴素法(时间复杂度极高)

  for ( int i = 1; i <= x; i ++ )
      for ( int j =1; j <= i; j ++ ) 
              res += b[j];

//优化法
1.a1到ax的和如图1)所示,要求图1)的和,先将图1)补成图2)

                      2)
1)                    b1 b2 b3 b4 ... bx    注:在没有b的第0行也要补,补完后是x+1行
b1                    b1 b2 b3 b4 ... bx
b1 b2                 b1 b2 b3 b4 ... bx
b1 b2 b3     ->       b1 b2 b3 b4 ... bx
...                   ...
b1 b2 b3 b4 ... bx    b1 b2 b3 b4 ... bx

2)的求和:(b1 + b2 + ... + bx) * (x + 1)
1)的求和:(b1 + b2 + ... + bx) * (x + 1) - (b1 + 2*b2 + ... + x*bx)
    -> 1)的和即为 2)减去i*b的前缀和
    -> 用tr1维护bi的前缀和,tr2维护i*bi的前缀和
    -> sum(tr1, x) * (x + 1) - sum(tr2, x)


代码

#include <iostream>
#include <cstring>
using namespace std;

typedef long long LL;
const int N = 1e5 + 10;
LL tr1[N], tr2[N];
int a[N], n, m;

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 ++ )
    {
        add(tr1, i, a[i] - a[i - 1]);
        add(tr2, i, (LL)(a[i] - a[i - 1]) * i); //可能会爆int
    }
    
    while ( m -- )
    {
        char op[2];
        int l, r, d;
        scanf("%s%d%d", op, &l, &r);
        if ( *op == 'C' ) 
        {
            scanf("%d", &d);
            add(tr1, l, d), add(tr2, l, l * d);
            add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
        }
        else printf("%lld\n", prefix_sum(r) - prefix_sum(l - 1));
    }
    return 0;
}

End

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值