区间修改区间查询问题
单点修改区间查询问题(问题 1):给定数组 A 1.. n A_{1 .. n} A1..n, m m m 次操作为修改或查询二者之一,修改操作是给定下标 p p p 和增量 q q q 将 A p A_p Ap 修改为 A p + q A_p + q Ap+q,查询操作是给定下标 l , r ( l ≤ r ) l, r (l \leq r) l,r(l≤r) 查询当前数组区间和 ∑ i = l r A i = A l + . . . + A r \sum_{i = l}^r A_i = A_l + ... + A_r ∑i=lrAi=Al+...+Ar。
算法 1-1:维护原数组 A 1.. n A_{1 .. n} A1..n,修改操作和查询操作无特别策略。
算法 1-2:维护前缀和数组 S 1.. n S_{1 .. n} S1..n,构造时满足 S 0 = 0 , S k = ∑ i = 1 k A i = A 1 + . . . + A k − 1 + A k = S k − 1 + A k S_0 = 0, S_k = \sum_{i = 1}^k A_i = A_1 + ... + A_{k - 1} + A_k = S_{k - 1} + A_k S0=0,Sk=∑i=1kAi=A1+...+Ak−1+Ak=Sk−1+Ak,修改操作即迭代修改 S p , S p + 1 , . . . , S n S_p, S_{p + 1}, ..., S_n Sp,Sp+1,...,Sn 为 S p + q , S p + 1 + q , . . . , S n + q S_p + q, S_{p + 1} + q, ..., S_n + q Sp+q,Sp+1+q,...,Sn+q,查询操作即计算 S r − S l − 1 S_r - S_{l - 1} Sr−Sl−1。
算法 1-3:维护原数组的树状数组(Indexed Binary Tree)。树状数组可以看作是一种特殊的前缀和数组,能实现原数组上的单点修改和前缀和查询问题,与前缀和数组相比通过牺牲查询效率去换取较平衡的修改效率。
算法 | 构造时间复杂度 | 修改时间复杂度 | 查询时间复杂度 |
---|---|---|---|
1-1 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
1-2 | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
1-3 | O ( n ) O(n) O(n) | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
区间修改单点查询问题(问题 2):给定数组 A 1.. n A_{1 .. n} A1..n, m m m 次操作为修改或查询二者之一,修改操作是给定下标 l , r ( l ≤ r ) l, r (l \leq r) l,r(l≤r) 和增量 q q q 将 A l , A l + 1 , . . . , A r A_l, A_{l + 1}, ..., A_{r} Al,Al+1,...,Ar 修改为 A l + q , A l + 1 + q , . . . , A r + q A_l + q, A_{l + 1} + q, ..., A_{r} + q Al+q,Al+1+q,...,Ar+q,查询操作是给定下标 p p p 查询当前数组值 A p A_p Ap。
算法 2-1:维护原数组 A 1.. n A_{1 .. n} A1..n,修改操作和查询操作无特别策略。
算法 2-2:维护差分数组 D 1.. n D_{1 .. n} D1..n,构造时令 A 0 = 0 A_0 = 0 A0=0,满足 D k = A k − A k − 1 D_k = A_k - A_{k - 1} Dk=Ak−Ak−1( D D D 是 A A A 的差分数组等价于 A A A 是 D D D 的前缀和数组),修改操作即令 D l = D l + q , D r + 1 = D r + 1 − q D_l = D_l + q, D_{r + 1} = D_{r + 1} - q Dl=Dl+q,Dr+1=Dr+1−q,查询操作即迭代求和 A p = ∑ i = 1 p D i = D 1 + D 2 + . . . + D p A_p = \sum_{i = 1}^p D_i = D_1 + D_2 + ... + D_p Ap=∑i=1pDi=D1+D2+...+Dp。
算法 2-3:维护差分数组的树状数组。因为经过以上推导,原数组上的问题 2 转化为了差分数组上的问题 1。
算法 | 构造时间复杂度 | 修改时间复杂度 | 查询时间复杂度 |
---|---|---|---|
2-1 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
2-2 | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
2-3 | O ( n ) O(n) O(n) | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
区间修改区间查询问题(问题 3):给定数组 A 1.. n A_{1 .. n} A1..n, m m m 次操作为修改或查询二者之一,修改操作是给定下标 l , r ( l ≤ r ) l, r (l \leq r) l,r(l≤r) 和增量 q q q 将 A l , A l + 1 , . . . , A r A_l, A_{l + 1}, ..., A_{r} Al,Al+1,...,Ar 修改为 A l + q , A l + 1 + q , . . . , A r + q A_l + q, A_{l + 1} + q, ..., A_{r} + q Al+q,Al+1+q,...,Ar+q,查询操作是给定下标 l , r ( l ≤ r ) l, r (l \leq r) l,r(l≤r) 查询当前数组区间和 ∑ i = l r A i = A l + . . . + A r \sum_{i = l}^r A_i = A_l + ... + A_r ∑i=lrAi=Al+...+Ar。
算法 3-1:维护原数组 A 1.. n A_{1 .. n} A1..n,修改操作和查询操作无特别策略。
算法 3-2:维护差分数组 D 1.. n D_{1 .. n} D1..n,构造同上,修改操作同上,查询操作即迭代求和 ∑ k = l r A k = ∑ k = l r ∑ i = 1 k D i = ( r − l + 1 ) ∑ i = 1 l − 1 D i + ∑ i = l r ( r − i + 1 ) D i \sum_{k = l}^r A_k = \sum_{k = l}^r \sum_{i = 1}^k D_i = (r - l + 1) \sum_{i = 1}^{l - 1}{D_i} + \sum_{i = l}^r{(r - i + 1) D_i} ∑k=lrAk=∑k=lr∑i=1kDi=(r−l+1)∑i=1l−1Di+∑i=lr(r−i+1)Di。
算法 3-3:通过前缀和
S
k
S_k
Sk 简化差分数组查询表达式如下:
S
k
=
∑
i
=
1
k
A
i
=
∑
i
=
1
k
∑
j
=
1
i
D
j
=
k
D
1
+
(
k
−
1
)
D
2
+
.
.
.
+
D
k
=
(
k
+
1
)
(
D
1
+
D
2
+
.
.
.
+
D
k
)
−
(
D
1
+
2
D
2
+
.
.
.
+
k
D
k
)
=
(
k
+
1
)
∑
i
=
1
k
D
i
−
∑
i
=
1
k
i
D
i
∑
k
=
l
r
A
k
=
S
r
−
S
l
−
1
=
[
(
r
+
1
)
∑
i
=
1
r
D
i
−
l
∑
i
=
1
l
−
1
D
i
]
−
∑
i
=
l
r
i
D
i
S_k = \sum_{i = 1}^k A_i = \sum_{i = 1}^k \sum_{j = 1}^i D_j = k D_1 + (k - 1) D_2 + ... + D_k \\ = (k + 1)(D_1 + D_2 + ... + D_k) - (D_1 + 2 D_2 + ... + k D_k) \\ = (k + 1) \sum_{i = 1}^k{D_i} - \sum_{i = 1}^k{iD_i} \\ \sum_{k = l}^r A_k = S_r - S_{l - 1} = \left[(r + 1) \sum_{i = 1}^r D_i - l \sum_{i = 1}^{l - 1} D_i \right] - \sum_{i = l}^r{i D_i}
Sk=i=1∑kAi=i=1∑kj=1∑iDj=kD1+(k−1)D2+...+Dk=(k+1)(D1+D2+...+Dk)−(D1+2D2+...+kDk)=(k+1)i=1∑kDi−i=1∑kiDik=l∑rAk=Sr−Sl−1=[(r+1)i=1∑rDi−li=1∑l−1Di]−i=l∑riDi
设差分下标积数组
M
1..
n
M_{1 .. n}
M1..n,满足条件
M
k
=
k
D
k
M_k = k D_k
Mk=kDk,当修改操作令
D
l
=
D
l
+
q
,
D
r
+
1
=
D
r
+
1
−
q
D_l = D_l + q, D_{r + 1} = D_{r + 1} - q
Dl=Dl+q,Dr+1=Dr+1−q 时,同步令
M
l
=
M
l
+
l
∗
q
,
M
r
+
1
=
M
r
+
1
−
(
r
+
1
)
∗
q
M_l = M_l + l * q, M_{r + 1} = M_{r + 1} - (r + 1) * q
Ml=Ml+l∗q,Mr+1=Mr+1−(r+1)∗q。同步维护差分数组
D
D
D 的树状数组和差分下标积数组
M
M
M 的树状数组,查询操作即计算
∑
k
=
l
r
S
k
=
(
k
+
1
)
∑
i
=
1
k
D
i
−
∑
i
=
1
k
M
i
∑
k
=
l
r
A
k
=
S
r
−
S
l
−
1
\sum_{k = l}^r S_k = (k + 1) \sum_{i = 1}^k{D_i} - \sum_{i = 1}^k{M_i} \\ \sum_{k = l}^r A_k = S_r - S_{l - 1}
k=l∑rSk=(k+1)i=1∑kDi−i=1∑kMik=l∑rAk=Sr−Sl−1
算法 | 构造时间复杂度 | 修改时间复杂度 | 查询时间复杂度 |
---|---|---|---|
3-1 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) |
3-2 | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
3-3 | O ( n ) O(n) O(n) | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
算法 3-3 代码如下:
#define FD(A, i) (A[i] - ((i) > 1 ? A[(i) - 1] : 0))
#define FM(A, i) ((i) * FD(A, i))
void Construct(int *D, int *M, int *A, int n)
{
IBT_Construct(D, A, n, FD);
IBT_Construct(M, A, n, FM);
}
void Modify(int *D, int *M, int n, int l, int r, int q)
{
IBT_Modify(D, n, l, q);
IBT_Modify(D, n, r + 1, -q);
IBT_Modify(M, n, l, l * q);
IBT_Modify(M, n, r + 1, -(r + 1) * q);
}
int Query(int *D, int *M, int l, int r)
{
int s0, s1;
s0 = l * IBT_Query(D, l - 1) - IBT_Query(M, l - 1);
s1 = (r + 1) * IBT_Query(D, r) - IBT_Query(M, r);
return s1 - s0;
}
该问题也可以用线段树解决,但常数较大,且代码量过大,在此不做深入讨论。
树状数组的定义和性质
令
T
1..
n
T_{1 .. n}
T1..n 为
A
1..
n
A_{1 .. n}
A1..n 上的树状数组,则
∀
k
∈
{
1
,
.
.
.
,
n
}
,
T
k
=
∑
i
=
0
l
o
w
b
i
t
(
k
)
−
1
A
k
−
i
\forall k \in \{1, ..., n\}, T_k = \sum_{i = 0}^{lowbit(k) - 1} A_{k - i}
∀k∈{1,...,n},Tk=∑i=0lowbit(k)−1Ak−i,其中
l
o
w
b
i
t
(
x
)
=
x
a
n
d
(
−
x
)
lowbit(x) = x \ and (-x)
lowbit(x)=x and(−x),
a
n
d
and
and 表示按位与运算。为了更直观地解释
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x) 与
x
x
x 之间的关系,将
x
x
x 转为二进制补码表示(设
x
x
x 长四字节并且
x
>
0
x > 0
x>0)
x
=
b
31
.
.
.
b
1
b
0
‾
x = \overline{b_{31}...b_1b_0}
x=b31...b1b0,其中
b
i
∈
{
0
,
1
}
b_i \in \{0, 1\}
bi∈{0,1}。不妨设
b
0
=
b
1
=
.
.
.
=
b
k
−
1
=
0
,
b
k
=
1
b_0 = b_1 = ... = b_{k - 1} = 0, b_k = 1
b0=b1=...=bk−1=0,bk=1,则有
x
=
(
b
31
.
.
.
b
k
+
1
100...0
)
2
x = (b_{31}...b_{k + 1}100...0)_2
x=(b31...bk+1100...0)2
由于补码表示下
−
x
=
(
n
o
t
x
)
+
1
-x = (not \ x) + 1
−x=(not x)+1(
n
o
t
not
not 表示按位取反),令
b
i
′
=
1
−
b
i
b'_i = 1 - b_i
bi′=1−bi,则
n
o
t
x
=
(
b
31
′
.
.
.
b
k
+
1
′
011...1
)
2
−
x
=
(
b
31
′
.
.
.
b
k
+
1
′
100...0
)
2
not \ x = (b'_{31}...b'_{k+1}011...1)_2 \\ -x = (b'_{31}...b'_{k+1}100...0)_2
not x=(b31′...bk+1′011...1)2−x=(b31′...bk+1′100...0)2
而由于
b
i
a
n
d
b
i
′
=
0
b_i \ and \ b'_i = 0
bi and bi′=0,故
l
o
w
b
i
t
(
x
)
=
x
a
n
d
(
−
x
)
=
(
00...0100..0
)
2
=
2
k
lowbit(x) = x \ and \ (-x) = (00...0100..0)_2 = 2^k
lowbit(x)=x and (−x)=(00...0100..0)2=2k
于是得出以下三条性质:
第一,
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x) 是
2
2
2 的自然数幂;
第二,
x
m
o
d
l
o
w
b
i
t
(
x
)
=
0
x \ mod \ lowbit(x) = 0
x mod lowbit(x)=0;
第三,
x
m
o
d
(
2
∗
l
o
w
b
i
t
(
x
)
)
≠
0
x \ mod \ (2 * lowbit(x)) \not = 0
x mod (2∗lowbit(x))=0。
树状数组
T
1..
n
T_{1 .. n}
T1..n 上的每一项都是原数组
a
1..
n
a_{1 .. n}
a1..n 上的一段区间和,其中任一项
T
k
T_k
Tk 对应区间右端点为
k
k
k,区间长度为
l
o
w
b
i
t
(
k
)
lowbit(k)
lowbit(k)。举个例子,因为
l
o
w
b
i
t
(
12
)
=
4
lowbit(12) = 4
lowbit(12)=4,故
T
12
=
A
9
+
A
10
+
A
11
+
A
12
T_{12} = A_9 + A_{10} + A_{11} + A_{12}
T12=A9+A10+A11+A12。于是按照
l
o
w
b
i
t
(
k
)
lowbit(k)
lowbit(k) 将树状数组
T
1..
n
T_{1 .. n}
T1..n 划分成不同层次,第
i
i
i 层是所有使得
l
o
w
b
i
t
(
k
)
=
2
i
lowbit(k) = 2^i
lowbit(k)=2i 的
T
k
T_k
Tk 项。具体来说,第
0
0
0 层是所有下标模
2
2
2 余
1
1
1 的项,第
1
1
1 层是所有下标模
4
4
4 余
2
2
2 的项,第
2
2
2 层是所有下标模
8
8
8 余
4
4
4 的项,以此类推。设
F
k
j
=
∑
i
=
0
2
j
−
1
A
k
−
i
(
k
m
o
d
2
j
=
0
)
F_k^j = \sum_{i = 0}^{2^j - 1} A_{k - i} (k \ mod \ 2^j = 0)
Fkj=∑i=02j−1Ak−i(k mod 2j=0),则有
F
k
j
=
{
F
k
j
−
1
+
F
k
−
2
j
j
−
1
j
>
0
A
k
j
=
0
F_k^j = \left\{\begin{aligned} & F_k^{j - 1} + F_{k - 2^j}^{j - 1} \quad & j > 0 \\ & A_k \quad & j = 0 \\ \end{aligned}\right.
Fkj={Fkj−1+Fk−2jj−1Akj>0j=0
而由于
T
k
=
F
k
log
2
l
o
w
b
i
t
(
k
)
T_k = F_k^{\log_2 lowbit(k)}
Tk=Fklog2lowbit(k),故有
T
k
=
{
F
k
log
2
l
o
w
b
i
t
(
k
)
−
1
+
T
k
−
l
o
w
b
i
t
(
k
)
l
o
w
b
i
t
(
k
)
>
1
A
k
l
o
w
b
i
t
(
k
)
=
1
F
k
log
2
j
=
{
F
k
log
2
j
−
1
+
T
k
−
j
j
>
1
A
k
j
=
1
T_k = \left\{\begin{aligned} & F_k^{\log_2 lowbit(k) - 1} + T_{k - lowbit(k)} \quad & lowbit(k) > 1 \\ & A_k \quad & lowbit(k) = 1 \\ \end{aligned}\right. \\ F_k^{\log_2 j} = \left\{\begin{aligned} & F_k^{\log_2 j - 1} + T_{k - j} \quad & j > 1 \\ & A_k \quad & j = 1 \\ \end{aligned}\right.
Tk={Fklog2lowbit(k)−1+Tk−lowbit(k)Aklowbit(k)>1lowbit(k)=1Fklog2j={Fklog2j−1+Tk−jAkj>1j=1
A 1..12 A_{1 .. 12} A1..12 | A 1 A_1 A1 | A 2 A_2 A2 | A 3 A_3 A3 | A 4 A_4 A4 | A 5 A_5 A5 | A 6 A_6 A6 | A 7 A_7 A7 | A 8 A_8 A8 | A 9 A_9 A9 | A 10 A_{10} A10 | A 11 A_{11} A11 | A 12 A_{12} A12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
L 0 L_0 L0 | T 1 T_1 T1 | F 2 0 F_2^0 F20 | T 3 T_3 T3 | F 4 0 F_4^0 F40 | T 5 T_5 T5 | F 6 0 F_6^0 F60 | T 7 T_7 T7 | F 8 0 F_8^0 F80 | T 9 T_9 T9 | F 10 0 F_{10}^0 F100 | T 11 T_{11} T11 | F 12 0 F_{12}^0 F120 |
L 1 L_1 L1 | T 2 T_2 T2 | F 4 1 F_4^1 F41 | T 6 T_6 T6 | F 8 1 F_8^1 F81 | T 10 T_{10} T10 | F 12 1 F_{12}^1 F121 | ||||||
L 2 L_2 L2 | T 4 T_4 T4 | F 8 2 F_8^2 F82 | T 12 T_ {12} T12 | |||||||||
L 3 L_3 L3 | T 8 T_8 T8 |
对于以上有关 T k T_k Tk 和 F k j F_k^j Fkj 的定义式,若其形式为 X = Y + Z X = Y + Z X=Y+Z,则令 X X X 为 Y Y Y 和 Z Z Z 的父节点。如此, T ∪ F T \cup F T∪F 构成了由若干棵大小互不相同的满二叉树组成的森林。当 n n n 为 2 2 2 的自然数次幂时,森林退化成一棵满二叉树。这是该数据结构名为树状数组的原因。在实际代码实现中, F F F 作为树状数组 T T T 构造过程的中间结果而不另外保存。而任一 A k A_k Ak 值的修改,对应着满二叉树结构上某一叶节点值的变化,最多造成满二叉树结构上 O ( log n ) O(\log n) O(logn) 个节点值的变化(即从叶节点到根节点唯一路径上的节点)。此即树状数组的修改逻辑。
关于原数组 A 1.. n A_{1 .. n} A1..n 的前缀和数组 S 1.. n S_{1 .. n} S1..n 和树状数组 T 1.. n T_{1 .. n} T1..n 之间的关系,由于有 T k = ∑ i = 0 l o w b i t ( k ) − 1 A k − i = S k − S k − l o w b i t ( k ) T_k = \sum_{i = 0}^{lowbit(k) - 1} A_{k - i} = S_k - S_{k - lowbit(k)} Tk=∑i=0lowbit(k)−1Ak−i=Sk−Sk−lowbit(k),故要通过 T 1.. n T_{1 .. n} T1..n 求出 S k S_k Sk,可以利用递推式 S 0 = 0 , S k = S k − l o w b i t ( k ) + T k S_0 = 0, S_k = S_{k - lowbit(k)} + T_k S0=0,Sk=Sk−lowbit(k)+Tk,而又由于 l o w b i t ( k ) ∈ [ 1 , k ] lowbit(k) \in [1, k] lowbit(k)∈[1,k],保证了递推式下标的合法性和递推式的有穷性。
事实上,如果仅关注 T 1.. n T_{1 .. n} T1..n 的内部结构,可以惊讶地发现规模相同的树状数组和二项堆的结构是同构的(但是需要注意,二项堆不能像树状数组一样用数组实现,因为二项堆通过指针形式保证 O ( log n ) O(\log n) O(logn) 的堆合并时间复杂度,而数组形式仅复制就已经是 O ( n ) O(n) O(n) 了)。维护数组区间和的树状数组与维护可合并堆最值的二项堆的同构性表现了二进制思想在数据结构领域的优美应用。
树状数组的代码实现和复杂度分析
构造代码如下:
#define IBT_Construct(T, A, n, f) \
for(int i = 1; i <= n; i++) \
T[i] = f(A, i); \
for(int i = 2; i <= n; i <<= 1) \
for(int j = i; j <= n; j += i) \
T[j] += T[j - (i >> 1)];
构造时间复杂度
O
(
n
)
O(n)
O(n)。表面上看似乎代码里有二层循环,但以数学形式表示构造过程时间函数
T
C
(
n
)
=
n
+
⌊
n
2
⌋
+
⌊
n
4
⌋
+
.
.
.
+
⌊
n
2
⌊
log
2
n
⌋
⌋
≤
n
+
n
2
+
n
4
+
.
.
.
+
n
⌊
log
2
n
⌋
=
n
[
1
+
1
2
+
1
4
+
.
.
.
+
1
⌊
log
2
n
⌋
]
<
2
n
=
O
(
n
)
T_C(n) = n + \lfloor\frac{n}{2}\rfloor + \lfloor\frac{n}{4}\rfloor + ... + \lfloor\frac{n}{2^{\lfloor\log_2{n}\rfloor}}\rfloor \\ \leq n + \frac{n}{2} + \frac{n}{4} + ... + \frac{n}{\lfloor\log_2{n}\rfloor} \\ = n \left[ 1 + \frac{1}{2} + \frac{1}{4} + ... + \frac{1}{\lfloor\log_2{n}\rfloor} \right] \\ < 2n = O(n)
TC(n)=n+⌊2n⌋+⌊4n⌋+...+⌊2⌊log2n⌋n⌋≤n+2n+4n+...+⌊log2n⌋n=n[1+21+41+...+⌊log2n⌋1]<2n=O(n)
计算得出构造过程总时间复杂度是线性的。
另一种构造方法是逐项构造,即初始清零后用 n n n 次修改操作,时间复杂度 O ( n log n ) O(n \log n) O(nlogn),虽然略劣于正规构造方法,但节省了部分代码量,也可接受。
关于逐项构造的时间复杂度可能会不太显然,因为与其同构的二项堆采用逐项插入的方法构造,虽然单元素插入时间复杂度为
O
(
log
n
)
O(\log n)
O(logn),但经过摊还分析后总构造时间复杂度为
O
(
n
)
O(n)
O(n)。是什么造成二者时间复杂度上的不同?直观来说,设
n
n
n 为
2
2
2 的自然数次幂,对于一棵规模为
n
n
n 的二项树构造,只需要把两棵规模为
n
2
\frac{n}{2}
2n 的二项树之间添加一条边即可(前
n
2
\frac{n}{2}
2n 次插入和后
n
2
\frac{n}{2}
2n 次插入除了最后一步合并,其它操作都是独立的);而对于
T
n
T_n
Tn 的计算,树结构上从
T
n
2
T_{\frac{n}{2}}
T2n 到
T
n
T_n
Tn 的边实际上走过了
n
2
\frac{n}{2}
2n 次,因为
A
1
,
A
2
,
.
.
.
,
A
n
2
A_1, A_2, ..., A_{\frac{n}{2}}
A1,A2,...,A2n 在修改算法中均通过
T
n
2
T_{\frac{n}{2}}
T2n 到
T
n
T_n
Tn。以
n
=
8
n = 8
n=8 为例,设
M
o
d
i
f
y
(
A
k
)
Modify(A_k)
Modify(Ak) 为修改
A
k
A_k
Ak 所涉及的树状数组项的集合,即
M
o
d
i
f
y
(
A
k
)
=
{
T
i
∣
i
−
l
o
w
b
i
t
(
i
)
<
k
≤
i
}
Modify(A_k) = \{T_i | i - lowbit(i) < k \leq i\}
Modify(Ak)={Ti∣i−lowbit(i)<k≤i},则有
M
o
d
i
f
y
(
A
1
)
=
{
T
1
,
T
2
,
T
4
,
T
8
‾
}
M
o
d
i
f
y
(
A
2
)
=
{
T
2
,
T
4
,
T
8
‾
}
M
o
d
i
f
y
(
A
3
)
=
{
T
3
,
T
4
,
T
8
‾
}
M
o
d
i
f
y
(
A
4
)
=
{
T
4
,
T
8
‾
}
M
o
d
i
f
y
(
A
5
)
=
{
T
5
,
T
6
,
T
8
}
M
o
d
i
f
y
(
A
6
)
=
{
T
6
,
T
8
}
M
o
d
i
f
y
(
A
7
)
=
{
T
7
,
T
8
}
M
o
d
i
f
y
(
A
8
)
=
{
T
8
}
\begin{aligned} & Modify(A_1) = \{T_1, T_2, \underline{T_4, T_8}\} \\ & Modify(A_2) = \{T_2, \underline{T_4, T_8}\} \\ & Modify(A_3) = \{T_3, \underline{T_4, T_8}\} \\ & Modify(A_4) = \{\underline{T_4, T_8}\} \\ & Modify(A_5) = \{T_5, T_6, T_8\} \\ & Modify(A_6) = \{T_6, T_8\} \\ & Modify(A_7) = \{T_7, T_8\} \\ & Modify(A_8) = \{T_8\} \\ \end{aligned}
Modify(A1)={T1,T2,T4,T8}Modify(A2)={T2,T4,T8}Modify(A3)={T3,T4,T8}Modify(A4)={T4,T8}Modify(A5)={T5,T6,T8}Modify(A6)={T6,T8}Modify(A7)={T7,T8}Modify(A8)={T8}
故以数学形式表示树状数组和二项堆的逐项构造过程时间函数
T
C
′
(
n
)
=
2
T
′
(
n
2
)
+
n
2
T
C
′
′
(
n
)
=
2
T
′
′
(
n
2
)
+
1
T'_C(n) = 2T'(\frac{n}{2}) + \frac{n}{2} \\ T''_C(n) = 2T''(\frac{n}{2}) + 1
TC′(n)=2T′(2n)+2nTC′′(n)=2T′′(2n)+1
计算得出树状数组逐项构造时间复杂度
T
C
′
(
n
)
=
O
(
n
log
n
)
T'_C(n) = O(n \log n)
TC′(n)=O(nlogn),二项堆逐项构造时间复杂度
T
C
′
′
(
n
)
=
O
(
n
)
T''_C(n) = O(n)
TC′′(n)=O(n)。
修改操作令 A p = A p + q A_p = A_p + q Ap=Ap+q,查询操作返回前缀和 S p = ∑ i = 1 p A i S_p = \sum_{i = 1}^p A_i Sp=∑i=1pAi,代码如下:
#define lowbit(x) (x & -(x))
#define IBT_Modify(T, n, p, q) \
for(int i = p; i <= n; i += lowbit(i)) \
T[i] += q;
#define IBT_Query(T, p) \
({ \
int r = 0; \
for(int i = p; i > 0; i -= lowbit(i)) \
r += T[i]; \
r; \
})
修改时间复杂度 O ( log n ) O(\log n) O(logn),查询时间复杂度 O ( log n ) O(\log n) O(logn)。修改操作经过上文分析,得出至多修改 O ( log n ) O(\log n) O(logn) 个树状数组项,由此分析出时间复杂度。查询操作利用上文前缀和递推式 S k = S k − l o w b i t ( k ) + T k S_k = S_{k - lowbit(k)} + T_k Sk=Sk−lowbit(k)+Tk,而二进制补码形式下 k − l o w b i t ( k ) k - lowbit(k) k−lowbit(k) 相比 k k k 实际上是将权值最小的 1 1 1 改为 0 0 0,故查询操作迭代次数等于 k k k 的二进制补码形式下 1 1 1 的个数,由此分析出时间复杂度。
最后,树状数组的另一应用场景是求数组的逆序对数,算法大致是按值从大到小顺序,在对应下标位置统计当前前缀和,并插入值 1 1 1,最后求和即为总逆序对数。不过对于大多数情形而言,区间求和问题有更泛用的线段树,逆序对问题用归并排序解决一石二鸟(树状数组求逆序对需要先排序并记下各项在原数组中的下标,而归并排序求逆序对只需要在排序过程中统计即可),树状数组的优势似乎只有常数小而已了(不得不说,线段树和归并的常数确实大)。