树状数组入门

1 树状数组

1.1 引例导入

n n n个数据 a 1 , a 2 ⋯   , a n ( 1 ≤ n ≤ 1 0 6 ) a_1, a_2 \cdots, a_n(1 \le n \le 10^6) a1,a2,an(1n106), 有 m m m个操作 ( 1 ≤ m ≤ 1 0 6 ) (1 \le m \le 10^6) (1m106), 每个操作为以下两种之一
1 x 表示查询 ∑ x i = 1 a [ i ] \sum^{i=1}_x a[i] xi=1a[i]
2 x y 表示把a[x]修改为y

用普通数组做修改为 O ( 1 ) O(1) O(1), 查询为 O ( n ) O(n) O(n)
用前缀和做修改为 O ( n ) O(n) O(n), 查询为 O ( 1 ) O(1) O(1)
用暴力或前缀和做都会超时, 我们就要用到树状数组

1.2 简介

树状数组可应用于单点修改, 区间求和
单次修改和查询时间复杂度都为 O ( log ⁡ n ) O(\log n) O(logn), 空间复杂度为 O ( n ) O(n) O(n)

2 使用方法

2.1构建

假如我们把 a a a数组看成是完全二叉树的叶子, 构建出树, 在把所有节点向右靠, 就得到以下图形
树状数组的形态
有人可能会奇怪, 为什么这棵树红色的节点不用来存东西
可以通过每一位的二进制看出 c i c_i ci的层数取决于 i i i二进制末尾有多少 0 0 0, 而 0 0 0的数量可以用最后的一位 1 1 1算出, 我们就需要找出最后的 1 1 1

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

正确性证明: 设 x x x的末 k − 1 k-1 k1位是 0 0 0, 第 k k k位是 1 1 1
转成负数的反码后, 第 k k k位是 0 0 0, 末 k − 1 k-1 k1都是 1 1 1
+ 1 +1 +1变成补码后, 第 k k k位是 1 1 1, 其他位正好与 x x x取反, 证明完毕
c x c_x cx要表示的数是 ∑ i = x − l o w b i t ( x ) + 1 x a i \sum_{i=x-lowbit(x)+1}^x{a_i} i=xlowbit(x)+1xai, 也就是从 c x c_x cx向前数 l o w b i t ( x ) lowbit(x) lowbit(x)个数
的组成1
的组成2

2.2 修改

从图中可以找出以下规律: i + l o w b i t ( i ) i + lowbit(i) i+lowbit(i)正好是 i i i的父节点, 所以我们修改 a i a_i ai, 所有 i i i的祖先都要进行修改, 就可以顺着 i i i一直往上找到根节点进行修改

void update(int x, int val){  //x是节点编号, val是要修改的值
	for(;x <= n;x += lowbit(x))
		c[x] += val;
}

2.3 查询

我们要进行查询 ∑ i = 1 7 a i \sum_{i=1}^7{a_i} i=17ai的值, 发现 s u m 7 = c 7 + c 6 + c 4 sum_7 = c_7 + c_6 + c_4 sum7=c7+c6+c4, 而 7    6    4 7\;6\;4 764这三个数的二进制为 111      110      100 111\;\;110\;\;100 111110100, 7 − l o w b i t ( 7 ) = 6 7 - lowbit(7) = 6 7lowbit(7)=6, 6 − l o w b i t ( 6 ) = 4 6 - lowbit(6) = 4 6lowbit(6)=4, 所以只需要每次减去 l o w b i t ( i ) lowbit(i) lowbit(i)求和, 就可以得出结果

int query(int x){  //查询a[1]到a[x]的和
	int res = 0;
	for(;x;x -= lowbit(x))
		res += c[x];
	return res;
}

3 例题

3.1 逆序对

3.1.1 题目思路

求逆序对, 也就是求前面的数比后面的数大, 可以每输入一个数 x x x就查询比 x x x小的数累加到答案, 1 ≤ a i ≤ 1 0 9 1 \le a_i \le 10^9 1ai109, 要用到离散化

3.1.2 AC代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <map>
using namespace std;

const int N = le6;
long long a[N],b[N],c[N],ans;
int n, x;
map<int, int> mp;

int lowbit(int);
void update(int, int);
long long query(int);

