C++树状数组

一个问题

(摘自BIT-1)
时间限制:1秒 内存限制:128M
题目描述
给定数组 a 1 , a 2 . . . a n a_1,a_2...a_n a1,a2...an,进行 m m m次操作,操作有两种:
1 i x:将​​ a i a_i ai加上 x x x
2 l r:求 a l + . . . + a r a_l + ... + a_r al+...+ar
输入描述
第一行:输入两个数 n,m,表示给定数组的长度和操作数
第二行:输入n个数,表示长度为n的给定数组
接下来q行:每行输入3个数字,表示如题的某一种操作
输出描述
对于每个2操作,输出对应结果
输入样例

3 2
1 2 3
1 2 1
2 1 3

输出样例

7

数据描述
1 ≤ n , m ≤ 1 0 6 1 \leq n,m \leq 10^6 1n,m106
∣ a i ∣ ≤ 1 0 6 |a_i| \leq 10^6 ai106
1 ≤ l , r , i ≤ n 1 \leq l,r,i \leq n 1l,r,in
∣ x ∣ ≤ 1 0 6 |x| \leq 10^6 x106

这道题乍一看是一个很普通的求前缀和的问题,但是,前缀和的修改操作却是 O ( n ) O(n) O(n)级别,显然超时…
难道就没有其他的办法了吗?

树状数组

树状数组是一个类似树的数组,可以很快地完成数组的修改操作,以及前缀和的查找

时空复杂度

树状数组的查找前缀和的时间复杂度与修改单点的时间复杂度都为 O ( log ⁡ 2 n ) O(\log_{2}{n}) O(log2n)
空间复杂度很明显,为 O ( n ) O(n) O(n)

树状数组的结构

刚开始我们有一个输入进去的 a 1 − a n a_1-a_n a1an,如下图:
2
同学们就问了,这哪里来构建树状数组,这不是树吗?
我们可以这样看:
3
而树状数组( c n c_n cn)在这棵树中是这样的:
4
c i c_i ci分别记录了他自己的叶子结点中所有 a i a_i ai的值之和

l o w b i t ( ) lowbit() lowbit()

l o w b i t ( ) lowbit() lowbit() 是求一个数在二进制中最低位‘1’所代表的数值
举个例子
l o w b i t ( 6 ) = 2 lowbit(6)=2 lowbit(6)=2,因为 6 6 6的二进制中的表示为 110 110 110,最低位的’1’代表的是 2 2 2
l o w b i t ( 8 ) = 8 lowbit(8)=8 lowbit(8)=8,因为 6 6 6的二进制中的表示为 1000 1000 1000,最低位的’1’代表的是 8 8 8
代码实现:

# define lowbit(x) ((x) & (-(x)))
// define版本
int lowbit(int x) { return x & (-x); }
// 函数版本

树状数组单点修改

观察下面这些数的下标:
c [ 1 ] = a [ 1 ] c[1]=a[1] c[1]=a[1]
c [ 2 ] = a [ 1 ] + a [ 2 ] c[2]=a[1]+a[2] c[2]=a[1]+a[2]
c [ 3 ] = a [ 3 ] c[3]=a[3] c[3]=a[3]
c [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] c[4]=a[1]+a[2]+a[3]+a[4] c[4]=a[1]+a[2]+a[3]+a[4]
c [ 5 ] = a [ 5 ] c[5]=a[5] c[5]=a[5]
c [ 6 ] = a [ 5 ] + a [ 6 ] c[6]=a[5]+a[6] c[6]=a[5]+a[6]
c [ 7 ] = a [ 7 ] c[7]=a[7] c[7]=a[7]
c [ 8 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] + a [ 8 ] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]

假如我们修改 a [ 3 ] a[3] a[3]这个点
我们就得去修改 c [ 3 ] , c [ 4 ] , c [ 8 ] c[3],c[4],c[8] c[3],c[4],c[8]的值
而下标 3   4   8 3\ 4\ 8 3 4 8的关系如下:
3 ( 0011 ) → l o w b i t ( 3 ) 4 ( 0100 ) → l o w b i t ( 4 ) 8 ( 1000 ) 3(0011) \xrightarrow{ lowbit(3)} 4(0100)\xrightarrow{lowbit(4)} 8(1000) 3(0011)lowbit(3) 4(0100)lowbit(4) 8(1000)

我们会发现一个有趣的问题:

如果修改 a [ x ] a[x] a[x],就得修改 a [ x ] , a [ x + l o w b i t ( x ) ] , . . . , a [ i ≤ n ] a[x],a[x+lowbit(x)],...,a[i \leq n] a[x],a[x+lowbit(x)],...,a[in]

是不是所有的单点修改都是这样的?
让我们再来举几个例子:

修改 a [ 7 ] a[7] a[7],就得修改 c [ 7 ] , c [ 8 ] c[7],c[8] c[7],c[8]
7 ( 111 ) → l o w b i t ( 7 ) 8 ( 1000 ) 7(111)\xrightarrow{lowbit(7)}8(1000) 7(111)lowbit(7) 8(1000)

修改 a [ 6 ] a[6] a[6],就得修改 a [ 6 ] , a [ 8 ] a[6],a[8] a[6],a[8]
6 ( 110 ) → l o w b i t ( 6 ) 8 ( 1000 ) 6(110)\xrightarrow{lowbit(6)}8(1000) 6(110)lowbit(6) 8(1000)

所以修改 a [ x ] a[x] a[x],就得修改 a [ x ] , a [ x + l o w b i t ( x ) ] , . . . , a [ i ≤ n ] a[x],a[x+lowbit(x)],...,a[i \leq n] a[x],a[x+lowbit(x)],...,a[in]

