树状数组:
树状数组是一种类似简化版线段树的数据结构,它可以求区间和,他比线段树好在更容易实现。
树状数组的实现的想法:
树状数组的实现如图形所示:
A数组是要放入树状数组的数组,C数组就是树状数组,C数组的值是由A数组的部分值的和。
由图可知:
C[1] = A[1];
C[2] = A[1] + A[2];
C[3] = A[3];
C[4] = A[1] + A[2] + A[3] + A[4];
C[5] = A[5];
C[5] = A[5] + A[6];
C[7] = C[7];
C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];
我们把数字变成二进制的话在看:
C[0001] = A[0001];
C[0010] = A[0001] + A[0010];
C[0011] = A[0011];
C[0100] = A[0001] + A[0010] + A[0011] + A[0100];
C[0101] = A[0101];
C[0110] = A[0101] + A[0110];
C[0111] = C[0111];
C[1000] = A[0001] + A[0010] + A[0011] + A[0100] + A[0101] + A[0110] + A[0111] + A[1000];
我们可以发现
C[i] = A[i - 2^k + 1] +…+A[i],其中k是i二进制最低位对应的幂数;
这样一种算的方法使得求1~x的区间和的便于实现;
例如1~3区间:
sum = C[3] + C[2] ;
二进制:
sum = C[0011] + C[0010];
1~5区间:
sum = C[5] + c[4];
二进制:
sum = C[0101] + C[0100];
1~7区间:
sum = C[7] + C[6] +C[4];
二进制:
sum = C[0111] + C[0110] + C[0100];
相信看了这几个例子,你一定看出了一些规律,求和的时候求的都是下标为x和x按位置先后减掉一部分1(二进制)的数的和。
那么我知道这一点之后,我们就只需要找到一个比较简单的实现求当前最低位的1的方法就行了
此时我们就要使用到lowbit函数
int lowbit(int x){
return x&-x;
}
lowbit(x)就是x最低位的1所对应的数。
我们来看一下原理(以下全部由二进制表示)
随意举一个数x
x =
+
110100
+110100
+110100
原码:
0110100
0110100
0110100
反码:
0110100
0110100
0110100
补码:
0110100
0110100
0110100
-x =
−
110100
-110100
−110100
原码:
1110100
1110100
1110100
反码:
1001011
1001011
1001011
1001011
\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,1001011
1001011
+
0000001
\,\,\,\,\,\,\,\,\,\,\,\,\,\,+\,\,\,\,\,\,\,0000001
+0000001
—
—
—
—
\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,————
————
补码:
1001100
\,\,\,\,\,\,\,\,\,\,\,\,\, 1001100
1001100
0110100
\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,0110100
0110100
&
1001100
\,\,\,\,\,\,\,\,\,\,\,\,\,\,\&\,\,\,\,\,\,\,1001100
&1001100
—
—
—
—
\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,————
————
x&-x:
0000100
\,\,\,\,\,\,\,\,\,\,\,\,\,\,0000100
0000100
显然100是x=+110100中最低位对应的数。
其实很好想,x为正数时,x补码就等于原码,-x的补码是按位取反+1,很容易知道按位取反+1,这个1会进位,进到加1前的第一个0的位置停止,我们很容易知道这个0所在的pos位置就是x的补码中的第一个1的位置,且此时x和-x的补码在除了pos位置都为1,其余位置都不相等。故x&-x为x最低位的1对应的数。
树状数组实现代码:
树状数组中单点修改(也可理解为插入)
void Insert(int value,int pos){
while(pos <= N){//插入一个点就更新所有包含此点的数组
C[pos] += value;
pos += lowbit(pos);
}
}
求区间1~x的区间和
int interval_sum(int x){
int ans = 0;
while(x){
ans += C[x];
x -= lowbit(x);
}
return ans;
}
我们之前提到的区间[l,r]求和
sum = interval_sum(r) - interval_sum(l - 1);
很明显地,这只能进行单点修改,区间修改无法进行(此处说的无法进行,是在作为非直接循环单点修改的暴力方法不能作为方法的前提下)
我们要快速修改区间不得不用到差分数组与树状数组结合——差分树状数组
差分树状数组
1)差分数组
我们想找出一个数组的d和我们已有的数组a满足
a
[
i
]
=
∑
i
=
1
n
d
[
i
]
a[i] = \sum_{i=1}^nd[i]
a[i]=∑i=1nd[i],
此数组d就是差分数组。
很显然a[i] - a[i-1] = d[i];
当然数组还原只需要a[i] = a[i - 1] + d[i];
可能有人会问差分数组有什么好处吗?
有的,区间多次修改并且只问最终查询时,十分有用。
例如我们有一个对[l,r]的区间修改,修改值为v,那么d[l]+=v,d[r+1]+=-v即可.
原
差
分
数
组
d
原差分数组d
原差分数组d
d
[
1
]
,
.
.
.
,
d
[
l
]
,
.
.
.
,
d
[
r
]
,
d
[
r
+
1
]
,
.
.
d
[
n
]
d[1],...,d[l],...,d[r],d[r+1],..d[n]
d[1],...,d[l],...,d[r],d[r+1],..d[n]
原
数
组
a
原数组a
原数组a
a
[
1
]
=
d
[
1
]
a[1] = d[1]
a[1]=d[1]
a
[
2
]
=
d
[
1
]
+
d
[
2
]
a[2] = d[1] + d[2]
a[2]=d[1]+d[2]
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
a
[
l
]
=
d
[
1
]
+
.
.
.
+
d
[
l
]
a[l] = d[1] + ... +d[l]
a[l]=d[1]+...+d[l]
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
a
[
r
]
=
d
[
1
]
+
.
.
.
+
d
[
r
]
a[r] = d[1] +...+d[r]
a[r]=d[1]+...+d[r]
a
[
r
+
1
]
=
d
[
1
]
+
.
.
.
+
d
[
r
+
1
]
a[r+1] = d[1] +...+d[r+1]
a[r+1]=d[1]+...+d[r+1]
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
a
[
n
]
=
a
[
1
]
+
.
.
.
+
a
[
n
]
a[n] = a[1] +... +a[n]
a[n]=a[1]+...+a[n]
\,
修
改
后
的
分
数
组
d
修改后的分数组d
修改后的分数组d
d
[
1
]
,
.
.
.
,
d
[
l
]
+
v
,
.
.
.
,
d
[
r
]
,
d
[
r
+
1
]
+
(
−
v
)
,
.
.
d
[
n
]
d[1],...,d[l]+v,...,d[r],d[r+1]+(-v),..d[n]
d[1],...,d[l]+v,...,d[r],d[r+1]+(−v),..d[n]
修
改
后
的
数
组
a
修改后的数组a
修改后的数组a
a
[
1
]
=
d
[
1
]
a[1] = d[1]
a[1]=d[1]
a
[
2
]
=
d
[
1
]
+
d
[
2
]
a[2] = d[1] + d[2]
a[2]=d[1]+d[2]
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
a
[
l
]
=
d
[
1
]
+
.
.
.
+
d
[
l
−
1
]
+
d
[
l
]
+
v
a[l] = d[1] + ... +d[l-1]+d[l]+v
a[l]=d[1]+...+d[l−1]+d[l]+v
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
a
[
r
]
=
d
[
1
]
+
.
.
.
+
d
[
l
−
1
]
+
(
d
[
l
]
+
v
)
+
d
[
l
+
1
]
.
.
.
+
d
[
r
]
a[r] = d[1] +...+d[l-1]+(d[l]+v) +d[l+1]...+d[r]
a[r]=d[1]+...+d[l−1]+(d[l]+v)+d[l+1]...+d[r]
a
[
r
+
1
]
=
d
[
1
]
+
.
.
.
+
d
[
l
−
1
]
+
(
d
[
l
]
+
v
)
+
d
[
l
+
1
]
.
.
.
+
d
[
r
]
+
d
[
r
+
1
]
−
v
=
a[r+1] = d[1] +...+d[l-1]+(d[l]+v)+d[l+1]...+d[r]+d[r+1]-v=
a[r+1]=d[1]+...+d[l−1]+(d[l]+v)+d[l+1]...+d[r]+d[r+1]−v=
d
[
1
]
+
.
.
.
+
d
[
l
−
1
]
+
d
[
l
]
+
d
[
l
+
1
]
.
.
.
+
d
[
r
]
+
d
[
r
+
1
]
d[1] +...+d[l-1]+d[l]+d[l+1]...+d[r]+d[r+1]
d[1]+...+d[l−1]+d[l]+d[l+1]...+d[r]+d[r+1]
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
.
\,\,\,\,\,\,\,\,\,\,\,\,\,.
.
a
[
n
]
=
a
[
1
]
+
.
.
.
+
a
[
n
]
a[n] = a[1] +... +a[n]
a[n]=a[1]+...+a[n]
为什么呢,因为我们知道a[i] = a[i - 1] + d[i],所有在i以及i后的d[j]都会加上v.
所以在l处加v,r+1处加-v,可以控制仅在l,r区间内所有值加了v。用差分就可以O(1)修改,O(n)查询,当修改次数较查询次数多很多的时候,此方法很不错。
我们如果把差分数组应用到树状数组会怎样呢
此时我们把数组d插入树状数组,
我们求和的算式也会改变。
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
.
.
.
+
a
[
n
]
=
a[1] + a[2] + a[3]+...+ a[n]=
a[1]+a[2]+a[3]+...+a[n]=
d
[
1
]
+
d
[
1
]
+
d
[
2
]
+
d
[
1
]
+
d
[
2
]
+
d
[
3
]
+
.
.
.
+
d
[
1
]
+
d
[
2
]
+
d
[
3
]
+
.
.
.
d
[
n
]
=
d[1] + d[1] + d[2]+d[1]+d[2]+d[3]+...+d[1]+d[2]+d[3]+...d[n]=
d[1]+d[1]+d[2]+d[1]+d[2]+d[3]+...+d[1]+d[2]+d[3]+...d[n]=
n
(
d
[
1
]
+
d
[
2
]
+
d
[
3
]
+
.
.
+
d
[
n
]
)
−
[
(
d
[
2
]
+
d
[
3
]
+
.
.
d
[
n
]
)
+
(
d
[
3
]
+
.
.
d
[
n
]
)
+
(
d
[
4
]
+
.
.
d
[
n
]
)
+
.
.
.
.
.
.
+
(
0
)
]
=
n(d[1] +d[2]+d[3]+..+d[n])-[(d[2]+d[3]+..d[n])+(d[3]+..d[n])+(d[4]+..d[n])+......+(0)]=
n(d[1]+d[2]+d[3]+..+d[n])−[(d[2]+d[3]+..d[n])+(d[3]+..d[n])+(d[4]+..d[n])+......+(0)]=
n
(
∑
i
=
1
n
d
[
i
]
)
−
∑
i
=
1
n
d
[
i
]
∗
(
i
−
1
)
n(\sum_{i=1}^nd[i])-\sum_{i=1}^nd[i]*(i-1)
n(∑i=1nd[i])−∑i=1nd[i]∗(i−1)
我们此时只需在维护时,用一个数组维护
∑
i
=
1
n
d
[
i
]
\sum_{i=1}^nd[i]
∑i=1nd[i]另一个数组维护
∑
i
=
1
n
d
[
i
]
∗
(
i
−
1
)
\sum_{i=1}^nd[i]*(i-1)
∑i=1nd[i]∗(i−1)。
显然第一个数组就是最普通的树状数组,第二个我们定义sum数组,两个数组我们都在树状数组的操作中一起维护。
修改的代码:
区间修改的函数
void Interval_Updata(int value,int pos){
while(pos<=N){
c[i] += value;
sum[i] += value*(pos-1)
pos += lowbit(pos);
}
}
区间修改时的操作
Interval_Updata(v,l);
Interval_Updata(-v,r+1);
树状数组c[i]的修改还是和普通树状数组相同。
sum的求和式由于原本的式子是一个加法式,且对应pos的出现的次数即合并后的乘法的系数是一定的,所有sum的修改可以用每次修改value*(pos-1)就是,例如sum[i]每次修改都是改动加value*(i-1)
区间求和代码(和普通树状数组一致,求的是1~x的和):
int Interval_Sum(int pos){
int ans = 0,len = pos;
while(pos){
ans += len*c[pos] -sum[pos];
pos -= lowbit(pos);
}
return ans;
}
其余的操作差分树状数组和普通树状数组没有区别;
有了差分树状数组我们解决很多问题的时候可以不用线段树了,可以极大减少代码量,如果你用的电子版的板子就当我没说。