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


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 a∘b 和 a a a,可以推得 b b b。
  • 结合律 :即维护的信息所做的运算 ∘ \circ ∘ 满足 ( a ∘ b ) ∘ c = a ∘ ( b ∘ c ) (a\circ b)\circ c=a\circ(b\circ c) (a∘b)∘c=a∘(b∘c)。

在树状数组中,记 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=l∑rai 可以拆成 ∑ 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=1∑rai−i=1∑l−1ai。

显然查询前缀和我们只要不断减去当前的 lowbit \text{lowbit} lowbit 即可。

修改和查询的时间复杂度都是 O ( n log ⁡ n ) O(n\log n) O(nlogn)。

具体实现:

cpp 复制代码
#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=ai−ai−1),就变成了单点修改 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 bl←bl+x,br+1←br+1−x,区间查询 ∑ i = 1 x b i \sum\limits_{i=1}^xb_i i=1∑xbi 了。

代码和上一题几乎是一样的,所以不再展示了。

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=1∑rai−i=1∑l−1ai,也就是说我们只要考虑如何求一个前缀和 ∑ i = 1 x a i \sum\limits_{i=1}^xa_i i=1∑xai。

同上一题一样,将 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 x i ⋅ b i \begin{aligned} &\sum\limits_{i=1}^xa_i\\ =&\sum\limits_{i=1}^x\sum\limits_{j=1}^ib_j\\ =&b_1+\\ &b_1+b_2+\\ &b_1+b_2+b_3+\\ &\cdots+\\ &b_1+b_2+b_3+\cdots+b_x\\ =&\sum\limits_{i=1}^x(x-i+1)b_i\\ =&(x+1)\sum\limits_{i=1}^xb_i-\sum\limits_{i=1}^xi\cdot b_i \end{aligned} ====i=1∑xaii=1∑xj=1∑ibjb1+b1+b2+b1+b2+b3+⋯+b1+b2+b3+⋯+bxi=1∑x(x−i+1)bi(x+1)i=1∑xbi−i=1∑xi⋅bi

发现前后两个和式都是可以用 BIT 维护的!

于是就做完了。代码留给读者实现。

3. 值域树状数组

也称权值树状数组,就是将值的出现情况展现在 BIT 上,类似于哈希表。

Luogu P1908 逆序对 为例。

题目要求 i < j i<j i<j 且 a i > a j a_i>a_j ai>aj 的二元组 ( i , j ) (i,j) (i,j) 个数。

考虑一个显然的事情:开一个桶,存放每种数的出现次数 c i c_i ci。那么当加入 a j a_j aj 时,能与 j j j 配对形成 ( i , j ) (i,j) (i,j) 的 i i i 的个数就是 ∑ k = 1 i − 1 c k \sum\limits_{k=1}^{i-1}c_k k=1∑i−1ck。

而这个东西恰恰是可以使用 BIT 维护的!

代码也很简单,留给读者实现。

4. 二维树状数组

也称 BIT 套 BIT,也就是把 BIT 放到平面上。

Loj 133 二维树状数组 1:单点修改,区间查询 为例。

单点修改是同理的,只要在循环外再套一层就可以。

cpp 复制代码
void add(int x,int y,int d)
{
	for(int i=x;i<=n;i+=i&-i)
	{
		for(int j=y;j<=m;j+=j&-j)
		{
			bit[i][j]+=d;
		}
	}
}

区间查询同理。但是注意查询的是 ∑ i = 1 x ∑ j = 1 y a i , j \sum\limits_{i=1}^x\sum\limits_{j=1}^ya_{i,j} i=1∑xj=1∑yai,j 的信息,即二维前缀和,减一减即可。

cpp 复制代码
int qry(int x,int y)
{
	int res=0;
	for(int i=x;i;i-=i&-i)
	{
		for(int j=y;j;j-=j&-j)
		{
			res+=bit[i][j];
		}
	}
	return res;
}
int qry(int a,int b,int c,int d){return qry(c,d)-qry(c,b-1)-qry(a-1,d)+qry(c-1,d-1);}

Loj 134 二维树状数组 2:区间修改,单点查询 做二维差分即可。

但是,Luogu P4514 上帝造题的七分钟 / Loj 135 二维树状数组 3:区间修改,区间查询 就不是这样的了。

