文章目录
引入
背景:
给定一个序列如何求出其中某个区间的值,例如
a[] = {1,2,3,5,4,8,9,6,3,4}
现在我想查询a[3] - a[7] 这个区间的值,很显然我们可以用一个循环做到,那么如果给定序列很大,想求的区间也很多,每次使用循环做,就会很慢。
前缀和
如何优化??,高中我们就学过数列的前n项和,我们可以利用这个思想,对于任何一个区间我们可以使用(注:S是前n项和) S7 - S2 即可求出 a[3] - a[7]这个区间的值,因为:
S7 = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7]
S2 = a[1] + a[2]
两式相减即可得到 a[3] ~ a[7] 的值,这就是前缀和的思想。
练习题
//参考代码
#include <iostream>
using namespace std;
const int N = 100010;
int f[N]; //前缀和数组
int main()
{
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
int a;
cin >> a;
f[i] += f[i - 1] + a; //每次等于前一项和 + 当前项
}
while(m --)
{
int l,r;
cin >> l >> r;
cout << f[r] - f[l - 1] << endl;
}
}
那么现在需求更改了:
1.数列a中的值可能会更改
2.快速查看某个区间的值
此时在使用之前的前缀和的方法就不行了,因为每次数列变动就会重修计算一次前缀和数组,每次时间复杂度是 O(n),如果有 m 次更改,复杂度就是 O(nm) 当 n 和 m 比较大的时候基本就会TLE,如果使用差分的话,可以 O(1) 完成操作1,但操作 2 需要 O(n),前缀和恰好相反。
树状数组
现在有一个退而求其次的做法 ——— 树状数组,可以以O(logn)的复杂度完成上述两个操作,大概就是使用一个数组,数组中的每个元素记录一段区间的和,在修改的我们只需要修改 logn 个区间,查询的时候,我们将对应的区间段加上就能得到结果。(正题开始)
原理
根据任意正整数关于2的不重复次幂的唯一分解性质(就是一个数可以用二进制表示),我们可以将一个正整数分解成:
x
=
2
i
1
+
2
i
2
+
.
.
.
+
2
i
m
x = 2^{i_1}+2^{i_2}+ ... +2^{i_m}
x=2i1+2i2+...+2im
不妨设:
i
1
>
i
2
>
.
.
.
>
i
m
{i_1}> {i_2}> ... > {i_m}
i1>i2>...>im
进一步我们可以将区间[1,x]分成 O(log x) 个小区间
1.长度为
2
i
1
2^{i_1}
2i1 的小区间
[
1
,
2
i
1
]
[1,2^{i_1}]
[1,2i1]
2.长度为
2
i
2
2^{i_2}
2i2 的小区间
[
2
i
1
+
1
,
2
i
1
+
2
i
2
]
[2^{i_1}+1, 2^{i_1} + 2^{i_2}]
[2i1+1,2i1+2i2]
3.长度为
2
i
3
2^{i_3}
2i3 的小区间
[
2
i
1
+
2
i
2
+
1
,
2
i
1
+
2
i
2
+
2
i
3
]
[2^{i_1} + 2^{i_2} + 1,2^{i_1} + 2^{i_2} + 2^{i_3}]
[2i1+2i2+1,2i1+2i2+2i3]
.
.
.
.
.
.
......
......
m 长度为
2
i
m
2^{i_m}
2im 的小区间
[
2
i
1
+
2
i
2
+
.
.
.
+
2
i
m
−
1
+
1
,
2
i
1
+
2
i
2
+
+
2
i
3
]
[2^{i_1} + 2^{i_2}+ ... + 2^{i_{m - 1}} + 1,2^{i_1} + 2^{i_2} ++ 2^{i_3}]
[2i1+2i2+...+2im−1+1,2i1+2i2++2i3]
是不是很一头雾水,我们来模拟一下就清楚了
x = 7 分解为:
2
2
+
2
1
+
2
0
2^2+2^1+2^0
22+21+20, 区间[1,7]可以分为[1,4],[5,6],[7,7]三个小区间。
我们将x = 7的二进制位表示出来:0111 我们发现上面三个区间长度就等于 ”二进制分解“ 下最小 2 的幂次。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mrLITWKG-1582810116827)(http://139.9.81.229:8090/upload/2020/2/image-9a249aea17e04f0391ae98523b47fef0.png)]
lowbit运算
这里顺便就要提到 lowbit 运算,它可以返回最低为的 1 及其该位后面的所有 0 组成的所有值,如有不懂,可以百度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-utdlfuJk-1582810116828)(http://139.9.81.229:8090/upload/2020/2/image-84aaaf37f1ad43df8250f8dc320121f4.png)]
lowbit代码实现
int lowbit(int x)
{
return x & -x;
}
有了这个函数加上前面根据二进位中的 1 来划分区间,给定任意一个整数 x ,我们可以使用下面这段代码划分成 O(logx) 个小区间:
while(x > 0)
{
printf("[%d, %d]\n",x - lowbit(x) + 1 ,x);
x -= lowbit(x);
}
具体思路
现在对于任意给定一个序列 a ,我们在建立一个数组c,其中c[x]保存序列 a 的区间 [x - lowbit(x) + 1,x] 中所有数的和
首先我们强调一下,树状数组的下标从 11 开始计数,这一点我们看到后面就会很清晰了。我们先了解如下的定义,请大家一定先记住一下性质:
1.数组 C 是一个对原始数组 A 的预处理数组。
2.每个内部节点c[x] 保存以它为根的子树中所有叶节点的和。
3.每个内部节点从c[x] 的子节点个数等于 lowbit(x)的位数。
4.除了树根以外,每个内部节点c[x] 的父节点是 c[x + lowbit(x)]
5.树的深度为 O(lonN)
上面的过程我们用如下的表来表示。
1. “单点更新”操作:“从子结点到父结点”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njvQEW9E-1582810116832)(http://139.9.81.229:8090/upload/2020/2/image-5ce7f1ce09834814bdef2edff8c6afdd.png)]
例1:修改
A
[
3
]
A[3]
A[3], 分析对数组
C
C
C 产生的变化。
从图中我们可以看出 A [ 3 ] A[3] A[3] 的父结点以及祖先结点依次是 C [ 3 ] C[3] C[3]、 C [ 4 ] C[4] C[4]、 C [ 8 ] C[8] C[8] ,所以修改了 A [ 3 ] A[3] A[3] 以后 C [ 3 ] C[3] C[3]、 C [ 4 ] C[4] C[4]、 C [ 8 ] C[8] C[8] 的值也要修改。
先看
C
[
3
]
C[3]
C[3]
l
o
w
b
i
t
(
3
)
=
1
,
3
+
l
o
w
b
i
t
(
3
)
=
4
lowbit(3)=1,3+lowbit(3)=4
lowbit(3)=1,3+lowbit(3)=4 就是
C
[
3
]
C[3]
C[3] 的父亲结点
C
[
4
]
C[4]
C[4] 的索引值。
再看
C
[
4
]
C[4]
C[4] ,
l
o
w
b
i
t
(
4
)
=
4
,
4
+
l
o
w
b
i
t
(
4
)
=
8
lowbit(4)=4,4+lowbit(4)=8
lowbit(4)=4,4+lowbit(4)=8 就是
C
[
4
]
C[4]
C[4] 的父亲结点
C
[
8
]
C[8]
C[8]的索引值。
从图中,也可以验证:“红色结点的索引值 + 右下角蓝色圆形结点的值 = 红色结点的双亲结点的索引值”,对应上面性质4。
分析到这里“单点更新”的代码就可以马上写出来了。
2.单点更新代码实现
void add(int x,int y) // x 是修改的点,y是加的值
{
for(;x <= N;x += lowbit(x))c[x] += y; //N 是序列的总点数
}
3.查询前缀和
查询前缀和,即 a 的 1 ~ x 的个数的和,按照刚刚的方法,把 [ 1 , x ] [1,x] [1,x]分成 O(logN) 个小区间,而每个小区间的区间和都保存在数组 c c c中,所以查询前缀就等于加上每个区间的值即可,代码如下:
int ask(int x)
{
int ans = 0; //记录和
for(;x ; x -= lowbit(x)) ans += c[x];
return ans;
}
4.区间查询
对于任意一个区间的和,我们只需要计算 ask® - ask(l -1)即可,l 是左端点,r 是右端点。
5.如何建树
为了简便,比较一般的初始化是:直接建立一个全为0的数组c,然后对每个位置 x 执行 add(x,a[x]) 就可以完成对原序列 a 构造树状数组的过程,时间复杂度 O(N logN)。
例题:
区间查询,单点修改
区间修改,单点查询
区间查询,区间修改
树状数组维护区间最值
树状数组求逆序对
树状数组求指定长度单调子序列
参考代码:
1.区间查询,单点修改
#include <iostream>
#include <algorithm>
#include <stdio.h>
using namespace std;
const int N = 5 * 1e5 + 10;
int c[N];
int n,m;
inline int lowbit(int x)
{
return x & -x;
}
int ask(int x)
{
int ans = 0;
for(;x;x -= lowbit(x)) ans += c[x];
return ans;
}
void add(int x,int t)
{
for(;x <= n; x += lowbit(x)) c[x] += t;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i ++)
{
int t;
cin >> t;
add(i,t); //初始化树
}
int a,b,c;
while(m --)
{
cin >> a >> b >> c;
if(a == 1)
add(b,c); //修改区间的某个值
else
printf("%d\n",ask(c) - ask(b - 1));//查询某个区间
}
}
2.区间修改,单点查询
//本题是树状数组 + 差分
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N],d[N];
int n,m;
inline int lowbit(int x)
{
return x & -x;
}
int ask(int x)
{
int ans = 0;
for(; x ; x -= lowbit(x)) ans += d[x];
return ans;
}
int add(int x,int c)
{
for(;x <= n; x += lowbit(x)) d[x] += c;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> a[i];
while(m --)
{
string op;
cin >> op;
if(op == "Q")
{
int x;
cin >> x;
cout << a[x] + ask(x) << endl;
}
else
{
int l,r,c;
cin >> l >> r >> c;
add(l,c);
add(r + 1, -c );
}
}
}
3.区间查询,区间修改
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N];
LL cs[2][N],sum[N];
int n,m;
inline int lowbit(int x)
{
return x & -x;
}
LL ask(int k,int x)
{
LL ans = 0;
for(; x ; x -= lowbit(x)) ans += cs[k][x];
return ans;
}
void add(int k,int x,int c)
{
for(; x <= n; x += lowbit(x)) cs[k][x] += c;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
sum[i] = sum[i - 1] + a[i];
}
while(m --)
{
string op;
int l,r,d;
cin >> op;
if(op == "Q")
{
cin >> l >> r;
LL ans = sum[r] + (r + 1) * ask(0,r) - ask(1,r);
ans -= sum[l - 1] + l * ask(0,l-1) - ask(1,l - 1);
printf("%lld\n",ans);
}
else
{
cin >> l >> r >> d;
add(0,l,d);
add(0,r + 1,-d);
add(1,l,l * d);
add(1,r + 1,-(r + 1) * d);
}
}
}