数据结构——树状数组

前言

树状数组(英语:Binary Indexed Tree),最早由Peter M. Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND EXPERIENCE。

解决问题

已知一个数列,你需要进行下面两种操作:

  • 单点修改:将某一个数加上 k k k
  • 区间求和:求出某区间每一个数的和

单点修改总共 p p p 次,区间查询总共 q q q 次,数列长度为 n n n
数据要求: 1 ⩽ n ⩽ 5 × 1 0 5 1 ⩽ p + q ⩽ 5 × 1 0 5 1 \leqslant n \leqslant 5\times10^5\qquad1 \leqslant p+q \leqslant 5\times10^5 1n5×1051p+q5×105

常规方法

一般,定义一个数组,单点修改时间复杂度为 O ( 1 ) O(1) O(1) ,但是求第 x x x y y y 的数之和的时间复杂度为 O ( ∣ y − x ∣ ) O(\left\vert y-x\right\vert) O(yx) , 而最坏情况下区间查询时间复杂度为 O ( n ) O(n) O(n) ,最后总的最坏情况下的时间复杂度就会达到 O ( ( p + q ) × n ) O((p+q)\times n) O((p+q)×n)

这时,聪明的你一定会想到前缀和数组,但不久你会发现虽然查询时间复杂度为 O ( 1 ) O(1) O(1) ,但是只要修改一个数,这个数后面的前缀和都要加,最坏的时候时间复杂度依然为 O ( n ) O(n) O(n) ,所以最后最坏情况下的时间复杂度还是 O ( ( p + q ) × n ) O((p+q)\times n) O((p+q)×n)

你会发现要么一个 O ( 1 ) O(1) O(1) ,一个 O ( N ) O(N) O(N) ;要么一个 O ( N ) O(N) O(N) ,一个 O ( 1 ) O(1) O(1) 。在题目没有说查询次数少,还是修改次数少时,这种方法显然行不通!!!

那,就没有一个折中的办法吗?当然有,那就是树状数组。

树状数组

如果用他解决问题,那么修改和查询的时间复杂度就会都为 O ( log ⁡ n ) O(\log{n}) O(logn),那么 5 × 1 0 5 5\times 10^5 5×105 的数据就冒的问题啦!

树状数组是一个复杂的东东,首先假设最开始的时候原来的数组 a a a 内的元素为 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 {1,2,3,4,5,6,7,8,9,10} 1,2,3,4,5,6,7,8,9,10 那么他的树状数组 c c c 就会长成这个样子:
图1
可以看到一下规律:
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]
c [ 9 ] = a [ 9 ] c[9]=a[9] c[9]=a[9]
c [ 10 ] = a [ 9 ] + a [ 10 ] c[10]=a[9]+a[10] c[10]=a[9]+a[10]
似乎奇数的其本身,偶数是前缀和,但放在 c [ 6 ] c[6] c[6] c [ 10 ] c[10] c[10] 那里有亿点不对……

其实规律如下:
c [ i ] = ∑ j   =   i   −   2 k   +   1 i a j ( 2 k = l o w b i t ( i ) ) c[i]=\sum\limits_{j\,=\,i\,-\,2^k\,+\,1}^{i}a_j (2^k=lowbit(i)) c[i]=j=i2k+1iaj(2k=lowbit(i))
那么问题又来了 l o w b i t ( ) lowbit() lowbit() 是个什么函数?
其实,将数 n n n 转化成二进制后最后一个 1 1 1 所代表的的量就是 l o w b i t ( n ) lowbit(n) lowbit(n) ,他在树状数组中起到了相当大的作用。
e . g . e.g. e.g.    2 → 10      ∴ l o w b i t ( 2 ) = 2 \;2 \to 10 \ \ \ \ \quad\therefore lowbit(2)=2 210    lowbit(2)=2
6 → 110        ∴ l o w b i t ( 6 ) = 2 \quad \quad 6 \to 110 \ \ \ \ \ \ \therefore lowbit(6)=2 6110      lowbit(6)=2
8 → 1010      ∴ l o w b i t ( 8 ) = 8 \quad \quad 8 \to 1010\ \ \ \ \therefore lowbit(8)=8 81010    lowbit(8)=8

在程序中需要手写,程序如下(自己理解):

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

单点修改

而如果改变 x x x 的值,就要加上自己的 l o w b i t lowbit lowbit函数 ,一直加到 n n n,这些节点都要加,比如一共有 8 8 8 个数,第 3 3 3 个数要加上 k k k,那么:

c [ 0011 ] + = k ; → c [ 3 ] c[0011] += k;\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\to c[3] c[0011]+=k;c[3]
c [ 0011 + 0001 ] ( c [ 0100 ] ) + = k ; → c [ 4 ] c[0011+0001] (c[0100]) += k;\quad\quad\quad\to c[4] c[0011+0001](c[0100])+=k;c[4]
c [ 0100 + 0100 ] ( c [ 1000 ] ) + = k ; → c [ 8 ] c[0100+0100] (c[1000]) += k;\quad\quad\quad\to c[8] c[0100+0100](c[1000])+=k;c[8]

