「数据结构详解·十五」树状数组

时间:2024-12-15 07:09:26
  • 「数据结构详解·一」树的初步
  • 「数据结构详解·二」二叉树的初步
  • 「数据结构详解·三」栈
  • 「数据结构详解·四」队列
  • 「数据结构详解·五」链表
  • 「数据结构详解·六」哈希表
  • 「数据结构详解·七」并查集的初步
  • 「数据结构详解·八」带权并查集 & 扩展域并查集
  • 「数据结构详解·九」图的初步
  • 「数据结构详解·十」双端队列 & 单调队列的初步
  • 「数据结构详解·十一」单调栈
  • 「数据结构详解·十二」有向无环图 & 拓扑排序
  • 「数据结构详解·十三」优先队列 & 二叉堆的初步
  • 「数据结构详解·十四」对顶堆
  • 「数据结构详解·十五」树状数组

0. 前置知识:lowbit 运算

我们定义 lowbit ( x ) \text{lowbit}(x) lowbit(x) x x x 在二进制下最低的 1 1 1 所代表的数。
lowbit ( 101 0 2 ) = 1 0 2 = 2 10 , lowbit ( 1110 1 2 ) = 1 2 = 1 10 \text{lowbit}(1010_2)=10_2=2_{10},\text{lowbit}(11101_2)=1_2=1_{10} lowbit(10102)=102=210,lowbit(111012)=12=110

我们要如何计算这个东西呢?
一种通常的计算方法是 lowbit ( x ) = x & ( ( ∼ x ) + 1 ) = x & − x \text{lowbit}(x)=x\&((\sim x)+1)=x\&-x lowbit(x)=x&((x)+1)=x&x,手模一下可以得到正确性。

现在你学会了计算 lowbit,下面就可以学习树状数组了!

1. 树状数组的概念

树状数组(Binary Indexed Tree, BIT, Fenwick Tree),也称作二叉索引树,是一种维护序列信息的数据结构。所维护的序列信息和运算需要满足一定的要求:

  • 可差分性:即如果知道了 a ∘ b a\circ b ab a a a,可以推得 b b b
  • 结合律:即维护的信息所做的运算 ∘ \circ 满足 ( a ∘ b ) ∘ c = a ∘ ( b ∘ c ) (a\circ b)\circ c=a\circ(b\circ c) (ab)c=a(bc)

在树状数组中,记 bit i \text{bit}_i biti区间 [ i- lowbit( i )+1, i ] \textbf{[\textit{i-}lowbit(\textit i)+1,\textit i]} [i-lowbit(i)+1,i] 的信息和
那么,对于一个序列 a a a 的前缀信息,都被划分成了 log \textbf{log} log
如图所示,底部的点是 a i a_i ai,上方的点是 bit i \text{bit}_i biti

下面以例题具体解释树状数组的实现。

2. 例题详解

2-1. Luogu P3374 【模板】树状数组 1 / Loj 130 树状数组 1 :单点修改,区间查询

思考一下我们修改位置 x x x 的数会影响的到的 bit i \text{bit}_i biti
结合上图的观察,可以发现,如果我们从下往上修改,当修改的是 x x x,则下一次修改的就是 x + lowbit ( x ) x+\text{lowbit}(x) x+lowbit(x)(具体证明留给读者思考)。

而查询 ∑ i = l r a i \sum\limits_{i=l}^ra_i i=lrai 可以拆成 ∑ i = 1 r a i − ∑ i = 1 l − 1 a i \sum\limits_{i=1}^ra_i-\sum\limits_{i=1}^{l-1}a_i i=1raii=1l1ai
显然查询前缀和我们只要不断减去当前的 lowbit \text{lowbit} lowbit 即可。

修改和查询的时间复杂度都是 O ( n log ⁡ n ) O(n\log n) O(nlogn)
具体实现:

#include<bits/stdc++.h>
using namespace std;

struct fwk{
    int n,bit[500005];

    void init(int i)
    {
        n=i;
        memset(bit,0,sizeof(bit));
    }

    void add(int i,int c)
    {
        for(;i<=n;i+=i&-i) bit[i]+=c;
    }

    int qry(int i)
    {
        int res=0;
        for(;i;i-=i&-i) res+=bit[i];
        return res;
    }
}bit;

int main()
{
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
	int n,m;
	cin>>n>>m;
    bit.init(n);
	for(int i=1;i<=n;i++)
	{
        int x;
		cin>>x;
		bit.add(i,x);
	}
	while(m--)
	{
		int op,x,y;
		cin>>op>>x>>y;
		if(op==1) bit.add(x,y);
		else cout<<bit.qry(y)-bit.qry(x-1)<<endl;
	}
    return 0;
}

2-2. Luogu P3368 【模板】树状数组 2 / Loj 131 树状数组 2 :区间修改,单点查询

我们 BIT 只能单点修改啊?怎么办!
其实,只要将序列 a a a 差分成为 b b b 后( b i = a i − a i − 1 b_i=a_i-a_{i-1} bi=aiai1),就变成了单点修改 b l ← b l + x , b r + 1 ← b r + 1 − x b_{l}\gets b_l+x,b_{r+1}\gets b_{r+1}-x blbl+x,br+1br+1x,区间查询 ∑ i = 1 x b i \sum\limits_{i=1}^xb_i i=1xbi 了。
代码和上一题几乎是一样的,所以不再展示了。

2-3. Luogu P3372 【模板】线段树 1 / Loj 132 树状数组 3 :区间修改,区间查询

这就不是很好做了。
首先区间查询就是 ∑ i = 1 r a i − ∑ i = 1 l − 1 a i \sum\limits_{i=1}^ra_i-\sum\limits_{i=1}^{l-1}a_i i=1raii=1l1ai,也就是说我们只要考虑如何求一个前缀和 ∑ i = 1 x a i \sum\limits_{i=1}^xa_i i=1xai
同上一题一样,将 a a a 差分得到 b b b,有这样的推导:
∑ i = 1 x a i = ∑ i = 1 x ∑ j = 1 i b j = b 1 + b 1 + b 2 + b 1 + b 2 + b 3 + ⋯ + b 1 + b 2 + b 3 + ⋯ + b x = ∑ i = 1 x ( x − i + 1 ) b i = ( x + 1 ) ∑ i = 1 x b i − ∑ i = 1