和一维的同理,令 b i , j = a i , j − a i − 1 , j − a i , j − 1 + a i − 1 , j − 1 b_{i,j}=a_{i,j}-a_{i-1,j}-a_{i,j-1}+a_{i-1,j-1} bi,j=ai,j−ai−1,j−ai,j−1+ai−1,j−1 后推一个式子:
∑ i = 1 x ∑ j = 1 y a i , j = ∑ i = 1 x ∑ j = 1 y ∑ p = 1 i ∑ q = 1 j b p , q = ∑ i = 1 x ∑ j = 1 y ( x − i + 1 ) ( y − j + 1 ) b i , j = ∑ i = 1 x ∑ j = 1 y ( ( x + 1 ) ( y + 1 ) − ( x + 1 ) j − ( y + 1 ) i + i ⋅ j ) b i , j = ( x + 1 ) ( y + 1 ) ∑ i = 1 x ∑ j = 1 y b i , j − ( x + 1 ) ∑ i = 1 x ∑ j = 1 y j ⋅ b i , j − ( y + 1 ) ∑ i = 1 x ∑ j = 1 y i ⋅ b i , j + ∑ i = 1 x ∑ j = 1 y i ⋅ j ⋅ b i , j \def\s{\sum\limits} \begin{aligned} &\s_{i=1}^x\s_{j=1}^ya_{i,j}\\ =&\s_{i=1}^x\s_{j=1}^y\s_{p=1}^i\s_{q=1}^jb_{p,q}\\ =&\s_{i=1}^x\s_{j=1}^y(x-i+1)(y-j+1)b_{i,j}\\ =&\s_{i=1}^x\s_{j=1}^y((x+1)(y+1)-(x+1)j-(y+1)i+i\cdot j)b_{i,j}\\ =&(x+1)(y+1)\s_{i=1}^x\s_{j=1}^yb_{i,j}-(x+1)\s_{i=1}^x\s_{j=1}^yj\cdot b_{i,j}-(y+1)\s_{i=1}^x\s_{j=1}^yi\cdot b_{i,j}+\s_{i=1}^x\s_{j=1}^yi\cdot j\cdot b_{i,j} \end{aligned} ====i=1∑xj=1∑yai,ji=1∑xj=1∑yp=1∑iq=1∑jbp,qi=1∑xj=1∑y(x−i+1)(y−j+1)bi,ji=1∑xj=1∑y((x+1)(y+1)−(x+1)j−(y+1)i+i⋅j)bi,j(x+1)(y+1)i=1∑xj=1∑ybi,j−(x+1)i=1∑xj=1∑yj⋅bi,j−(y+1)i=1∑xj=1∑yi⋅bi,j+i=1∑xj=1∑yi⋅j⋅bi,j

于是开 4 4 4 个 BIT 分别维护 b i , j , i ⋅ b i , j , j ⋅ b i , j , i ⋅ j ⋅ b i , j b_{i,j},i\cdot b_{i,j},j\cdot b_{i,j},i\cdot j\cdot b_{i,j} bi,j,i⋅bi,j,j⋅bi,j,i⋅j⋅bi,j 即可。

5.巩固练习

事实上,BIT 还有很多应用,如维护不可差分的信息等。但这些通常是用其他数据结构平替的,故不在此继续做阐述。感兴趣的读者可以自行了解。

相关推荐
一只小bit20 分钟前
C++之初识模版
开发语言·c++
CodeClimb1 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
apz_end2 小时前
埃氏算法C++实现: 快速输出质数( 素数 )
开发语言·c++·算法·埃氏算法
仟濹3 小时前
【贪心算法】洛谷P1106 - 删数问题
c语言·c++·算法·贪心算法
苦 涩3 小时前
考研408笔记之数据结构(七)——排序
数据结构
北顾南栀倾寒3 小时前
[Qt]系统相关-网络编程-TCP、UDP、HTTP协议
开发语言·网络·c++·qt·tcp/ip·http·udp
Victoria.a4 小时前
顺序表和链表(详解)
数据结构·链表
old_power4 小时前
【PCL】Segmentation 模块—— 基于图割算法的点云分割(Min-Cut Based Segmentation)
c++·算法·计算机视觉·3d
涛ing5 小时前
21. C语言 `typedef`:类型重命名
linux·c语言·开发语言·c++·vscode·算法·visual studio
笔耕不辍cj5 小时前
两两交换链表中的节点
数据结构·windows·链表