导言
深藏于算法与数据结构中的思想非常的美妙,尤其是当我们一个一个攻克其中的难点,体会其中蕴含的"哲理"时, 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 ],我们有
-
若 x 为 奇 数 \color{Orange}奇数 奇数,C[ x ] = A[ x ]
-
若 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[x−2k+1]+A[x−2k+2]+A[x−2k+3]+⋅⋅⋅+A[x]
带入发现对于 X 为奇数时,也满足这个规律,所以在树状数组中,
( 核 心 ) \color{Red}(核心) (核心) C[ x ] 表示 区间 【 x − 2 k + 1 , x 】 \color{sKyblue}【x - 2^k + 1, ~x】 【x−2k+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】 【x−2k+1, x】
= 【 x − l o w b i t ( x ) + 1 , x 】 \color{sKyblue}【x - lowbit(x) + 1, ~x】 【x−lowbit(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,其实对于树状数组的初始化,我们主要有两种方式
-
暴 力 构 造 树 状 数 组 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]); }
-
线 性 构 造 树 状 数 组 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]);
}
以上内容尚未完全,随着今后学习的推进,我会继续对其进行补充与完善。另外,大家如果觉得我写的还行的话,还请赠予我一个可爱的赞,你的赞对于我是莫大的支持。
【预告】
- 树状数组的拓展及例题
- 线段树详解