int lowbit(int x){
	return x & (-x);
}
void update(int x){
	//把a[x]增加1
	while(x <=n){
	c[x]++;
	x += 1owbit(x);
}
long long query(int x){
	// 求a[1] + a[2] + ... + a[x]
	long long res = 0;
	while(x >0){
		res += c[x];
		x -= 1owbit(x);
	}
	return res;
}
int main(){
	scanf("%d", &n);
	for(int i = 1;i <= n;i++){
		scanf("%11d", &a[i]);
		b[i] = a[i];
	}
	sort(b+1, b+n+1);
	for(int i = 1;i <= n;i++){
		mp[b[i]] = i;
	}
	for(int i = 1;i <= n;i++){
		x = mp[a[i]];
		update(x);
		ans += i - query(x);
	}
	printf("%11d\n", ans);
	return 0;
}

3.2 BIT-2

3.2.1 题目大意

给定有 n n n个元素的数组 a a a, 进行 q q q次操作, 有两种操作:
1 l r k : 将 a l a_l al a r a_r ar每个数 + k +k +k
2 k : 输出 a k a_k ak

3.2.2 题目思路

用差分思想, c c c表示差分数组, u p d a t e update update时把 c l c_l cl c r c_r cr k k k, q u e r y query query时计算 c k c_k ck的前缀和

3.2.3 AC代码

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;

const int N = 1e6 + 5;
int n, q;
ll c[N];

int lowbit(int x){
	return x & (-x);
}
void update(int l, int r, int k){
	for(;l <= n;l += lowbit(l))
		c[l] += k;
	for(r++;r <= n;r += lowbit(r))
		c[r] -= k;
}
ll query(int k){
	ll res = 0;
	for(;k != 0;k -= lowbit(k))
		res += c[k];
	return res;
}

int main(){
	scanf("%d %d", &n, &q);
	int t;
	for(int i = 1;i <= n;i++){
		scanf("%d", &t);
		update(i, i, t);
	}
	int op, l, r, k;
	while(q--){
		scanf("%d", &op);
		if(op == 1){
			scanf("%d %d %d", &l, &r, &k);
			update(l, r, k);
		}
		else if(op == 2){
			scanf("%d", &k);
			if(k == 0)	printf("0\n");
			else	printf("%lld\n", query(k));
		}
	}
	return 0;
}

3.3 BIT-3

3.3.1 题目大意

给定有 n n n个元素的数组 a a a, 进行 q q q次操作, 有两种操作:
1 l r k : 将 a l a_l al a r a_r ar每个数 + k +k +k
2 l r : 输出 a l a_l al a r a_r ar的和

3.3.2 题目思路

∑ i = l r a i = ∑ i = l r ∑ j = 1 i c i \sum_{i=l}^r{a_i} = \sum_{i=l}^r \sum_{j=1}^i{c_i} i=lrai=i=lrj=1ici
∵ a 1 = c 1 , \because a_1 = c_1, a1=c1,
a 2 = c 1 + c 2 , a_2 = c_1 + c_2, a2=c1+c2,
a 3 = c 1 + c 2 + c 3 , a_3 = c_1 + c_2 + c_3, a3=c1+c2+c3,
a 4 = c 1 + c 2 + c 3 + c 4 , a_4 = c_1 + c_2 + c_3 + c_4, a4=c1+c2+c3+c4,
⋯   ⋯ \cdots~\cdots  
∴ ∑ i = l r ∑ j = 1 i c i = ∑ i = l r c i ∗ ( l − i + 1 ) = ( l + 1 ) ∗ ∑ i = l r c i − ∑ i = l r c i ∗ i \therefore \sum_{i=l}^r \sum_{j=1}^i{c_i} = \sum_{i=l}^r{c_i * (l-i+1)} = (l + 1) * \sum_{i=l}^r{c_i} - \sum_{i=l}^r{c_i * i} i=lrj=1ici=i=lrci(li+1)=(l+1)i=lrcii=lrcii
所以我们只需要维护两个数组 c i c_i ci s u m i = c i ∗ i sum_i = c_i * i sumi=cii

3.3.3 AC代码

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;  //等效于 define ll long long

const int N = 1e6 + 5;
int n, q;
ll c[N], sum[N];

int lowbit(int x){
	return x & (-x);
}
void update(int l, int r, int x){
	for(int i = l;i <= n;i += lowbit(i)){
		c[i] += x;
		sum[i] += x * l;
	}
	for(int i = r + 1;i <= n;i += lowbit(i)){
		c[i] -= x;
		sum[i] -= x * (r+1);
	}
}
ll query(int l, int r){
	ll res1 = 0, res2 = 0;
	for(int i = l;i != 0;i -= lowbit(i)){
		res1 += c[i] * l;
		res2 += sum[i];
	}
	for(int i = r + 1;i != 0;i -= lowbit(i)){
	    res1 += c[i] * (r + 1);
		res2 += sum[i];
	}
	return res1 - res2;
}

int main(){
	scanf("%d %d", &n, &q);
	int t;
	for(int i = 1;i <= n;i++){
		scanf("%d", &t);
		update(i, i, t);
	}
	int op, l, r, k;
	while(q--){
		scanf("%d", &op);
		if(op == 1){
			scanf("%d %d %d", &l, &r, &k);
			update(l, r, k);
		}
		else if(op == 2){
			scanf("%d %d", &l, &r);
			printf("%lld\n", query(l, r));
		}
	}
	return 0;
}

3.4 其他例题

火柴排队
和逆序对差别不大

虔诚的墓主人
比较难了

感谢观看
此篇文章为可达鸭Y1第5和第6节课介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值