高级数据结构(一)树状数组

引入

根据任意正整数关于 2 的不重复次幂的唯一分解原理性质,若一个正整数 x 的二进制表示为 ak1ak2a2a1a0,其中等于 1 的位是 {ai1,ai2,,aim},则正整数 x 可以被二进制分解为:

x=2i1+2i2++2im

其中,mlogk

不妨设 i1>i2>>im,进一步地,区间 [1,x] 可以分成 O(logx) 个小区间:

  1. 长度为 2i1 的小区间 [1,2i1]
  2. 长度为 2i2 的小区间 [2i1+1,2i1+2i2]
  3. 长度为 2i3 的小区间 [2i1+2i2+1,2i1+2i2+2i3]

m. 长度为 2im 的小区间 [2i1+2i2++2im1+1,2i1+2i2++2im]

这些小区间的共同特点是:若区间结尾为 R,则区间长度就等于 R 的“二进制分解”下最小的 2 的次幂,即 lowbit(R)。例如 x=7=22+21+20,区间 [1,7] 可以分成 [1,4],[5,6][7,7] 三个小区间,长度分别是 lowbit(4)=4,lowbit(6)=2,lowbit(7) = 1

给定一个整数 x,下面这段代码可以计算出区间 [1,x] 分成的 O(logx) 个小区间。

while(x > 0) {
	printf("[%d, %d]\n", x - (x & -x) + 1, x);
	x -= x & -x;
}

1.树状数组简介

树状数组Binary Indexed Trees)就是一种基于上述思想的数据结构。

树状数组支持的操作:

1. 区间和、区间异或和、区间乘积和 RMQ(显然,支持的操作都具有交换律,这也算是树状数组的一大特性吧)

2. 单点修改(朴素的树状数组结构不支持区间修改,当然也可以普及成区间修改结构)

功能听起来和前缀和数组有点像,但它的优势在哪呢?

以求区间和为例,我们知道,前缀和数组求区间和的时间复杂度为 O(n),单点修改操作的时间复杂度为 O(1),所以对于 m 次询问,总时间复杂度为 O(mn)

树状数组平均了一下,两种操作的时间复杂度都是 O(logn),所以对于 m 次询问,总时间复杂度为 O(mlogn),比前缀和数组快上许多。

2.树状数组的存储特点:

对于给定的序列 a,我们建立一个数组 c,其中 c[x] 保存序列 a,的区间 [xlowbit(x)+1,x] 中所有数的和,即 i=xlowbit(x)+1xa[i]

为什么它叫做树状数组呢?事实上,数组 c 可以看作一个如下图所示的树形结构,图中最下边一行是 N 个叶节点(N=8),代表数值 a[1N]。该结构满足以下性质:

  1. 每个内部节点 c[x] 保存以它为根的子树中所有叶结点的和;
  2. 每个内部节点 c[x] 的子节点个数等于 lowbit(x) 的位数;
  3. 除树根外,每个内部节点 c[x] 的父节点是 c[x+lowbit(x)]
  4. 树的深度为 O(logN)
    如果 N 不是 2 的整次幂,那么树状数组就是一个具有同样性质的森林结构。

由图可知:

c[8]=c[4]+c[6]+c[7]+c[8]

c[7]=c[7]

c[6]=c[5]+c[6]

3.树状数组的实现

在执行所有操作之前,我们需要对树状数组进行初始化——针对原始序列 a 构造一个树状数组。

为了简便起见,比较一般的初始化方法是:直接建立一个全为 0 的数组 c,然后对每个位置 x 执行 add(x,a[x]),就完成了对原始序列 a 构造树状数组的过程,时间复杂度为 O(nlogn)。通常采用这种方法已经足够。

更高效的初始化方法是:从小到大依次考虑每个节点 x,借助 lowbit 运算扫描它的子节点并求和。若采用这种方法,上面树形结构中的每条边只会被遍历一次,时间复杂度为 O(k=1logNkN/2k)=O(n)

快速初始化:

void init() {
    for (int i = 1; i <= n; i++) {
        pre[i] = pre[i - 1] + a[i];
        c[i] = pre[i] - pre[i - lowbit(i)];
    }
}

查询前缀和:

int ask(int x) {
	int res = 0;
	for(; x; x -= lowbit(x)) res += c[x];
	return res;
}

单点修改:

void add(int x, int y) {
	for(; x <= n; x += lowbit(x)) c[x] += y;
}

P3374 【模板】树状数组 1


单点询问 + 区间修改:

利用了差分的思想,维护一个差分数组 c,对于指令:把 [x,y] 上的每一个数加上 k,就可转化成把 c[x] 加上 k,再把 c[y+1] 减去 k

P3368 【模板】树状数组 2


区间询问 + 区间修改:

(话说这不就是线段树吗)

说实话,树状数组不仅跑得比线段树快,码量要比线段树小得多,就是比较难想。

P3372 【模板】线段树 1

P2357 守墓人

#include <iostream>

using namespace std;

const int N = 5000010;
typedef long long ll;
int n, m;
ll c[2][N];

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

ll ask(int id, int x) {
	ll res = 0;
	for(; x; x -= lowbit(x)) res += c[id][x];
	return res;
}

void add(int id, int x, ll y) {
	for(; x <= n; x += lowbit(x)) c[id][x] += y;
}

int main() {
	scanf("%d%d", &n, &m);
	ll a, las = 0;
	for(int i = 1; i <= n; i++) {
		scanf("%lld", &a);
		add(0, i, a - las);
		add(1, i, (i - 1) * (a - las));
		las = a;
	}
	int op, x, y;
	ll k;
	while(m--) {
		scanf("%d%d%d", &op, &x, &y);
		if(op == 1) {
			scanf("%lld", &k);
			add(0, x, k);
			add(0, y + 1, -k);
			add(1, x, (x - 1) * k);
			add(1, y + 1, -y * k);
		}
		else {
			printf("%lld\n", y * ask(0, y) - ask(1, y) - (x - 1) * ask(0, x - 1) + ask(1, x - 1));
		}
	}
	return 0;
}
posted @   Brilliant11001  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示