树状数组详解(附图解,模板及经典例题分析)

导言

深藏于算法与数据结构中的思想非常的美妙,尤其是当我们一个一个攻克其中的难点,体会其中蕴含的"哲理"时, A 题的自信力也会有所增加,心情也会格外的舒爽。最近重新接触了树状数组和线段树的题目,决定对其进行一定程度上的系统梳理,并与大家分享,如有不足之处,还请指教,大家共同进步

树状数组

1.基本概念

​ 顾名思义,就是像树一样的数据结构,与 Trie 树类似,其结构类型为完全二叉树,节点排列非常的有规律,故我们直接可以使用数组来模拟,以达到简洁而高效的目的

假设我们有一长为 n 的序列 {a1, a2 , ····,an}, 进行以下操作

(1) 单 点 修 改 \color{Orange}单点修改 : add(x, v)

​ 表示在 第 x 位置上 加上一个数值 v

(2) 区 间 查 询 \color{Orange}区间查询 : query(x)x <= n

​ 表示区间 [1, x] 上的和,即 query(x) = a1 + a2 + ··· + ax。那么我们可以得出区间[x, y] 上的区间和即为 query(y) - query(x - 1) (前缀和思想)

对于上述操作在 n很小的情况下,我们完全可以使用差分与前缀和来操作,复杂度是 O(n)。但是,如果 n很大的情况下,这样的做法效率就会非常低。此时我们就需要一种高效的数据结构来用空间换取时间,也就是树状数组


2.原理推导及如何建立树状数组

如下图所示,我们有

  • 数组A: 传入数据的原数组
  • 数组C:建立起来的树状数组

在这里插入图片描述

( 1 ) 区 间 查 询 − − O ( l o g n ) \color{Turquoise}(1) 区间查询 - -O(logn) (1)O(logn)

从图中我们可以得出,对于树状数组 C 来说

C [ 1 ] \color{green}C[ 1 ] C[1] = A[ 1 ]

C [ 2 ] \color{green}C[ 2 ] C[2] = A[ 2 ] + C [ 1 ] \color{green}C[ 1 ] C[1]

C [ 3 ] \color{green}C[ 3 ] C[3] = A[ 3 ]

C [ 4 ] \color{green}C[ 4 ] C[4] = A[ 4 ] + C [ 3 ] \color{green} C[ 3 ] C[3] + C [ 2 ] \color{green}C[ 2 ] C[2]

C [ 5 ] \color{green}C[ 5 ] C[5] = A[ 5 ]

C [ 6 ] \color{green}C[ 6 ] C[6] = A[ 6 ] + C [ 5 ] \color{green}C[ 5 ] C[5]

C [ 7 ] \color{green}C[ 7 ] C[7] = A[ 7 ]

C [ 8 ] \color{green}C[ 8 ] C[8] = A[ 8 ] + C [ 7 ] \color{green}C[ 7 ] C[7] + C [ 6 ] \color{green}C[ 6 ] C[6]+ C [ 4 ] \color{green}C[ 4 ] C[4]

       ~~~~~~        = A[ 1 ] + A[ 2 ] + A[ 3 ] + A[ 4 ] + A[ 5 ] + A[ 6 ] + A[ 7 ] + A[ 8 ]

初步发现,对于每一个 C[ x ],我们有

  1. 若 x 为 奇 数 \color{Orange}奇数 ,C[ x ] = A[ x ]

  2. 若 x 为 偶 数 \color{Orange}偶数 ,则不然:

    C[ 2 ] = A[ 2 ] + A[ 1 ]

    C[ 4 ] = A[ 4 ] + A[ 3 ] + A[ 2 ] + A[ 1 ]

    C[ 6 ] = A[ 6 ] + A[ 5 ]

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

根据数学归纳可以得出,此时 C [ x ] = A [ x − 2 k + 1 ] + A [ x − 2 k + 2 ] + A [ x − 2 k + 3 ] + ⋅ ⋅ ⋅ + A [ x ] C[ x ] = A[x - 2^k + 1] + A[x - 2^k + 2] + A[x - 2^k + 3] +···+ A[x] C[x]=A[x2k+1]+A[x2k+2]+A[x2k+3]++A[x]

带入发现对于 X 为奇数时,也满足这个规律,所以在树状数组中,

( 核 心 ) \color{Red}(核心) C[ x ] 表示 区间 【 x − 2 k + 1 ,   x 】 \color{sKyblue}【x - 2^k + 1, ~x】 x2k+1, x 的和

k 表示 x 的二进制表示中从最低位到最高位连续零的长度,也就是最后一位 1 的位置,我们可以通过一个名为 l o w b i t \color{Maroon}lowbit lowbit 的操作将其求出