这样就能维护树状数组。

代码如下:

int add(int x,int k)
{
    while(x <= n)
    {
    	c[x] += k;
    	x += lowbit(x);
    }
}

区间查询

就是前缀和,比如查询 x x x y y y 区间的和,那么就将从 1 1 1 y y y 的和减去从 1 1 1 x x x 的和。
1 1 1 y y y 的和求法是,将 y y y 转为二进制,然后一直减去 l o w b i t ( y ) lowbit(y) lowbit(y),一直到 0 0 0

比如求 1 1 1 7 7 7 的和

a n s + = c [ 0111 ] ; → c [ 7 ] ans += c[0111]; \quad\quad\quad\quad\quad\quad\quad\quad\quad\to c[7] ans+=c[0111];c[7]
a n s + = c [ 0111 − 0001 ] ( c [ 0110 ] ) ; → c [ 6 ] ans += c[0111-0001](c[0110]);\quad\quad\to c[6] ans+=c[01110001](c[0110]);c[6]
a n s + = c [ 0110 − 0010 ] ( c [ 0100 ] ) ; → c [ 2 ] ans += c[0110-0010](c[0100]);\quad\quad\to c[2] ans+=c[01100010](c[0100]);c[2]
a n s + = c [ 0100 − 0100 ] ( c [ 0 ] 无 意 义 , 结 束 ) ] ans += c[0100-0100] (c[0]无意义,结束)] ans+=c[01000100](c[0])]

代码如下:

int sum(int x)
{
	int t = 0;
	while(x > 0)
	{
		t += c[x]; 
		x -= lowbit(x);
	}
	return t;
}

单点修改,区间查询

最终到了处理环节。

相信有人已经发出了疑问:初始化呢?
其实,读入时将数加入 c c c 数组即可 ( a d d add add 操作 )。

好了,最后规定一下输入方式:
P3374 【模板】树状数组 1

上代码!

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5 * 1e5;
namespace New_Math
{
	int lowbit(int x)
	{
		return (x & -x);
	}
}

using namespace New_Math;

namespace Tree_Array
{
	int c[MAXN];
	int sum(int x)
    {
    	int t = 0;
    	while(x > 0)
    	{
    		t += c[x]; 
    		x -= lowbit(x);
    	}
    	return t;
    }
    int add(int x,int k,int n)
    {
    	while(x <= n)
    	{
    		c[x] += k;
    		x += lowbit(x);
    	}
    }
}

using namespace Tree_Array;

int main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i <= n;i++)
	{
		int opt; 
		scanf("%d",&opt);
		add(i,opt,n);
	}
	for(int i = 1;i <= m;i++)
	{
		int a,b,c;
		scanf("%d %d %d",&a,&b,&c);
		if(a == 1)
		{
			add(b,c,n);
		}
		else
		{
			cout << sum(c) - sum(b - 1) << endl;
		}
	}
	return 0;
}

区间修改,单点查询

题目见下:
传送门

相信大家都听说过差分数组,他的每一个元素代表着其数本身与前一个的差。设原数组为 a [ i ] a[i] a[i] , 设差分数组 c [ i ] = a [ i ] − a [ i − 1 ] ( a [ 0 ] = 0 ) c[i]=a[i]−a[i−1](a[0]=0) c[i]=a[i]a[i1](a[0]=0) ,则 a [ i ] = ∑ j = 1 i c [ j ] a[i]=\sum\limits^i_{j=1}c[j] a[i]=j=1ic[j],可以通过求 c [ i ] c[i] c[i] 的前缀和查询。
如果将区间 [ l , r ] [l,r] [l,r] 都增加 k k k ,那么可以将 c [ l ] + = k , c [ r + 1 ] − = k c[l] += k ,c[r + 1] -= k c[l]+=k,c[r+1]=k,而查询的时候求差分数组的前缀和即可。(不懂的可以自行查询:差分数组

所以只需修改最后的地方即可,上代码!

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5 * 1e5;
namespace New_Math
{
	int lowbit(int x)
	{
		return (x & -x);
	}
}

using namespace New_Math;

namespace Tree_Array
{
	int c[MAXN];
	int sum(int x)
    {
    	int t = 0;
    	while(x > 0)
    	{
    		t += c[x]; 
    		x -= lowbit(x);
    	}
    	return t;
    }
    int add(int x,int k,int n)
    {
    	while(x <= n)
    	{
    		c[x] += k;
    		x += lowbit(x);
    	}
    }
}

using namespace Tree_Array;

int main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i <= n;i++)
	{
		int opt; 
		scanf("%d",&opt);
		add(i,opt,n);
	}
	for(int i = 1;i <= m;i++)
	{
		int a,b,c;
		scanf("%d %d %d",&a,&b,&c);
		if(a == 1)
		{
			add(b,c,n);
		}
		else
		{
			cout << sum(c) - sum(b - 1) << endl;
		}
	}
	return 0;
}

区间修改,区间查询

