之前学习了一些树状数组、线段树的知识点,在此做一个小总结。
树状数组
树状数组是一种可以进行单点修改,区间查询(如区间和,区间乘法)的速度较快,代码量较小的数据结构。
原理
tree[x]存储的右边界是自己,长度是lowbit(x)(左闭右闭)。lowbit(x)是二进制中x从低位到高位中第一个不为零的数字以及它后面的零所组的数字。一种快速的计算方法是x&(-x)。(这里就不再证明了)。
区间查询
首先我们来看如何进行区间查询。在前缀和中我们查询l到r的区间,就是用r的前缀和减去l-1的前缀和,在树状数组中同样也是如此。现在我们就把问题转换成了如何查询从1到x的前缀。我们从tree[x]开始往前跳,会发现我们新查询的区域的右边界就是原来查询的区域的左边界减一。因为tree[x]的左边界是x-lowbit(x)+1,所以新查询的区域的右边界就是x-lowbit(x)。以此类推,直到新查询的区域的右边界为零,就说明已经查询完了。最后把这些区域合起来即可。
单点修改
又该如何进行单点修改呢?那就要看哪些区域包含x了。这是一个树状数组的图:
oi-wiki.net/ds/images/fenwick.svg
可以发现,包含a[x]的c[y]一定包含c[x],而且y在树状数组树形态上是x的祖先(借用一下谢谢),所以就可以跳了:先修改c[x],然后将x跳到x+lowbit(x),以此类推,直到x子大于n,就说明超过边界了。
代码
题目
如题,已知一个数列,你需要进行下面两种操作:
-
将某一个数加上 x;
-
求出某区间每一个数的和。
示例代码
cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,m,tree[600050];
void add(ll x,ll k){
while(x<=n){
tree[x]+=k;
x+=x&(-x);
}
}
ll query(ll x){
ll sum=0;
while(x){
sum+=tree[x];
x-=x&(-x);
}
return sum;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
ll x;
cin>>x;
add(i,x);
}
for(int i=1;i<=m;i++){
ll a,b,c;
cin>>a>>b>>c;
if(a==1){
add(b,c);
}else{
ll ans=query(c)-query(b-1);
cout<<ans<<endl;
}
}
return 0;
}
线段树
线段树是一种可以进行区间修改、区间查询的数据结构。虽然它比树状数组代码量更大,运行起来常数也会稍微大一点,但它比树状数组更加全面。树状数组能做的他都能做,树状数组不能做的他也能做(如求max,min值)。
原理
跟树状数组相比,线段树就很直接粗暴了。它的根节点是一条从1到n的线段,左边的儿子节点是这条线段的左半部分,右边的儿子节点是这条线段的右半部分,其他的线段也是这样。以此类推,直到线段长度为1,只包含一个点。当根节点编号为一的时候,每个节点的两个儿子节点的编号分别是2*x和2*x+1,若节点的范围是[l,r],那么设中点mid=(l+r)/2,左儿子节点的范围是[l,mid],右儿子节点的范围是[mid+1,r]。
单点修改
如果只能单点修改区间查询的线段树,看似和树状数组没什么区别,但是他可以解决max,min值等问题,所以这里也讲一下。
其实单点修改很简单。就是类似于DFS,每一次判断一下这一个点在左右哪两个子树之中,直到dfs到叶子节点,修改完之后再层层往上传就可以了。
区间修改
区间修改和单点修改不同。如果还是像单点修改一样直接所有的都dfs,那么时间复杂度肯定爆炸。这时候该怎么办呢?懒惰标志------lazy就上线了。
这里,lazy[i]表示对i往下的子节点进行修改(区间内所有点修改的总和),这一个时候,当我们修改区间包括了这一条线段时,我们就不会继续再传下去了,而是对这个点进行修改之后修改lazy的值。
当然,如果你走到了一个节点,并发现这里有lazy标志(值不为0),就要把lazy标志传下去,同时把自己lazy标志的值修改为0。
区间查询
当我们的查询区间包括了这一条线段时,我们就不会继续再传下去了,而是直接返回值。当然,和区间修改一样,如果你走到了一个节点,并发现这里有lazy标志(值不为0),就要把lazy标志传下去,同时把自己lazy标志的值修改为0。然后在查看要查询的区间是否与左子节点和右子节点的线段相交(或包含),如果相交(或包含)就继续DFS下去。
代码
题目(单点修改区间查询)
很多学校流行一种比较的习惯。老师们很喜欢询问,从某某到某某当中,分数最高的是多少。这让很多学生很反感。
不管你喜不喜欢,现在需要你做的是,就是按照老师的要求,写一个程序,模拟老师的询问。当然,老师有时候需要更新某位同学的成绩。
示例代码
cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,m,a[250050],tree[1000050];
void build(ll x,ll l, ll r){
if(l==r){
tree[x]=a[l];
return;
}
ll mid=(l+r)/2;
build(x*2,l,mid);
build(x*2+1,mid+1,r);
tree[x]=max(tree[x*2],tree[x*2+1]);
}
void update(ll x,ll l,ll r,ll pos,ll val){
if(l==r){
if(val>tree[x])tree[x]=val;
return;
}
ll mid=(l+r)/2;
if(pos<=mid)update(x*2,l,mid,pos,val);
else update(x*2+1,mid+1,r,pos,val);
tree[x]=max(tree[x*2],tree[x*2+1]);
}
ll query(ll x,ll l,ll r,ll p1,ll p2){
if(l>=p1&&r<=p2){
return tree[x];
}
ll ans=0;
ll mid=(l+r)/2;
if(mid>=p1)ans=max(ans,query(x*2,l,mid,p1,p2));
if(mid<p2)ans=max(ans,query(x*2+1,mid+1,r,p1,p2));
return ans;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
build(1,1,n);
for(int i=1;i<=m;i++){
char c;
ll b,d;
cin>>c>>b>>d;
if(c=='Q'){
cout<<query(1,1,n,b,d)<<endl;
}else{
update(1,1,n,b,d);
}
}
return 0;
}
题目(区间修改区间查询)
如题,已知一个数列 {ai},你需要进行下面两种操作:
- 将某区间每一个数加上 k。
- 求出某区间每一个数的和。
示例代码
cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,m,a[150050],tree[500050],lazy[500050];
void build(ll x,ll l,ll r){
if(l==r){
tree[x]=a[l];
return;
}
ll mid=(l+r)/2;
build(x*2,l,mid);
build(x*2+1,mid+1,r);
tree[x]=tree[2*x]+tree[2*x+1];
}
void update(ll x,ll l,ll r,ll p1,ll p2,ll val){
if(l>=p1&&r<=p2){
lazy[x]+=val;
tree[x]+=(r-l+1)*val;
return;
}
ll mid=(r+l)/2;
if(lazy[x]){
lazy[2*x]+=lazy[x]; lazy[2*x+1]+=lazy[x];
tree[2*x]+=(mid-l+1)*lazy[x]; tree[2*x+1]+=(r-mid)*lazy[x];
lazy[x]=0;
}
if(mid>=p1)update(2*x,l,mid,p1,p2,val);
if(mid<p2)update(2*x+1,mid+1,r,p1,p2,val);
tree[x]=tree[2*x]+tree[2*x+1];
}
ll query(ll x,ll l,ll r,ll p1,ll p2){
if(l>=p1&&r<=p2){
return tree[x];
}
ll mid=(r+l)/2;
if(lazy[x]){
lazy[2*x]+=lazy[x]; lazy[2*x+1]+=lazy[x];
tree[2*x]+=(mid-l+1)*lazy[x]; tree[2*x+1]+=(r-mid)*lazy[x];
lazy[x]=0;
}
ll ans=0;
if(mid>=p1)ans+=query(2*x,l,mid,p1,p2);
if(mid<p2)ans+=query(2*x+1,mid+1,r,p1,p2);
return ans;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
build(1,1,n);
while(m--){
ll a,b,c,d;
cin>>a>>b>>c;
if(a==1){
cin>>d;
update(1,1,n,b,c,d);
}else{
cout<<query(1,1,n,b,c)<<endl;
}
}
return 0;
}
如果大家有其他想法的,可以补充。