既然全部符合这个规律,那么就可以根据这一点来进行单点修改操作了

单点修改代码实现

# define lowbit(x) ((x) & (-(x)))
void update(int x, int y) {	// x为要修改的下标,y为要增加的值
	for(int i = x; i <= n; i += lowbit(i))
		c[i] += y;
}

树状数组区间查询

还是观察下面这些数的下标:
c [ 1 ] = a [ 1 ] c[1]=a[1] c[1]=a[1]
c [ 2 ] = a [ 1 ] + a [ 2 ] c[2]=a[1]+a[2] c[2]=a[1]+a[2]
c [ 3 ] = a [ 3 ] c[3]=a[3] c[3]=a[3]
c [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] c[4]=a[1]+a[2]+a[3]+a[4] c[4]=a[1]+a[2]+a[3]+a[4]
c [ 5 ] = a [ 5 ] c[5]=a[5] c[5]=a[5]
c [ 6 ] = a [ 5 ] + a [ 6 ] c[6]=a[5]+a[6] c[6]=a[5]+a[6]
c [ 7 ] = a [ 7 ] c[7]=a[7] c[7]=a[7]
c [ 8 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] + a [ 8 ] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]

如果我们要查询 ∑ i = 1 n = 7 a i \sum_{i= 1}^{n = 7}a_i i=1n=7ai
只需要求出 c [ 7 ] , c [ 6 ] , c [ 4 ] c[7],c[6],c[4] c[7],c[6],c[4]的和就可以了,而他们的下标变化正好与修改相反:
7 ( 111 ) → l o w b i t ( 7 ) 6 ( 110 ) → l o w b i i t ( 6 ) 4 ( 100 ) 7(111)\xrightarrow{lowbit(7)}6(110)\xrightarrow{lowbiit(6)}4(100) 7(111)lowbit(7) 6(110)lowbiit(6) 4(100)

区间查询代码实现

# define lowbit(x) ((x) & (-(x)))
int getsum(int x){
	int ans = 0;
	for(int i = x; i; i -= lowbit(x))
		ans += c[i];
	return ans;
}

问题的解决

让我们返回上面的问题,树状数组正好能够解决此问题
(数据很大,记得用scanf printf~,数组记得开longlong~)
C o d e Code Code

# include <cstdio>
# include <cstring>
# include <cmath>
# include <algorithm>
# include <vector>
# include <iostream>
# include <string>
# include <set>
# include <unordered_set>
# include <map>
# include <unordered_map>
# include <stack>
# include <queue>
# include <deque>
# define pf push_front
# define pb push_back
# define ppf pop_front()
# define ppb pop_back()
# define mp make_pair
# define p_a first
# define p_b second
# define PI acos(-1, 0)
# define LF putchar('\n')
# define SP putchar(' ')
# define repi(a, b) for(int i = (a); i <= (b); i++)
# define repi_(a, b) for(int i = (a); i >= (b); i--)
# define repj(a, b) for(int j = (a); j <= (b); j++)
# define repj_(a, b) for(int j = (a); j >= (b); j--)
# define repk(a, b) for(int k = (a); k <= (b); k++)
# define repk_(a, b) for(int k = (a); k >= (b); k--)
# define WW(x) while((x)--)
# define paix(a, n) sort((a) + 1, (a) + 1 + (n));
# define BUG(z) cout << endl << (z) << endl;
# define lowbit(z) ((z) & (-z))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef unsigned int ui;
typedef pair<int, int> pii;
const int N1 = 1e6 + 5, N2 = 1e3 + 5, M1 = 1e9 + 7, M2 = 998244353, INF1 = 0x7fffffff, INF2 = 0x3f3f3f3f;
const ll INF = 9223372036854775807;
template <typename T>
void read(T& x) { x = 0; char ch = getchar(); ll f = 1; while (!isdigit(ch)) { if (ch == '-') f *= -1; ch = getchar(); } while (isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); } x *= f; }
template <typename T, typename... Args>
void read(T& first, Args&... args) { read(first); read(args...); }
template <typename T>
void write(T arg) { T x = arg; if (x < 0) { putchar('-'); x = -x; } if (x > 9) write(x / 10); putchar(x % 10 + '0'); }
template <typename T, typename... Ts>
void write(T arg, Ts... args) { write(arg); if (sizeof...(args) != 0) { putchar(' '); write(args...); } }
void CLOSE() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); }
int MAXn(int a, int b) { return b > a ? b : a; }
int MINn(int a, int b) { return b < a ? b : a; }



// ————————————————————分割线~~~ 
int n, m;
ll a[N1], c[N1];
void update(int x, ll y) {
	for(int i = x; i <= n; i += lowbit(i))
		c[i] += y;
}
ll getsum(int x) {
	ll ans = 0;
	for(int i = x; i; i -= lowbit(i))
		ans += c[i];
	return ans;
}
signed main() {
	/*
    freopen("", "r", stdin);
    freopen("", "w", stdout);
    */
    scanf("%d%d", &n, &m);
    repi(1, n) {
    	scanf("%lld", &a[i]);
    	update(i, a[i]);
	} WW(m) {
		int ch, x, y;
		scanf("%d%d%d", &ch, &x, &y);
		if (ch == 1) update(x, ll(y));
		else printf("%lld\n", getsum(y) - getsum(x - 1));
	}
    /*
    fclose(stdin);
    fclose(stdout);
    */
	return 0;
}

总结

树状数组是一种非常好用的解决问题的方式,也有诸多的变形,本篇博客中并未提及,希望大家能够灵活地使用树状数组,解决更多的问题。
感谢您的观看
希望这篇博客能够帮助到您!


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值