首先对于追求编码简单快捷的同学来说,比如ACM。树状数组可以实现的线段树都能实现。
树状数组是动态维护前缀和的工具
树状数组:
1.单点修改,单点查询
2.区间修改,单点查询
3.单点修改,区间查询
这三种我们使用树状数组比线段树敲起来简单,似乎还更快。
至于其他,就敲线段树吧
区间修改,单点查询用数组数组也可以的后面有补充
那么修改指什么呢?比如给某个数加上一个数,或者给某个区间(要结合差分数组)加上一个数
查询又可以是什么呢?比如查询某个点的前缀和,进而到区间求和。
至于时间复杂度,你都来百度模板了,那应该懂时间节省在哪了 其实省时你会发现就省在区间操作,使O(n)降至O(log n)
1.单点修改,区间查询
例题:P3374 【模板】树状数组 1
如题,已知一个数列,你需要进行下面两种操作:
将某一个数加上 xx
求出某区间每一个数的和
输入格式
第一行包含两个正整数 n,mn,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 nn 个用空格分隔的整数,其中第 ii 个数字表示数列第 ii 项的初始值。
接下来 mm 行每行包含 33 个整数,表示一个操作,具体如下:
1 x k 含义:将第 x 个数加上 k
2 x y 含义:输出区间 [x,y][x,y] 内每个数的和
输出格式
输出包含若干行整数,即为所有操作 22 的结果。
输入输出样例
5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4
输出 #1
14
16
AC模板:
#include<bits/stdc++.h>
using namespace std;
#define ll long long int
int n;//长度
ll a[1000005];//原输入数组
ll tree[1000005];//树状数组空间复杂度O(n)
int lowbit(int x)//x二进制数的末尾最后一个1和其后的所有0构成的二进制数
{
return x&(-x); //取反+1就等于 -x
}//返回的值就是其覆盖的长度
void add(int x,ll k)//单点修改 O(log n)
{
for(;x<=n;x+=lowbit(x))
tree[x]+=k;
return;
}//层层往父亲节点修改
ll ask(int x)//查询x的前缀和,向左上查找 O(log n)
{
ll ans=0ll;
for(;x;x-=lowbit(x))
ans+=tree[x];
return ans;
}
//区间<->单点
int main(void)
{
int q,k;
ll x;
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%lld",&x);
add(i,x);
}
while(q--)
{
int key;
ll r,l;
scanf("%d %lld %lld",&key,&l,&r);
if(key==1) add(l,r);//单点修改 给定 l, r,将 a[l] 加上 r;
else//求区间【l,r】的和 两个前缀和相减
printf("%lld\n",ask(r)-ask(l-1));
}
return 0;
}
由于tree数组初始化为0,add()既可以进行“建树”,又可以进行单点修改
2.区间修改,单点查询
区间修改:如果将x到y区间加上一个k,那就是引入差分数组修改x和y+1两点的差分值即可。
题目描述
如题,已知一个数列,你需要进行下面两种操作:
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
引入差分数组求区间修改问题的
何为差分数组:用洛谷P3368一楼大佬的题解:{
来介绍一下差分
设数组a[]={1,6,8,5,10},那么差分数组b[]={1,5,2,-3,5}
也就是说b[i]=a[i]-a[i-1];(a[0]=0;),那么a[i]=b[1]+…+b[i];(这个很好证的)。
假如区间[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的,所以在区间内b[i]-b[i-1]是不变的.
所以对区间[x,y]进行修改,只用修改b[x]与b[y+1]:
b[x]=b[x]+k;b[y+1]=b[y+1]-k;
}
这里再多讲一点:树状数组是维护前缀和的工具
差分数组优点在于区间修改,求某个数修改后的值需求前缀和,用差分数组时一般在输出时要遍历多个连续的数据
前者适用在线修改查询,后者适用于离线查询
送一道差分数组的题https://blog.csdn.net/qq_43791377/article/details/103394793
所以我们求区间修改问题就变为:我们用树状数组维护差分数组
那么当对 x ~ y 的区间进行修改的时候需要在树状数组中的第 x 个位置 + k, 第 y + 1 个位置 -k。
那么代码只需小小的修改即可:
AC代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long int
int n;//长度
int a[1000005];//原输入数组
ll tree[1000005];//树状数组空间复杂度O(n)
int lowbit(int x)//x二进制数的末尾最后一个1和其后的所有0构成的二进制数
{
return x&(-x); //取反+1就等于 -x
}//返回的值就是其覆盖的长度
void add(int x,ll k)//单点修改 O(log n)
{
for(;x<=n;x+=lowbit(x))
tree[x]+=k;
return;
}//层层往父亲节点修改
ll ask(int x)//查询x的前缀和,向左上查找 O(log n)
{
ll ans=0ll;
for(;x;x-=lowbit(x))
ans+=tree[x];
return ans;
}
//区间<->单点
int main(void)
{
int q,k;
ll x;
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
add(i,a[i]-a[i-1]);//维护差分数组
}
while(q--)
{
int key;
ll r,l;
scanf("%d",&key);
if(key==1)//区间【l,r】都加 x
{
scanf("%d %d %lld",&l,&r,&x);
//由于树状数组维护的是差分数组:
add(l,x);
add(r+1,-x);
}
else
{
scanf("%d",&l);
printf("%lld\n",ask(l));//为什么是前缀和呢?注意这是差分数组
//故ask(l)就是a[l]的值
}
}
return 0;
}
为社么要用树状数组维护差分数组而不是原数组??
引入差分数组就不用修改n-x次add()了【O(nlogn)+查询O(2log n)】,直接修改两个点【[O(2)+查询O(logn)]的值就行了!!!这样看快了好多有木有
补充:
前面我说过 “区间修改,单点查询”不建议用数组数组,但是我发现引入差分数组后,“区间修改,单点查询”也不会说用树状数组很难打。
区间修改,单点查询
需要题目的请点击:洛谷P3372
直接用B站某位大佬的视频截图吧:
总结:其实就是用两个树状数组维护差分数组 b[i] 和 i*b[i],还有计算前缀和(O(logn))
参考代码:(也不长,不难理解)
#include<bits/stdc++.h>
using namespace std;
#define ll long long int
int n;//长度
int a[1000005];//原输入数组
ll tree1[1000005];//维护差分数组b
ll tree2[1000005];//维护i*b[i] 为什么要维护这两个,因为后面区间查询时要求这两个的前缀和
int lowbit(int x)
{
return x&(-x); //取反+1就等于 -x
}//返回的值就是其覆盖的长度
void add(int x,ll k)//由于维护的是差分数组故相当于区间修改
{
for(int i=x;i<=n;i+=lowbit(i))
tree1[i]+=k,tree2[i]+=x*k;
return;
}//层层往父亲节点修改(修改父亲节点的值就是某一段的前缀和) ,所以为i
ll ask(int x)//查询x的前缀和,并计算 a[x]的 前缀和
{
ll ans=0ll;
for(int i=x;i;i-=lowbit(i))
ans+=(x+1)*tree1[i]-tree2[i];
return ans;
}
//a[x]的前缀和=∑∑d[j]=∑d[i](x-i+1)=(x+1)*(∑d[i])-(∑d[i]*i) 注‘∑’表示1~x求和
ll rang_ask(int l,int r)//区间查询
{
return ask(r)-ask(l-1);
}
int main(void)
{
int q,k;
ll x;
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
add(i,a[i]-a[i-1]);//维护差分数组
}
while(q--)
{
int key;
ll r,l;
scanf("%d",&key);
if(key==1)//区间【l,r】都加 x
{
scanf("%d %d %lld",&l,&r,&x);
//由于树状数组维护的是差分数组:
add(l,x);
add(r+1,-x);
}
else//求区间和,就是求 两个前缀和的差 !
{
scanf("%d %d",&l,&r);
printf("%lld\n",rang_ask(l,r));
}
}
return 0;
}
区间查询也可以写成这样:
ll ask(ll *z,int x)//单点查询前缀和
{
ll ans=0ll;
for(int i=x;i;i-=lowbit(i))
ans+=z[i];
return ans;
}
ll rang_ask(int l,int r)//区间查询
{
return ((r+1)*ask(tree1,r)-ask(tree2,r)) - ( (l-1 +1)*ask(tree1,l-1)-ask(tree2,l-1) ) ;
}
需要看线段树代码的请点击