树状数组

引入


  背景:

  给定一个序列如何求出其中某个区间的值,例如

	a[] = {1,2,3,5,4,8,9,6,3,4}

  现在我想查询a[3] - a[7] 这个区间的值,很显然我们可以用一个循环做到,那么如果给定序列很大,想求的区间也很多,每次使用循环做,就会很慢。

  前缀和

  如何优化??,高中我们就学过数列的前n项和,我们可以利用这个思想,对于任何一个区间我们可以使用(注:S是前n项和) S7 - S2 即可求出 a[3] - a[7]这个区间的值,因为:
  S7 = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7]
  S2 = a[1] + a[2]
两式相减即可得到 a[3] ~ a[7] 的值,这就是前缀和的思想。
练习题

//参考代码
#include <iostream>
using namespace std;
const int N = 100010;
int f[N]; //前缀和数组
int main()
{
    int n,m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++)
    {
        int a;
        cin >> a;
        f[i] += f[i - 1] + a;	//每次等于前一项和 + 当前项
    }
    while(m --)
    {
        int l,r;
        cin >> l >> r;
        cout << f[r] - f[l - 1] << endl;
    }
}

  那么现在需求更改了:
    1.数列a中的值可能会更改
    2.快速查看某个区间的值
  此时在使用之前的前缀和的方法就不行了,因为每次数列变动就会重修计算一次前缀和数组,每次时间复杂度是 O(n),如果有 m 次更改,复杂度就是 O(nm) 当 n 和 m 比较大的时候基本就会TLE,如果使用差分的话,可以 O(1) 完成操作1,但操作 2 需要 O(n),前缀和恰好相反。

树状数组


  现在有一个退而求其次的做法 ——— 树状数组,可以以O(logn)的复杂度完成上述两个操作,大概就是使用一个数组,数组中的每个元素记录一段区间的和,在修改的我们只需要修改 logn 个区间,查询的时候,我们将对应的区间段加上就能得到结果。(正题开始)

原理

  根据任意正整数关于2的不重复次幂的唯一分解性质(就是一个数可以用二进制表示),我们可以将一个正整数分解成:
x = 2 i 1 + 2 i 2 + . . . + 2 i m x = 2^{i_1}+2^{i_2}+ ... +2^{i_m} x=2i1+2i2+...+2im
  不妨设:
i 1 > i 2 > . . . > i m {i_1}> {i_2}> ... > {i_m} i1>i2>...>im
  进一步我们可以将区间[1,x]分成 O(log x) 个小区间
    1.长度为 2 i 1 2^{i_1} 2i1 的小区间  [ 1 , 2 i 1 ] [1,2^{i_1}] [1,2i1]
    2.长度为 2 i 2 2^{i_2} 2i2 的小区间  [ 2 i 1 + 1 , 2 i 1 + 2 i 2 ] [2^{i_1}+1, 2^{i_1} + 2^{i_2}] [2i1+1,2i1+2i2]
    3.长度为 2 i 3 2^{i_3} 2i3 的小区间  [ 2 i 1 + 2 i 2 + 1 , 2 i 1 + 2 i 2 + 2 i 3 ] [2^{i_1} + 2^{i_2} + 1,2^{i_1} + 2^{i_2} + 2^{i_3}] [2i1+2i2+1,2i1+2i2+2i3]
      . . . . . . ...... ......
    m 长度为 2 i m 2^{i_m} 2im 的小区间  [ 2 i 1 + 2 i 2 + . . . + 2 i m − 1 + 1 , 2 i 1 + 2 i 2 + + 2 i 3 ] [2^{i_1} + 2^{i_2}+ ... + 2^{i_{m - 1}} + 1,2^{i_1} + 2^{i_2} ++ 2^{i_3}] [2i1+2i2+...+2im1+1,2i1+2i2++2i3]

  是不是很一头雾水,我们来模拟一下就清楚了

    x = 7   分解为: 2 2 + 2 1 + 2 0 2^2+2^1+2^0 22+21+20,  区间[1,7]可以分为[1,4],[5,6],[7,7]三个小区间。
  我们将x = 7的二进制位表示出来:0111 我们发现上面三个区间长度就等于 ”二进制分解“ 下最小 2 的幂次。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mrLITWKG-1582810116827)(http://139.9.81.229:8090/upload/2020/2/image-9a249aea17e04f0391ae98523b47fef0.png)]

lowbit运算

  这里顺便就要提到 lowbit 运算,它可以返回最低为的 1 及其该位后面的所有 0 组成的所有值,如有不懂,可以百度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-utdlfuJk-1582810116828)(http://139.9.81.229:8090/upload/2020/2/image-84aaaf37f1ad43df8250f8dc320121f4.png)]

lowbit代码实现
int lowbit(int x) 
{
	return x & -x;
}

  有了这个函数加上前面根据二进位中的 1 来划分区间,给定任意一个整数 x ,我们可以使用下面这段代码划分成 O(logx) 个小区间:

while(x > 0)
{
	printf("[%d, %d]\n",x - lowbit(x) + 1 ,x);
	x -= lowbit(x);
}
具体思路

  现在对于任意给定一个序列 a ,我们在建立一个数组c,其中c[x]保存序列 a 的区间 [x - lowbit(x) + 1,x] 中所有数的和
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MvkAioPm-1582810116829)(http://139.9.81.229:8090/upload/2020/2/image-7fc2dae69fba48588f5459e03225f5db.png)]
  首先我们强调一下,树状数组的下标从 11 开始计数,这一点我们看到后面就会很清晰了。我们先了解如下的定义,请大家一定先记住一下性质:
  1.数组 C 是一个对原始数组 A 的预处理数组。
  2.每个内部节点c[x] 保存以它为根的子树中所有叶节点的和。
  3.每个内部节点从c[x] 的子节点个数等于 lowbit(x)的位数。
  4.除了树根以外,每个内部节点c[x] 的父节点是 c[x + lowbit(x)]
  5.树的深度为 O(lonN)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AdL1DSaz-1582810116829)(http://139.9.81.229:8090/upload/2020/2/image-8eae13393d9f4211bee8bb414b5bbd27.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lr4T6Llr-1582810116830)(http://139.9.81.229:8090/upload/2020/2/image-e50ce714f9c345f8953f6aa6e3ab7e5d.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SVoxtZOz-1582810116830)(http://139.9.81.229:8090/upload/2020/2/image-70af57a9c3db4407b105154824f985dc.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uhnf3wxc-1582810116831)(http://139.9.81.229:8090/upload/2020/2/image-4cbb237e146a4653ab5adb3298fa4048.png)]
上面的过程我们用如下的表来表示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oXWe4wzp-1582810116831)(http://139.9.81.229:8090/upload/2020/2/image-86458776482042b6a86431271a1bf782.png)]

1. “单点更新”操作:“从子结点到父结点”

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njvQEW9E-1582810116832)(http://139.9.81.229:8090/upload/2020/2/image-5ce7f1ce09834814bdef2edff8c6afdd.png)]
例1:修改 A [ 3 ] A[3] A[3], 分析对数组 C C C​​ 产生的变化。

  从图中我们可以看出 A [ 3 ] A[3] A[3] 的父结点以及祖先结点依次是 C [ 3 ] C[3] C[3] C [ 4 ] C[4] C[4] C [ 8 ] C[8] C[8] ,所以修改了 A [ 3 ] A[3] A[3] 以后 C [ 3 ] C[3] C[3] C [ 4 ] C[4] C[4] C [ 8 ] C[8] C[8] 的值也要修改。

  先看 C [ 3 ] C[3] C[3] l o w b i t ( 3 ) = 1 ​ ​ , 3 + l o w b i t ( 3 ) = 4 ​ lowbit(3)=1​​,3+lowbit(3)=4​ lowbit(3)=13+lowbit(3)=4​ 就是 C [ 3 ] ​ C[3]​ C[3] 的父亲结点 C [ 4 ] C[4] C[4]​ 的索引值。
再看 C [ 4 ] ​ C[4]​ C[4] l o w b i t ( 4 ) = 4 ​ , 4 + l o w b i t ( 4 ) = 8 lowbit(4)=4​,4+lowbit(4)=8 lowbit(4)=44+lowbit(4)=8​ 就是 C [ 4 ] C[4] C[4]​ 的父亲结点 C [ 8 ] ​ C[8]​ C[8]的索引值。
  从图中,也可以验证:“红色结点的索引值 + 右下角蓝色圆形结点的值 = 红色结点的双亲结点的索引值”,对应上面性质4。
分析到这里“单点更新”的代码就可以马上写出来了。

2.单点更新代码实现
void add(int x,int y)	// x 是修改的点,y是加的值
{
	for(;x <= N;x += lowbit(x))c[x] += y; //N 是序列的总点数

}
3.查询前缀和

  查询前缀和,即 a 的 1 ~ x 的个数的和,按照刚刚的方法,把 [ 1 , x ] [1,x] [1,x]分成 O(logN) 个小区间,而每个小区间的区间和都保存在数组 c c c中,所以查询前缀就等于加上每个区间的值即可,代码如下:

int ask(int x)
{
	int ans = 0;	//记录和
	for(;x ; x -= lowbit(x)) ans += c[x];
	return ans;
}
4.区间查询

  对于任意一个区间的和,我们只需要计算 ask® - ask(l -1)即可,l 是左端点,r 是右端点。

5.如何建树

  为了简便,比较一般的初始化是:直接建立一个全为0的数组c,然后对每个位置 x 执行 add(x,a[x]) 就可以完成对原序列 a 构造树状数组的过程,时间复杂度 O(N logN)。

例题:

区间查询,单点修改
区间修改,单点查询
区间查询,区间修改
树状数组维护区间最值
树状数组求逆序对
树状数组求指定长度单调子序列

参考代码:

1.区间查询,单点修改

#include <iostream>
#include <algorithm>
#include <stdio.h>

using namespace std;

const int N = 5 * 1e5 + 10;

int c[N];

int n,m;

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

int ask(int x)
{
    int ans = 0;
    for(;x;x -= lowbit(x)) ans += c[x];
    return ans;
}
void add(int x,int t)
{
    for(;x <= n; x += lowbit(x)) c[x] += t;
}

int main()
{
    scanf("%d%d",&n,&m);
    
    for(int i = 1; i <= n; i ++)
    {
        int t;
        cin >> t;
        add(i,t);		//初始化树
    }
    int a,b,c;
    while(m --)
    {
        cin >> a >> b >> c;
        if(a == 1)
            add(b,c);		//修改区间的某个值
        else 
            printf("%d\n",ask(c) - ask(b - 1));//查询某个区间
    }
    
}

2.区间修改,单点查询

//本题是树状数组 + 差分
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

int a[N],d[N];
int n,m;
inline int lowbit(int x)
{
    return x & -x;
}

int ask(int x)
{
    int ans = 0;
    for(; x ; x -= lowbit(x)) ans += d[x];
    return ans;
}
int add(int x,int c)
{
    for(;x <= n; x += lowbit(x)) d[x] += c;
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> a[i];
 
    while(m --)
    {
        string op;
        cin >> op;
        if(op == "Q")
        {
            int x;
            cin >> x;
            cout << a[x] + ask(x) << endl;
        }
        else
        {
            int l,r,c;
            cin >> l >> r >> c;
            add(l,c);
            add(r + 1, -c );
        }
    }
}

3.区间查询,区间修改

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N];
LL cs[2][N],sum[N];
int n,m;
inline int lowbit(int x)
{
    return x & -x;
}
LL ask(int k,int x)
{
    LL ans = 0;
    for(; x ; x -= lowbit(x)) ans += cs[k][x];
    return ans;
}
void add(int k,int x,int c)
{
    for(; x <= n; x += lowbit(x)) cs[k][x] += c;
}


int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++)
    {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
    }
    while(m --)
    {
        string op;
        int l,r,d;
        cin >> op;
        if(op == "Q")
        {
            cin >> l >> r;
            LL ans = sum[r] + (r + 1) * ask(0,r) - ask(1,r);
            ans -= sum[l - 1] + l * ask(0,l-1) - ask(1,l - 1);
            printf("%lld\n",ans);
        }
        else 
        {
            cin >> l >> r >> d;
            add(0,l,d);
            add(0,r + 1,-d);
            add(1,l,l * d);
            add(1,r + 1,-(r + 1) * d);
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值