树状数组
Copyright
本页面贡献者:YanhuiJessica。 本页面内容遵循 MIT 协议,转载请附上原文出处链接和本声明。
主要用途¶
- 单点修改,区间查询
- 区间修改,单点查询
- 区间修改,区间查询
单点修改,单点查询(ΦдΦ╬)- 求逆序对
原理¶
-
树状数组中使用数组来存储值,而数组中各个元素间呈树形关系
- e.g. 通过逐个插入的方式创建数组
[1, 2, 3, 4, 5]
的树状数组
- e.g. 通过逐个插入的方式创建数组
-
树状数组被发明是受「所有的整数都可以表示成 2 的幂和(二进制)」的启发,接下来,也将根据图片并结合二进制来解释
- A 数组用于存储原始数据,C 数组管理 A 数组(A 数组可视情况选择是否保留,某些情况下只用 C 数组足矣)(<ゝωΦ)
- 由图可知,C[2] 管理 A[1] 和 A[2],C[4] 管理 A[1]、A[2]、A[3] 和 A[4],以此类推
- 类似于二进制,比如 5 的二进制
101
可以拆分成101 = 100 + 1
,如果要求 A[1] 到 A[5] 的和,我们就可以直接使用 C[4] + C[5](这样看起来好像和前缀和差不多(ΦдΦ╬)前缀和修改起来方便吗?(Φ^Φ)) - 树状数组查询和修改的复杂度都为 O(log(n))
- A 数组用于存储原始数据,C 数组管理 A 数组(A 数组可视情况选择是否保留,某些情况下只用 C 数组足矣)(<ゝωΦ)
基本操作¶
lowbit 函数¶
- 你可能已经发现了,操作的关键在于二进制中的 1,准确的说是需要知道数由 2 的哪些幂组成
- 于是引入了函数
lowbit
,用来求数的二进制表达式中最低位的 1 所代表的值- 比如 6_{(10)}=110_{(2)},从右往左数第一个 1 和其后的 0 组成 10_{(2)},即代表值是 2_{(10)}
1 2 3 4 5 6 7 8
// 宏定义写法 #define lowbit(x) (x & (-x)) // 函数写法 int lowbit(int x) { return x & (-x); }
- 比如 6_{(10)}=110_{(2)},从右往左数第一个 1 和其后的 0 组成 10_{(2)},即代表值是 2_{(10)}
- 至于为什么是
x & -x
,可以学习关于补码的知识- 负数的补码是其对应正数二进制表示按位取反后加 1 的结果
- 6_{(10)}\, \& \,-6_{(10)} = 110_{(2)}\, \& \,(001_{(2)} + 1_{(2)}) = 010_{(2)}
单点修改¶
- 伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
procedure change(pos, v) // pos:要修改元素的下标 // v:改变的值 { while(pos <= n) // n 为数组元素的数量 { c[pos] += v; pos += lowbit(pos); } // 循环形式 // for(i = pos; i <= n; i += lowbit(i)) // c[i] += v; }
- 除非必要,A 数组的更新可以不考虑
求和¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
getsum(r) - getsum(l-1)
就可以了(<ゝωΦ)
进阶¶
区间修改,单点查询¶
- 利用差分建树
- 数组 D 用于存储 A 数组相邻两项的差值,即 D[i] = A[i] - A[i - 1]
- 对于下面的数组 A 及其差分数组 D
- A[] = \{1, 2, 3, 2, 1\}
- D[] = \{1, 1, 1, -1, -1\}
- 将 [1, 3] 区间内所有数加 2
- A[] = \{1, 4, 5, 4, 1\}
- D[] = \{1, 3, 1, -1, -3\}
- 差分数组的优点就体现了,当某个 [l, r] 区间内所有数统一加减时,只有 D[l] 和 D[r + 1] 两个元素的值发生变化
- 这样一来,C 数组就应该建立在 D 数组而非 A 数组上,区间更新则转化为更新两个点,更新函数和求和函数则不需要变
区间修改,区间查询¶
- 基于 区间修改,单点查询 的差分思路,考虑如何在其构建的树状数组中求数组 A 的前缀和:
- ∑_{i=1}^pa[i]=∑_{i=1}^p∑_{j=1}^id[j]
- 在式子 ∑_{i=1}^p∑_{j=1}^id[j] 中,d[1] 被用了 p 次,d[2] 被用了 p-1 次……由此可以推出:
- ∑_{i=1}^p∑_{j=1}^id[j]=∑_{i=1}^pd[i]*(p-i+1)=(p+1)*∑_{i=1}^pd[i]-∑_{i=1}^pd[i]*i
- 接下来,只需要维护两个数组的前缀和,sum1[i]=d[i] 和 sum2=d[i] * i
- 查询和修改的伪代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
procedure change(pos, v) { for(i = pos; i <= n; i += lowbit(i)) sum1[i] += v, sum2[i] += v * pos; } // 更新区间 [l, r] // change(l, v), change(r + 1, -v); procedure getsum(pos) { ans = 0; for(i = pos; i; i -= lowbit(i)) ans += (p + 1)*sum1[i] - sum2[i]; return ans; }