题目如下

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  • 将某区间每一个数加上 x x x
  • 求出某区间每一个数的和。
输入格式

第一行包含两个整数 N N N M M M,分别表示该数列数字的个数和操作的总个数。

第二行包含 N N N 个用空格分隔的整数,其中第 i i i 个数字表示数列第 i i i 项的初始值。

接下来 M M M 行每行包含 3 3 3 4 4 4 个整数,表示一个操作,具体如下:

操作 1 1 1: 格式: 1    x    y    k 1\;x\;y\;k\quad 1xyk 含义:将区间 [ x , y ] [x,y] [x,y] 内每个数加上 k k k

操作 2 2 2: 格式: 2    x    y        2\;x\;y\;\;\;\quad 2xy 含义:输出区间 [ x , y ] [x,y] [x,y] 内每个数的和。

输出格式

输出包含若干行整数,即为所有操作 2 2 2 的结果。

数据规模与约定

对于 100 % 100\% 100% 的数据: 1 ≤ N , M ≤ 5 × 1 0 5 , 1 ≤ x , y ≤ n 1 \leq N, M\le 5\times 10^5,1 \leq x, y \leq n 1N,M5×1051x,yn,保证任意时刻序列中任意元素的绝对值都不大于 2 30 2^{30} 230

解答

看到区间修改,一定就会使用差分数组,但是后面的取间查询,就有那么亿点点复杂了……

基于上一个问题,我们可以得出位置为 1 ∼ p 1\sim p 1p 的所有元素之和,即位置 p p p 的前缀和:

∑ i = 1 p a [ i ] = ∑ i = 1 p ∑ j = 1 i c [ j ] \sum\limits^{p}_{i=1}a[i]=\sum\limits_{i=1}^{p}\sum\limits_{j=1}^{i}c[j] i=1pa[i]=i=1pj=1ic[j]

观察式子 ∑ i = 1 p ∑ j = 1 i c [ j ] \sum\limits_{i=1}^{p}\sum\limits_{j=1}^{i}c[j] i=1pj=1ic[j],可以发现 c [ 1 ] c[1] c[1] 加了 p p p 次, c [ 2 ] c[2] c[2] 加了 p − 1 p-1 p1 … … \dots\dots
所以,总结以下就是:

∑ i = 1 p ∑ j = 1 i c [ j ] = ∑ i = 1 p c [ i ] × ( p − i + 1 ) = ( p + 1 ) ∑ i = 1 p c [ i ] − ∑ i = 1 p c [ i ] × i \sum\limits_{i=1}^{p}\sum\limits_{j=1}^{i}c[j]=\sum\limits_{i=1}^{p}c[i] \times (p - i + 1)=(p+1)\sum\limits_{i=1}^{p}c[i]-\sum\limits_{i=1}^{p}c[i]\times i i=1pj=1ic[j]=i=1pc[i]×(pi+1)=(p+1)i=1pc[i]i=1pc[i]×i

那么,我们就用两个数组维护:
c 1 [ i ] = c [ i ] c1[i]=c[i] c1[i]=c[i]
c 2 [ i ] = c [ i ] × i c2[i]=c[i]\times i c2[i]=c[i]×i

所以将 a d d add add 函数和 s u m sum sum 函数稍做修改,再将第一题的查询和第二题的修改合并以下即可。上代码!

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5 * 1e5;
namespace New_Math
{
	int lowbit(int x)
	{
		return (x & -x);
	}
}

using namespace New_Math;

namespace Tree_Array
{
	int c1[MAXN];
	int c2[MAXN];
	int sum(int x)
	{
		int t = 0,l = x;
		while(x > 0)
		{
			t += (l + 1) * c1[x] - c2[x]; 
			x -= lowbit(x);
		}
		return t;
	}
    int add(int x,int k,int n)
	{
		int l = x;
		while(x <= n)
		{
			c1[x] += k;
			c2[x] += l * k;
			x += lowbit(x);
		}
	}
}

using namespace Tree_Array;

int main()
{
	int n,m,last = 0;
	scanf("%d %d",&n,&m);
	for(int i = 1;i <= n;i++)
	{
		int opt;
		scanf("%d",&opt);
		add(i,opt - last,n);
		last = opt;
	}
	for(int i = 1;i <= m;i++)
	{
		int a;
		scanf("%d",&a);
		if(a == 1)
		{
			int b,c,d;
			scanf("%d %d %d",&b,&c,&d);
			add(b,d,n);
			add(c + 1,-d,n);
		}
		else
		{
			int b,c;
			scanf("%d %d",&b,&c);
			cout << sum(c) - sum(b - 1) << endl;
		}
	}
	return 0;
}

习题

最后留下亿些习题:(洛谷题目,网站:洛谷)
P2068 统计和
P2880 [USACO07JAN] Balanced Lineup G
P1168 中位数
P2357 守墓人
P2345 [USACO04OPEN] MooFest G
P2982 [USACO10FEB]Slowing down G
P4054 [JSOI2009] 计数问题

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值