树状数组
1.基本介绍
树状数组,也称作“二叉索引树”(Binary Indexed Tree)或 Fenwick 树。 它可以高效地实现如下两个操作:
1、单点更新。
2、数组前缀和的查询。
图片转载:地址
注:
数组下标从
1
开始 上面的二进制表示数组的下标
2.树状数组模板
lowbit(x)函数
l o w b i t ( x ) 函 数 : 返 回 x 的 最 后 一 位 1 , 如 x = 20 , 它 的 二 进 制 表 示 为 ( 10100 ) 2 则 l o w b i t ( 20 ) = ( 100 ) 2 = 4 lowbit(x)函数:返回x的最后一位1,如x = 20,它的二进制表示为(10100)_2则lowbit(20) = (100)_2 = 4 lowbit(x)函数:返回x的最后一位1,如x=20,它的二进制表示为(10100)2则lowbit(20)=(100)2=4
int lowbit(int x)
{
return x & -x;
}
add(idx,v)更新函数
add(idx,v)
:给原数组idx
位置上的数加上v
,那么也要更新它的管理者(父节点),也给父节点加v
,这样才能保证前缀和不变。
idx位置上它的父节点为:tr[idx + lowbit(idx)]
i
d
x
+
l
o
w
b
i
t
(
i
d
x
)
就
是
一
个
进
位
(
从
最
后
一
位
1
开
始
)
的
操
作
。
如
t
r
[
12
]
,
(
1100
)
2
在
最
后
一
位
1
进
位
,
即
110
0
2
(
12
)
+
10
0
2
(
4
)
=
(
10000
)
2
=
16
,
t
r
[
16
]
就
是
t
r
[
12
]
的
父
节
点
。
idx + lowbit(idx)就是一个进位(从最后一位1开始)的操作。如tr[12],(1100)_2在最后一位1进位,即1100_2(12) + 100_2(4) = (10000)_2 = 16,tr[16]就是tr[12]的父节点。
idx+lowbit(idx)就是一个进位(从最后一位1开始)的操作。如tr[12],(1100)2在最后一位1进位,即11002(12)+1002(4)=(10000)2=16,tr[16]就是tr[12]的父节点。
那么更新操作为:tr[idx + lowbit(idx)] + v
void add(int idx, int v)
{
for(int i = idx; i <= n; i += lowbit(i)) tr[i] += v;
}
query(x)查询函数
query(x)
:返回数组中位置1~x
的前缀和
int query(int x)
{
int sum = 0;
for(int i = x; i; i -= lowbit(i)) sum += tr[i];
return sum;
}
这个操作实质是将x拆分为它可含的二进制然后通过tr[]
数组进行求和的,我们知道tr[]
数组表示某一段数的和,在前面我们已经构造出了!。
如求位置在1~13
数的和
13
的
二
进
制
为
(
1101
)
2
,
可
划
为
(
1101
)
2
=
13
、
(
1100
)
2
=
12
、
(
1000
)
2
=
8
13的二进制为(1101)_2,可划为(1101)_2 = 13、(1100)_2 = 12、(1000)_2 = 8
13的二进制为(1101)2,可划为(1101)2=13、(1100)2=12、(1000)2=8
这个操作过程就是去掉最后一位1的实现的,当最终只剩下一个1的时,就不能在去掉了,至此操作结束。我们知道tr[]
数组表示某一段的和,在基本介绍图中已介绍tr[x] = 区间[x - lowbit(x) + 1, x]的和
,我们惊奇的发现:
t
r
[
100
0
2
]
=
t
r
[
8
]
=
区
间
[
1
,
8
]
和
t
r
[
110
0
2
]
=
t
r
[
12
]
=
区
间
[
9
,
12
]
和
t
r
[
110
1
2
]
=
t
r
[
13
]
=
区
间
[
13
,
13
]
和
tr[1000_2] = tr[8] = 区间[1,8]和 \\ tr[1100_2] = tr[12] = 区间[9,12]和 \\ tr[1101_2] = tr[13] = 区间[13,13]和
tr[10002]=tr[8]=区间[1,8]和tr[11002]=tr[12]=区间[9,12]和tr[11012]=tr[13]=区间[13,13]和
那么区间[1,13]的和 = tr[8] + tr[12] + tr[13]
了!!!是不是很神奇呀!!!
3.模板例题
动态求连续区间和
【题目链接】1264. 动态求连续区间和 - AcWing题库
给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b] 的连续和。
输入格式
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。第二行包含 n 个整数,表示完整数列。
接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。
数列从 1 开始计数。
输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n,
数据保证在任何时候,数列中所有元素之和均在 int 范围内。输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8
输出样例:
11
30
35
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int q[N];
int tr[N];
int n, m;
int lowbit(int x) // 返回末尾的1和连续的0
{
return x & -x;
}
// 给idx位置上的数加v,那么它的父节点(管理者也要加上v,保证前缀和不变)
void add(int idx, int v)
{
for(int i = idx; i <= n; i += lowbit(i)) tr[i] += v;
}
// 求前缀和(1~x):对x的二进制进行拆分
int query(int x)
{
int sum = 0;
for(int i = x; i ; i -= lowbit(i)) sum += tr[i];
return sum;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> q[i];
for (int i = 1; i <= n; i ++ ) add(i, q[i]);// 预处理
while (m -- )
{
int k, a, b;
cin >> k >> a >> b;
if(k == 0) cout << query(b) - query(a - 1) << endl;// 前缀和求区间和
else add(a, b);
}
return 0;
}
总结
数组数组的原理理解起来个人感觉非常的绕QAQ,不管怎么样,总之模板先熟背!!!!
参考文献:
acwing蓝桥杯辅导课
哔哩哔哩:地址
哔哩哔哩:地址