int lowbit (int x)
{
	return x & -x ;//返回 x 的最后一位 1 
}

如 x = 1010,则 lowbit(x)= 10

​ x = 101000,则lowbit(x)= 1000

所以, C[ x ] = 【 x − 2 k + 1 ,   x 】 【x - 2^k + 1, ~x】 x2k+1, x

​ = 【 x − l o w b i t ( x ) + 1 ,   x 】 \color{sKyblue}【x - lowbit(x) + 1, ~x】 xlowbit(x)+1, x的和

有了以上结论,对于树状数组来说,我们如何求出下图区间【1, x】 的区间和呢?

在这里插入图片描述

既然 C[ x ] 表示区间【x - lowbit(x) + 1, x】的和,那么【1, x】区间的和就是 C[ x ] + 递归段,即 C[ x ] + C[ x - lowbit(x) ] + ··· + C[ 1 ]

int query(int x)// 求出区间 【1 ,x】的和
{
    int ans = 0;
    for(int i = x; i ; i -= lowbit(i))
        	ans += c[i];
    
   return ans;
}

( 2 ) 单 点 修 改 − − O ( l o g n ) \color{Turquoise}(2) 单点修改- - O(logn) (2)O(logn)

​ 对于单点修改,也就是 在某个位置 x 加上一个数值 V,如下图所示,假设我们对数组C[ 3 ]位置进行更新时,那么它相应的父亲节点存储的和也需要进行更新,可以发现更新就是一个 “爬树” 的过程。一直把修改信息往上传递,直到到达树的最大高度,也就是树状数组的最大容量 MAXN

在这里插入图片描述

对于区间查询,我们可以形象化为一个 “下树” 的过程,那么单点修改可以看作它的 “逆运算”,即往高的区间走,所以我们就可以得到下列单点修改的代码段了:

const int MAXN = 100010; 
void add(int x, int v)
{
    for(int i = x; i < MAXN; i += lowbit(i) )
        c[i] += v;
}

关于树状数组的正确性证明,大家感兴趣的话可以参考一下这篇博客 树状数组正确性证明

3.代码模板

目前为止,我们尚未提及如何来用原数组 A 来初始化树状数组 C,其实对于树状数组的初始化,我们主要有两种方式

  1. 暴 力 构 造 树 状 数 组 C [ x ] \color{Red}暴力\color{White}构造树状数组 C[x] C[x] : 就是直接对每个位置 x 进行一次单点修改,一共进行 n 次操作,时间复杂度为 O ( n l o g n ) \color{Purple}O(nlogn) O(nlogn)

    void init()
    {
        for(int i = 1; i <= n; i ++ )
            add(i, a[i]);
    }
    
  2. 线 性 构 造 树 状 数 组 C [ x ] \color{Green}线性\color{White}构造树状数组 C[x] 线C[x]:考虑到树状数组的本质,我们知到一个C[x] 管辖的区域是【x - lowbit(x) + 1 ,x】中所有数的和,所以我们可以先对数组A求一个前缀和,利用前缀和 S[ x ] - S[x - lowbit(x)] 来更新数组C[x],即 C[ x ] = S[x] - S[x - lowbit(x)],这样就能线性构造了,时间复杂度为 O ( n ) \color{Purple}O(n) O(n)

    void init()
    {
        for(int i = 1; i <= n; i ++ )// 求a的前缀和
            s[i] = s[i - 1] + a[i];
        for(int i = 1; i <= n; i ++ )// 用前缀和求出c
    		c[i] = s[i] - s[i - lowbit(i)];
    }
    

故此我们即可得到完整的树状数组的模板

const int MAXN = 100010;
// 注意树状数组最大空间一般开到 1e5 + 10

int a[MAXN];// 原数组
int c[MAXN];// 树状数组
int n; // 题目所给的数组 A 数据长度

int lowbit(int x)// 返回最后一位 1,即 2^k
{
    return x & -x;
}
int query(int x)// 查询区间 [1, x] 的和
{
    int ans = 0;
    for(int i = x; i ; i -= lowbit(i))
		ans += c[i];
   
    return ans;
}
void add(int x, int v)//修改位置 x 的值
{
    for(int i = x; i < MAXN; i += lowbit(i))
        c[i] += v;
}
void init()// 初始化, 这里我选用O(nlogn)的方式
{
    for(int i = 1; i <= n; i ++ )
        add(i, a[i]);
}

以上内容尚未完全,随着今后学习的推进,我会继续对其进行补充与完善。另外,大家如果觉得我写的还行的话,还请赠予我一个可爱的赞,你的赞对于我是莫大的支持。


【预告】

  1. 树状数组的拓展及例题
  2. 线段树详解
  • 35
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值