树状数组
树状数组是一个查询和修改复杂度都为 log ( n ) \log(n) log(n)的数据结构。主要用于数组的单点修改和区间求和。
另外一个拥有类似功能的是线段树。
树状数组类似于线段树的简化版本,线段树能做的树状数组基本都能做。
树状数组的结构
树状数组的储存通常用一个数组来实现,数组的下标就是节点的编号。令 T [ i ] T[i] T[i]为树状数组,而 C [ i ] C[i] C[i]为原数组。
T [ i ] T[i] T[i]的节点值 i i i,写成二进制形式,例如树状数组中的节点值 i i i为 11010110 0 2 110101100_{2} 1101011002,那么节点 T [ i ] T[i] T[i]的值将是原数组 C [ i ] C[i] C[i]中从 11010100 0 2 110101000_{2} 1101010002一直到 11010110 0 2 110101100_{2} 1101011002的和。
Lowbit
定义一个二进制数的 L o w b i t Lowbit Lowbit为二进制中最低位的 1 1 1,例如 11010110 0 2 110101100_{2} 1101011002的 L o w b i t Lowbit Lowbit就是 00000010 0 2 000000100_{2} 0000001002。
如何求一个 L o w b i t Lowbit Lowbit,利用补码的知识,一个正数的相反数的补码是将这个正数的二进制从最低位开始一直复制直到遇到第一个 1 1 1为止(包括),从下一位开始,复制的时候要取反。
因此,运算 x & ( − x ) x \& (-x) x&(−x)可以得到一个二进制数的 L o w b i t Lowbit Lowbit。
int lowbit(int x)
{
return x & (-x);
}
树状数组的核心思想
我们定义 T [ i ] = ∑ i − lowbit ( i ) + 1 i a i T[i] = \sum_{i - \text{lowbit}(i) + 1}^{i} a_i T[i]=∑i−lowbit(i)+1iai。树状数组的主要思想就是如何去维护 T [ i ] T[i] T[i]。
树状数组的单点修改和构建
我们先定义一个和原数组大小相同的数组 T [ i ] T[i] T[i]。初始化内容都是 0 0 0。
我们有如下单点修改的代码:
int add(int i,int v)
{
while(i <= n)
{
tree[i] += v;
i += lowbit(i);
}
}
怎么理解呢,我们依次枚举管理原数组中节点 i i i的在树状数组中的节点,然后将这些节点分别修改即可。
例如,我们想修改
C
[
0111010
0
2
]
C[01110100_{2}]
C[011101002]这个节点,我们根据上面的知识知道,管理这个节点的树状数组节点有
T
[
0111100
0
2
]
T[01111000_{2}]
T[011110002]和
T
[
100000
0
2
]
T[1000000_{2}]
T[10000002]。如何遍历这个树状数组中的节点呢,即在i += lowbit(i);
即可。
构建一个树状数组,即把构建的过程看成单点修改的过程即可。
树状数组的单点查询
我们想求出 C [ 0 ] + … + C [ i ] C[0]+\ldots+C[i] C[0]+…+C[i]该怎么做呢。一个树状数组的节点管理了很多节点的和,因此,每个树状数组的节点管理一段的和,我们只需要遍历这些段即可。
int sum(int i)
{
int res;
while(i > 0)
{
res += tree[i];
i -= lowbit(i);
}
return res;
}
例如,我们想求
C
[
0
]
+
…
+
C
[
0111010
0
2
]
C[0]+\ldots+C[01110100_{2}]
C[0]+…+C[011101002]的和,树状数组的节点
T
[
0111010
0
2
]
T[01110100_{2}]
T[011101002]管理了和
C
[
0111000
1
2
]
+
…
+
C
[
0111010
0
2
]
C[01110001_{2}] + \ldots + C[01110100_{2}]
C[011100012]+…+C[011101002],
T
[
0111000
0
2
]
T[01110000_{2}]
T[011100002]管理了和
C
[
0110000
1
2
]
+
…
+
C
[
0111000
0
2
]
C[01100001_{2}] + \ldots + C[01110000_{2}]
C[011000012]+…+C[011100002],
T
[
0110000
0
2
]
T[01100000_{2}]
T[011000002]管理了和
C
[
0100000
1
2
]
+
…
+
C
[
0110000
0
2
]
C[01000001_{2}] + \ldots + C[01100000_{2}]
C[010000012]+…+C[011000002],
T
[
0100000
0
2
]
T[01000000_{2}]
T[010000002]管理了和
C
[
0000000
1
2
]
+
…
+
C
[
0100000
0
2
]
C[00000001_{2}] + \ldots + C[01000000_{2}]
C[000000012]+…+C[010000002]。
因此我们只需依次递减i -= lowbit(i);
即可遍历所有的段。
树状数组的区间修改和单点查询
这时候我们就需要用到差分数组的知识了,根据差分数组,我们可以在区间修改只需要修改断点值就行了。
我们把 C [ i ] C[i] C[i]看做是差分数组,D[i]定义为原数组, T [ i ] T[i] T[i]为树状数组维护的是 C [ i ] C[i] C[i], C [ i ] C[i] C[i]维护 D [ i ] D[i] D[i]。
void update(int l,int r,int val)
{
add(l,val);
add(r+1,-val);
}
查询只需要求出差分数组的前缀和即可,这一点只需要调用一次 s u m sum sum函数,故不再赘述。
树状数组的区间修改和区间查询
区间修改已经说明了,只需要修改端点即可。对于区间查询,我们可以先求出 D [ i ] D[i] D[i]两个前缀和,相减即可了。
因此,问题就转化成如何求一个 D [ i ] D[i] D[i]前缀和 D [ 1 ] + … + D [ n ] D[1] + \ldots + D[n] D[1]+…+D[n]。
∑ i = 1 n D [ i ] = ∑ j = 1 n ∑ i = 1 j C [ i ] = ∑ i = 1 n [ C [ i ] × ( n − i + 1 ) ] = ∑ i = 1 n [ C [ i ] × ( n + 1 ) ] − ∑ i = 1 n [ C [ i ] × i ] = ( n + 1 ) ∑ i = 1 n C [ i ] − ∑ i = 1 n ( C [ i ] × i ) \sum_{i=1}^{n}D[i] = \\ \sum_{j=1}^{n}\sum_{i=1}^{j}C[i] = \\ \sum_{i=1}^{n}[C[i] \times (n-i+1)] = \\ \sum_{i=1}^{n}[C[i] \times (n+1)] - \sum_{i=1}^{n}[C[i] \times i]= \\ (n+1) \sum_{i=1}^{n}C[i] - \sum_{i=1}^{n}(C[i] \times i) i=1∑nD[i]=j=1∑ni=1∑jC[i]=i=1∑n[C[i]×(n−i+1)]=i=1∑n[C[i]×(n+1)]−i=1∑n[C[i]×i]=(n+1)i=1∑nC[i]−i=1∑n(C[i]×i)
因此我们需要两个树状数组, T 1 [ i ] T_{1}[i] T1[i]维护数组 C [ i ] C[i] C[i]的和, T 2 [ i ] T_{2}[i] T2[i]维护数组 C [ i ] × i C[i] \times i C[i]×i的和。需要用的时候两个前缀和相减即可。
完整代码:
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt", "r", stdin)
typedef long long ll;
#define N 10000
int tree1[N], tree2[N];
int lowbit(int x)
{
return x & (-x);
}
int add(int *tree, int i, int v)
{
while (i <= N)
{
tree[i] += v;
i += lowbit(i);
}
}
int sum(int *tree, int i)
{
int res = 0;
while (i > 0)
{
res += tree[i];
i -= lowbit(i);
}
return res;
}
void update(int l, int r, int val)
{
add(tree1, l, val);
add(tree1, r + 1, -val);
add(tree2, l, l * val);
add(tree2, r + 1, -(r + 1) * val);
}
int TolSum(int l, int r)
{
int rv = sum(tree1, r) * (r + 1) - sum(tree2, r);
cout << rv << endl;
int lv = sum(tree1, l - 1) * l - sum(tree2, l - 1);
return rv - lv;
}
int main()
{
update(1, 3, 1);
update(2, 4, 1);
// 1 2 3 4
// 1 2 2 1
cout << TolSum(1, 4);
return 0;
}
应用
求逆序对
我们依次扫描输入的数组,假如遇到数字 i i i,那么就把 C [ i ] C[i] C[i]的值加一,如果要求 i i i位置上的逆序对的个数,可以求 C [ 1 ] + … + C [ i ] C[1] + \ldots + C[i] C[1]+…+C[i]表示小于等于 i i i的个数了,然后用已经插入的个数减去这个和,就是这个位置上的逆序对数了。
然而,当我们遇到数组中的数比较分散的时候,我们可以给他离散化一下,例如:
5 -1 3 7 9
1 2 3 4 5
然后对应排序:
-1 3 5 7 9
2 3 1 4 5
然后求 2 3 1 4 5的逆序对的个数即可。
树状数组维护最大,最小值
用树状数组维护前缀最大值和最小值,将树状数组中的加法改成max或者min即可,例如:
void add(int i, int val)
{
while (i <= n)
{
tree[i] = max(tree[i], val);
i += LB(i);
}
}
int query(int i)
{
int ans = 0;
while (i > 0)
{
ans = max(ans, tree[i]);
i -= LB(i);
}
return ans;
}