「学习笔记」树状数组


相信来到这里的 \(OIer\) 们,

都已经知道 前缀和 是什么了(不知道也不关我的事

前缀和可以支持 \(O(n)\) 级别的预处理以及 \(O(1)\) 级别的查询区间和。

但是,

前缀和的 修改时间复杂度,却是 \(O(n)\) 级别,

这是它最大的短板。

而我们今天要介绍的 树状数组,虽然只能做到 \(O(\log n)\) 级别的查询,

但是相较前缀和,

它的 单点修改时间复杂度,能达到 优秀的 \(O(\log n)\)

正题:

1. 核心操作

树状数组有一个 最为核心的操作

\(lowbit\)

顾名思义,\(lowbit\) 操作求的是十进制数 \(x_{(10)}\) 的二进制状态 \(x_{(2)}\)最低位

而这种操作非常简单,只需要一行代码:

inline int lowbit(x) {return x&-x}

举个栗子:

\(x_{(10)}=10,x_{(2)}=1010\)

\(-x_{(10)}=-10,-x_{(2)}=10110\)

我们知道,负数是用 补码 存储的,所以有:

-x~x+1 相等

于是 \(-x_{(2)}\)\(x_{(2)}\) 的最低位 仍然相等

2. 单点修改 \(\to\) 建树

接着,我们就可以开始建树。

这是一个典型的树状数组,可以发现,

图中 \(c_x\) 所管理的元素个数,正是 \(lowbit(x)\)

因此,有以下程序:

inline int lowbit(int x)
{
	return x&(-x);
}
inline void update(int x,int k) //单点修改
{
	while(x<=n)
	{
		a[x]+=k;
		x+=lowbit(x);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int x,i=1;i<=n;i++)
	{
		scanf("%d",&x);
		update(i,x);
	}
	return 0;
}

说得通俗一点,

建树操作,就是对一个所有值为 \(0\) 的树状数组进行 \(N\) 次单点修改

时间复杂度为 \(O(n\log n)\)

3. 区间查询 \(\operatorname{and}\) 单点查询

树状数组的求和操作,

本质上是对 \(1\sim x\) 的求和操作,

其实还是前缀和的思想,

只不过,

树状数组对区间进行划分,每一段区间的和已经确定,

由此就可以做到 \(O(\log n)\) 级别查询。

inline int lowbit(int x)
{
	return x&(-x);
}
inline int query(int x) //单点查询等同于长度为1的区间查询
{
	int res=0;
	while(x)
	{
		res+=a[x];
		x-=lowbit(x);
	}
	return res;
}
/*
while(q--)
{
	scanf("%d%d",&l,&r);
	printf("%d\n",query(r)-query(l-1));
}
*/

4. 区 间 修 改

终于到了最困难的操作。

若维护序列 \(a\) 的差分数组 \(b\),此时我们对 \(a\) 的一个前缀 \(r\) 求和,即 \(\sum_{i=1}^r a_i\)

通过 差分数组 的定义,得 \(a_i=\sum_{j=1}^i b_j\)

进行推导:

\[\begin{aligned} &\sum_{i=1}^r a_i\\ =&\sum_{i=1}^r\sum_{j=1}^i b_j\\ =&\sum_{j=1}^i\times (r-i+1)\\ =&\sum_{j=1}^i b_j\times (r+1)-\sum_{j=1}^i b_j\times i \end{aligned} \]

区间和可以用 两个前缀和相减 得到,因此只需要用 两个树状数组分别维护 \(\sum b_i\)\(\sum i\times b_i\),就能实现区间求和(剩下的结论自己感性理解

时间复杂度:\(O(\log n)\)

有以下程序(利用差分思想实现):

//求和时要记得把差分转化换掉
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
{
	scanf("%lld",&in);
	update(i,in-last); //差分建树
	last=in;
}
for(int x,y,k;m;m--)
{
	scanf("%lld%lld%lld",&x,&y,&k);
	update(x,k);
	update(y+1,-k);
	//差分单点修改
}

总结:

树状数组相对于我们之后要学习的 线段树 相比,树状数组的代码比线段树 更短,思维 更清晰,速度也 更快,在解决一些单点修改的问题时,树状数组是 不二之选

习题:

P3374 【模板】树状数组 1

P3368 【模板】树状数组 2