2020.4.9 腾讯实习面试面到了这个题 太久没接触已经忘了 答得也不是很好
一、问题引入
维护数组看起来是十分简单的事情。
修改某点的值只要有下标直接就能改了,那对于求某个区间的和,怎么做呢?
我们可以使用一个前缀和的方法
对原数组
a
[
1
]
a
[
2
]
a
[
3
]
.
.
.
a
[
n
]
a[1] a[2] a[3]... a[n]
a[1]a[2]a[3]...a[n]
我们创建一个前缀和数组s
s
[
1
]
=
a
[
1
]
,
s
[
2
]
=
a
[
1
]
+
a
[
2
]
,
s
[
3
]
=
a
[
1
]
+
a
[
2
]
+
a
[
3
]
,
.
.
.
s
[
n
]
=
s
[
1
]
+
s
[
2
]
+
.
.
.
s
[
n
]
s[1]=a[1],s[2]=a[1]+a[2],s[3]=a[1]+a[2]+a[3], ... s[n]=s[1]+s[2]+...s[n]
s[1]=a[1],s[2]=a[1]+a[2],s[3]=a[1]+a[2]+a[3],...s[n]=s[1]+s[2]+...s[n]
我们想要求区间l~r
的和可以使用s[r]-s[l-1]
来得到
假如我们想要单点修改呢?这样需要让该点之后的前缀和数组全部更新一遍!因此前缀和对于区间查询没有问题,对单点更新无法胜任。
二、树状树组来了
我们先来看看这究竟是个什么玩意。
A数组是原始n个数的数组,C数组就是是树状数组(“树状”数组,是指一个普通数组,按树状存储,而不是一种STL中的数据结构)。
C [ 1 ] = A [ 1 ] C [ 2 ] = C [ 1 ] + A [ 2 ] = A [ 1 ] + A [ 2 ] C [ 3 ] = A [ 3 ] C [ 4 ] = C [ 2 ] + C [ 3 ] + A [ 4 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] C [ 5 ] = A [ 5 ] C [ 6 ] = C [ 5 ] + A [ 6 ] = A [ 5 ] + A [ 6 ] C [ 7 ] = A [ 7 ] C [ 8 ] = C [ 4 ] + C [ 6 ] + C [ 7 ] + A [ 8 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] + A [ 5 ] + A [ 6 ] + A [ 7 ] + A [ 8 ] \begin{aligned} C[1] &= A[1] \\C[2] &= C[1] + A[2] = A[1] + A[2] \\C[3] &= A[3] \\C[4] &= C[2] + C[3] +A[4] = A[1] + A[2] + A[3] + A[4] \\C[5] &= A[5] \\C[6] &= C[5] + A[6] = A[5] + A[6] \\C[7] &= A[7] \\C[8] &= C[4] + C[6] + C[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8] \end{aligned} C[1]C[2]C[3]C[4]C[5]C[6]C[7]C[8]=A[1]=C[1]+A[2]=A[1]+A[2]=A[3]=C[2]+C[3]+A[4]=A[1]+A[2]+A[3]+A[4]=A[5]=C[5]+A[6]=A[5]+A[6]=A[7]=C[4]+C[6]+C[7]+A[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]
好像是有点规律,比如2的倍数后面跟着一大串的和。
那么这个k怎么求呢?我们要用到一个神奇的函数,请往下看
三、神奇的lowbit函数
l o w b i t ( x ) = x & ( − x ) = 2 k lowbit(x)=x\&(-x)=2^k lowbit(x)=x&(−x)=2k,其中k为x在二进制下末尾0的个数
int lowbit(int x)
{
return x&(-x);
}
//-t 代表t的负数 计算机中负数使用对应的正数的补码来表示
//例如 :
// t=6(0110) 此时 k=1
//-t=-6=(1001+1)=(1010)
// t&(-t)=(0010)=2=2^1
C
[
i
]
=
A
[
i
−
2
k
+
1
]
+
A
[
i
−
2
k
+
2
]
+
.
.
.
.
.
.
A
[
i
]
;
C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i];
C[i]=A[i−2k+1]+A[i−2k+2]+......A[i];
C
[
i
]
=
A
[
i
−
l
o
w
b
i
t
(
i
)
+
1
]
+
A
[
i
−
l
o
w
b
i
t
(
i
)
+
2
]
+
.
.
.
.
.
.
A
[
i
]
;
C[i]=A[i-lowbit(i)+1]+A[i-lowbit(i)+2]+......A[i];
C[i]=A[i−lowbit(i)+1]+A[i−lowbit(i)+2]+......A[i];
即把2^k都换成lowbit
四、树状数组的相关操作
getsum操作
我们想求 a [ 1 ] + a [ 2 ] + . . . + a [ x ] a[1]+a[2]+...+a[x] a[1]+a[2]+...+a[x]的和
int getsum(int x){
int ans=0;
for(int i = x; i ; i -= lowbit(i))//i要大于0
ans+=C[i];
return ans;
}
ok 下面利用C[i]数组,求A数组中前i项的和
举个例子 i=7;
sum[7]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7] ;
我们有
C[4]=A[1]+A[2]+A[3]+A[4]; C[6]=A[5]+A[6]; C[7]=A[7];
可以推出: sum[7]=C[4]+C[6]+C[7];
单点更新操作
当我们修改A数组中的某一个值时 应当如何更新C数组呢
回想一下区间查询的过程,再看一下上文中列出的图
当要动态改变一个数时,用刚刚的循环枚举出与它相关的位置,都增加(减少)即可。
牵一发而动全身!
void update(int k,int x){
for(int i = k;i <= n;i += lowbit(i))
C[i]+=x;
}
例如,当更新A[1]时,需要向上更新C[1], C[2], C[4], C[8]
写为二进制 C[(001)],C[(010)],C[(100)],C[(1000)]
1(001) C[1]+=A[1]
lowbit(1)=001 1+lowbit(1)=2(010) C[2]+=A[1]
lowbit(2)=010 2+lowbit(2)=4(100) C[4]+=A[1]
lowbit(4)=100 4+lowbit(4)=8(1000) C[8]+=A[1]
五、树状数组升级
区间修改
比如需要将x到y中每个数加上100,暴力O(N)。
怎么做呢?想想看
int i 循环x到y update(i,100) !!
那我要你树状数组何用???
但说明你已经开始掌握精髓了
引入一个概念 非常重要
差分
假设下标都从1开始。
设数组a[]={1,6,8,5,10}
,差分数组b[]={1,5,2,-3,5}
也就是说b[i]=a[i]-a[i-1]
,a[i]=b[1]+....+b[i]
(a相当于是b的前缀和)。
假如区间[2,4]都加上2的话
a数组变为a[]={1,8,10,7,10}
,b数组变为b[]={1,7,2,-3,3}
发现了没有,b数组只有b[2]和b[5]变了,因为区间[2,4]是同时加上2的,所以a数组该区间内相邻数的差值仍然不变。
所以对区间[x,y]进行修改,只用修改b[x]与b[y+1];
b[x]=b[x]+k;b[y+1]=b[y+1]-k;
六、例题
已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数数加上x
2.求出某一个数
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含2或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作2: 格式:2 x 含义:输出第x个数的值
输出格式:
输出包含若干行整数,即为所有操作2的结果。
输入输出样例
输入样例1:
5 5
1 5 4 2 3
1 2 4 2
2 3
1 1 5 -1
1 3 5 7
2 4
输出样例1:
6
10
这道题属于单点查询+区间修改,我们可以对原数组的差分数组建立一个树状数组,使用getsum来做单点查询,使用差分数组的性质来做区间修改
#include <bits/stdc++.h>
using namespace std;
#define lowbit(i) i&(-i)
int a[1000000];
int tree[1000000];
int N,M;
int getsum(int x){
int sum=0;
for(int i=x;i;i-=lowbit(i)){
sum+=tree[i];
}
return sum;
}
void insert(int x,int val){
for(int i=x;i<=N;i+=lowbit(i)){
tree[i]+=val;
}
}
void update(int l,int r,int val){
insert(l,val);//单点修改
insert(r+1,-val);//单点修改
}
int main(){
cin>>N>>M;
for(int i=1;i<=N;i++){
cin>>a[i];
insert(i,a[i]-a[i-1]);//对差分数组构建树状数组
}
for(int i=1;i<=M;i++){
int a,b,c,d;
cin>>a;
if(a==1){
cin>>b>>c>>d;
update(b,c,d);//b到c全部加d
}
else{
cin>>b;
printf("%d\n",getsum(b));
}
}
return 0;
}
需要区间求和呢?
我们需要额外一个数组来帮助我们~
例如,a是原数组,tree是我们的差分数组
我们有
a[1]=tree[1]
a[2]=tree[1]+tree[2]
a[3]=tree[1]+tree[2]+tree[3]
a[4]=tree[1]+tree[2]+tree[3]+tree[4]
类似的
a[1]+a[2]+……+a[r-1]+a[r]
//用上方公式推导得出
=tree[1]+(tree[1]+tree[2])+……+(tree[1]+……+tree[r])
//根据加法交换律与结合律:
=(tree[1]*(r))+(tree[2]*(r-1))+……(tree[r]*1)
//那么:
=r*(tree[1]+tree[2]+……+tree[r])-(tree[1]*0+tree[2]*1+……+tree[r]*(r-1))
你看r*(一坨)可以用我们的升级版树状数组做,后面的那个规律也很明显
tree1[i]=tree[i]*(i-1)对吧!
好了
只要针对这个修改一下代码即可
题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上x
2.求出某区间每一个数的和
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和
输出格式:
输出包含若干行整数,即为所有操作2的结果。
输入输出样例
输入样例1:
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
输出样例1:
11
8
20
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define lowbit(i) i&(-i)
ll n,m;
ll tree[100010]={0};
ll tree1[100010]={0};
ll getsum(ll *array,ll x){
ll sum=0;
for(int i=x;i;i-=lowbit(i)){
sum+=array[i];
}
return sum;
}
void insert(ll *array,ll index,ll val){
for(ll i=index;i<=n;i+=lowbit(i)){
array[i]+=val;
}
}
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
ll a,b=0;
for(ll i=1;i<=n;i++){
cin>>a;
b=a-b;//差分思想
insert(tree,i,b);
insert(tree1,i,b*(i-1));
b=a;
}
for(ll i=1;i<=m;i++){
int t,x,y,z;
cin>>t;
if(t==1){
cin>>x>>y>>z;
insert(tree,x,z);
insert(tree,y+1,-z);
insert(tree1,x,z*(x-1));
insert(tree1,y+1,-z*(y));
}
else{
cin>>x>>y;
cout<<getsum(tree,y)*y-(x-1)*getsum(tree,x-1)-(getsum(tree1,y)-getsum(tree1,x-1))<<endl;
}
}
return 0;
}