前言
树状数组(英语: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
1⩽n⩽5×1051⩽p+q⩽5×105
常规方法
一般,定义一个数组,单点修改时间复杂度为 O ( 1 ) O(1) O(1) ,但是求第 x x x 到 y y y 的数之和的时间复杂度为 O ( ∣ y − x ∣ ) O(\left\vert y-x\right\vert) O(∣y−x∣) , 而最坏情况下区间查询时间复杂度为 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 就会长成这个样子:
可以看到一下规律:
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=i−2k+1∑iaj(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
2→10 ∴lowbit(2)=2
6
→
110
∴
l
o
w
b
i
t
(
6
)
=
2
\quad \quad 6 \to 110 \ \ \ \ \ \ \therefore lowbit(6)=2
6→110 ∴lowbit(6)=2
8
→
1010
∴
l
o
w
b
i
t
(
8
)
=
8
\quad \quad 8 \to 1010\ \ \ \ \therefore lowbit(8)=8
8→1010 ∴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[0111−0001](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[0110−0010](c[0100]);→c[2]
a n s + = c [ 0100 − 0100 ] ( c [ 0 ] 无 意 义 , 结 束 ) ] ans += c[0100-0100] (c[0]无意义,结束)] ans+=c[0100−0100](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[i−1](a[0]=0) ,则
a
[
i
]
=
∑
j
=
1
i
c
[
j
]
a[i]=\sum\limits^i_{j=1}c[j]
a[i]=j=1∑ic[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 1≤N,M≤5×105,1≤x,y≤n,保证任意时刻序列中任意元素的绝对值都不大于 2 30 2^{30} 230 。
解答
看到区间修改,一定就会使用差分数组,但是后面的取间查询,就有那么亿点点复杂了……
基于上一个问题,我们可以得出位置为 1 ∼ p 1\sim p 1∼p 的所有元素之和,即位置 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=1∑pa[i]=i=1∑pj=1∑ic[j]
观察式子
∑
i
=
1
p
∑
j
=
1
i
c
[
j
]
\sum\limits_{i=1}^{p}\sum\limits_{j=1}^{i}c[j]
i=1∑pj=1∑ic[j],可以发现
c
[
1
]
c[1]
c[1] 加了
p
p
p 次,
c
[
2
]
c[2]
c[2] 加了
p
−
1
p-1
p−1 次
…
…
\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=1∑pj=1∑ic[j]=i=1∑pc[i]×(p−i+1)=(p+1)i=1∑pc[i]−i=1∑pc[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] 计数问题