一、引入
对于一个给定的序列,要想解决【单点修改,区间查询】问题:(区间查询即区间求和)
(1)对于普通数组而言,单点修改的时间复杂度是 O(1) ,但区间求和的时间复杂度是 O(n) 。
(2)如果用前缀和的方法维护这个数组,区间求和的时间复杂度就降到了O(1),但是单点修改会影响后面所有的元素,时间复杂度是O(n)。
(3)根据维基百科的定义:
A Fenwick tree or binary indexed tree is a data structure that can efficiently update elements and calculate prefix sums in a table of numbers.
所谓树状数组,或称Binary Indexed Tree, Fenwick Tree,是一种用于高效处理对一个存储数字的列表进行更新及求前缀和的数据结构。树状数组可以比较轻松地处理【单点修改,区间查询】问题,时间复杂度都为o(logn)。
树状数组还与线段树有很大的联系,树状数组可以实现的功能线段树都可以实现,但有些功能只能由线段树来实现,但是相比较而言,线段树代码比较复杂,对于简单的问题用树状数组就好啦~
二、实现
树状数组维护的数组下标必须从1开始!可以边看此文章边思考为什么哦~(原因在之后会讲到嘞!)
虽说我们在讨论树状数组,它是一棵树,但是这棵树我们并不需要实实在在地建立起来,这只是我们脑中自己构想的数据结构罢了!
树状数组的核心就是利用lowbit()函数来维护数组、建立树,以此使我们进行的相关操作的时间复杂度达到o(logn)。
1. lowbit()函数
该函数是用来求二进制数(从右往左数)第一个1以及后面的0的组成的值。
例如,lowbit(10100)=100。(都是二进制数)
lowbit(x)= x &( -x ) ,x为二进制数。
(对于一个二进制数x,-x相当于x按位取反再加1,即求其补码。&就是将其补码和x按位做与运算。)
那么为什么这样就可以求出来了呢?看下图~
发现了什么没有?
对于“(从右往左数)第一个1以及后面的0”x和(-x)是一样的,而前面的几位因为求反码取反了就是不一样的,因此求出来的就是“(从右往左数)第一个1以及后面的0”。
代码如下:
int lowbit(int x){
return (x)&(-x);
}
对于更好地理解之后讲到的时间复杂度的计算,了解一下这个:
对于二进制数x =0001,让它不断更新自己,使x= x + lowbit ( x ),直到x的值为,可以思考一下我们需要相加几次呢?
答案是log2( )=16次。那么如果我是让x即1,每次+1,则我得加多少次才能加完鸭是不。
2. 单点修改,区间查询
接下来的问题就是我该怎么借助 lowbit() 来实现【单点修改,区间查询】等操作呢?
首先看下图:(引用了其他博主的图哈~
https://blog.csdn.net/TheWayForDream/article/details/118436732)
1)单点修改
树状数组 tree [ i ] 维护的原数组a [ ] 区间为 ( i - lowbit ( i ) , i ] ,值为该区间之和。
而且我们发现tree [ i + 1 ] = tree[ i + lowbit ( i ) ]。所以我们要进行单点修改下标为i的值,其他包含下标为i的值也是需要更新的,则只需一个循环来更新好t数组,主要见代码:
void update(int i,int k){//对下标为i的值加上k
for(int pos=i;pos<=N;pos+=lowbit(pos)){
tree[pos]+=k;
}
}
时间复杂度为o(logn)。
2)区间查询
对于区间查询,我们采用的是前缀和相减的方法,为什么要这样做呢?
因为在树状数组里面求前缀和的时间复杂度为o(logn),挺快的呢。
例如,从图中可以看出,a数组区间为[1,7]的和=tree[7]+tree[6]+tree[4],然而6=7-lowbit(7),4=6-lowbit(6),对于4-lowbit(4)<=1,所以就是t[7]、t[6]、t[4]三项之和。
代码如下:
int pre_sum(int a){
int ans=0;
for(int i=a;i>=1;i-=lowbit(i)){
ans+=tree[i];
}
return ans;
}
int query(int a,int b){
return pre_sum(b)-pre_sum(a-1);
}
对于问题“树状数组维护的数组为啥下标必须从1开始”:
如果tree数组下标从0开始(for的条件变为x>=0),当x为1时,x-=lowbit(x)即x=0,这是正确的,下一个循环x-=lowbit(x)即x还是=0。下下循环还是这样,因此sum会死循环加上tree[0],所以下标得从1开始嘞!!
学完这些之后就可以试一下这道题~
题目链接:https://www.luogu.com.cn/problem/P3374
代码:
#include<iostream>
#include<stdio.h>
#include<string.h>
#define lowbit(x) x&(-x)
#define MAXN 500010
typedef long long ll;
using namespace std;
ll a[MAXN],tree[MAXN];
int N,M;
void update(int i,int k){//对下标为i的值加上k
for(int pos=i;pos<=N;pos+=lowbit(pos)){
tree[pos]+=k;
}
}
int pre_sum(int a){
int ans=0;
for(int i=a;i>=1;i-=lowbit(i)){
ans+=tree[i];
}
return ans;
}
int query(int a,int b){
return pre_sum(b)-pre_sum(a-1);
}
int main(){
int op,x,y,k;
cin>>N>>M;
memset(tree,0,sizeof(tree));
for(int i=1;i<=N;i++){
cin>>a[i];
update(i,a[i]);
}
for(int i=0;i<M;i++){
cin>>op;
if(op==1){//单点修改
cin>>x>>k;
update(x,k);
}else{//区间求和
cin>>x>>y;
cout<<query(x,y)<<endl;
}
}
return 0;
}
3. 区间修改,单点查询
对于解决该问题,我们如果仅仅和【单点修改,区间查询】的方法一样,则时间复杂度将会变得很大。在这里,我们需要借助差分数组和树状数组,用树状数组来维护差分数组实现快速解决问题。
1)差分数组
对于区间修改(指的是在某一区间加上或减去同一个数),我们首先要学习差分数组。
例如,一个数组为:4,3,5,1,2。
其差分数组i下标的值就是让原数组i下标的值减去i-1下标的值,即d [ i ] =a [ i ] - a [ i-1] 。当i=0时,d[i]=a[i],结果如下表所示:
下标 | 0 | 1 | 2 | 3 | 4 |
原数组a[] | 4 | 3 | 5 | 1 | 2 |
差分数组d[] | 4 | -1 | 2 | -4 | 1 |
前缀和 | 4 | 3 | 5 | 1 | 2 |
对差分数组进行求前缀和,我们发现其值就是原数组的值!!!
因此我们实现区间修改就很简单啦!
例如,我们想让区间[1,3]的值加上3,只需要让d[1] + = 3, d [4] - = 3即可。为什么呢?思考一下~
下标 | 0 | 1 | 2 | 3 | 4 |
原数组a[] | 4 | 3 | 5 | 1 | 2 |
差分数组d[] | 4 | -1 +3 | 2 | -4 | 1 -3 |
前缀和 | 4 | 3 +3 | 5 +3 | 1 +3 | 2 +3-3 |
因为前缀和的影响,我们在差分数组中加上某一个数,则这个数会被前缀和传下去,要想根据我们的需求截断,就给d[3+1]减去该数,就抵消掉啦~
因此,我们就可以使用差分数组快速实现区间修改!!!
对于更好地理解差分数组,可以参考该文章练习相应题目。
链接:https://blog.csdn.net/qq_44786250/article/details/100056975
2)单点查询
对于【单点查询】查第 i 位置的值,我们只需要利用树状数组求差分数组的前 i 项前缀和即可。
这种方法的时间复杂度会很低,都是o(logn)。
代码和【单点修改,区间查询】差不多,就把树状数组维护的对象变换一下。
学会了来练习一下吧!链接:https://www.luogu.com.cn/problem/P3368
代码:(自己写了之后再来看代码哦~)
#include<iostream>
#include<stdio.h>
#include<string.h>
#define lowbit(x) x&(-x)
#define MAXN 500010
typedef long long ll;
using namespace std;
ll a[MAXN],d[MAXN],tree[MAXN];
int N,M;
void update(int i,int k){//对下标为i的值加上k
for(int pos=i;pos<=N;pos+=lowbit(pos)){
tree[pos]+=k;
}
}
int pre_sum(int a){
int ans=0;
for(int i=a;i>=1;i-=lowbit(i)){
ans+=tree[i];
}
return ans;
}
int query(int a,int b){
return pre_sum(b)-pre_sum(a-1);
}
int main(){
int op,x,y,k;
cin>>N>>M;
memset(tree,0,sizeof(tree));
for(int i=1;i<=N;i++){
cin>>a[i];
d[i]=a[i]-a[i-1];
update(i,d[i]);
}
for(int i=0;i<M;i++){
cin>>op;
if(op==1){
cin>>x>>y>>k;
update(x,k);
update(y+1,-k);
}else{
cin>>x;
cout<<pre_sum(x)<<endl;
}
}
return 0;
}
再给到树状数组的一道应用题练习练习: https://www.luogu.com.cn/problem/P1908
三、结束
对于解决【区间修改,区间查询】,树状数组可能不能实现,得依靠线段树。
这篇文章就先讲到这啦,如有问题,请在评论区指出我会及时回复哒~拜拜~