今天想跟大家分享的东西是树状数组,什么是树状数组呢?根据名字就能大致推出就是利用我们常见的数组来模拟树状结构。那么他可以解决哪些问题呢?我最后会加以说明。
首先我们来说下树状数组长什么样子呢?
在这副图片上我们可以看到有两种元素的数组,黑颜色的数组代表原来的数组,我们用a[ ]来表示吧,而红颜色的数组代表我们要构造的树状数组,我们用b[ ]来表示。每个红颜色的数组里面存放的是它下面节点元素的和,观察图片不难得出:
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]
我们现在从上面的一些例子中找一下规律;我们不难发现如果n为2的幂次,那么c[n]=a[1]+a[2]+.....+a[n],那么这样的n在二进制表示下又有什么规律呢?我们发现它的二进制表示中只有最高位是1,其余位均是0,那么这与二进制表示下的数的最低位到最高位有没有什么关系呢?事实上c[n]=a[n-2^m+1]+a[n-2^m+2]+.....+a[n]其中m为n的二进制表示中末尾连续0的个数。
那么这个m该如何求呢,m=n&(-n);举个例子来说:
n为12,则二进制表示为1100,那么它的反码为各位取反,即0011,则此时为1的地方在n中都为0,那么将其反码加1则会使原来n中第一个1及其之前的数保持不变,而之后的数均被取反,那么他们做&运算会保留下第一个为1的数,这也就是lowbit函数的原理啦。
我们常见的区间问题主要有以下几种:
(1)单点更新,单点查询;
(2)单点更新,区间查询 ;
(3)区间更新,单点查询;
(4)区间更新,区间查询;
第一个问题用一个普通的一维数组即可实现,在这里就不多说了,我主要想介绍一下后三个问题的处理方法。
我们先处理第二个问题,c数组的含义比较简单,c[x]=c[x-lowbit(x)+1]+c[x-lowbit(x)+2]+……+c[x],那么我们要想求1~x内所有元素的和,只需要求c[x]+c[x-lowbit(x)]+c[x-lowbit(x)-lowbit(x-lowbit(x))]+……+c[0],这个可以用一个for循环来实现,如果我们对a[i]进行修改,我们所构造的数组是储存a数组的和,所以肯定会受到影响,但是哪些元素受到影响了呢?实际上是c[i+lowbit(i)],c[i+lowbit(i)+lowbit(i+lowbit(i))]......;不难发现,这个操作恰好可以用lowbit函数解决。下面给出建立树状数组的板子:
int lowbit(int x)//求出x中第一个为1的位置
{
return x&(-x);
}
void update(int x,int k)//构造树状数组
{
while(x<=n)
{
c[x]+=k;
x+=lowbit(x);
}
}
int sum(int i)//实现查询
{
int res=0;
while(i>0)
{
res+=c[i];
i-=lowbit(i);
}
return res;
}
这就是单点更新,区间查询的板子了。下面给一个例题:(纯模板)洛谷3374
题目描述
如题,已知一个数列,你需要进行下面两种操作:
-
将某一个数加上 x
-
求出某区间每一个数的和
输入格式
第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 个整数,表示一个操作,具体如下:
-
1 x k
含义:将第 x 个数加上 k -
2 x y
含义:输出区间 [x,y] 内每个数的和
输入输出样例
输入
5 5 1 5 4 2 3 1 1 3 2 2 5 1 3 -1 1 4 2 2 1 4
输出
14 16
#include<iostream>
using namespace std;
int a[500010],c[500010];
int n,m;
int lowbit(int x)
{
return x&(-x);
}
void update(int x,int k)
{
while(x<=n)
{
c[x]+=k;
x+=lowbit(x);
}
}
int sum(int i)
{
int res=0;
while(i>0)
{
res+=c[i];
i-=lowbit(i);
}
return res;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
update(i,a[i]);//每输入一次值就更新一次所构造的数组
}
int a,b,c;
while(m--)
{
cin>>a>>b>>c;
if(a==1)
update(b,c);
if(a==2)
cout<<(sum(c)-sum(b-1))<<endl;
}
return 0;
}
我们现在来考虑第三个问题,也就是单点区间更新,单点查询;暴力做法就是在修改区间内的数一个一个修改,但这样的复杂度显然有点高,我们怎样能够通过树状数组来实现这个操作呢?我们联想一下普通数组的区间更新,单点查询问题,我们当时是构造的差分数组,同样的,我们在这里也可以构造一个差分数组d[],那么d[n]不就表示a[n]-a[n-1]了吗,那么a[n]=d[1]+d[2]+d[3]+....+d[n]=a[1]+(a[2]-a[1])+(a[3]-a[2])+....+(a[n]-a[n-1])=a[n],也就是说我们单点查询的结果就是d数组的前缀和,也就是对应的a数组的值。而修改区间[l,r]仅仅相当于修改点l和点r而已。下面给出一道例题:(纯模板)
如题,已知一个数列,你需要进行下面两种操作:
-
将某区间每一个数数加上 x;
-
求出某一个数的值。
输入格式
第一行包含两个整数 N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含 N 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 M 行每行包含 2 或 4个整数,表示一个操作,具体如下:
操作 1: 格式:1 x y k
含义:将区间 [x,y] 内每个数加上 kk;
操作 2: 格式:2 x
含义:输出第 x 个数的值。
输出格式
输出包含若干行整数,即为所有操作 2 的结果。
输入输出样例
输入
5 5 1 5 4 2 3 1 2 4 2 2 3 1 1 5 -1 1 3 5 7 2 4
输出
6 10
下面是代码
#include<iostream>
using namespace std;
int a[500010],c[500010];
int n,m;
int lowbit(int x)
{
return x&(-x);
}
void update(int x,int k)
{
while(x<=n)
{
c[x]+=k;
x+=lowbit(x);
}
}
int sum(int i)
{
int res=0;
while(i>0)
{
res+=c[i];
i-=lowbit(i);
}
return res;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
update(i,a[i]-a[i-1]);//我们所构造的是差分数组
}
int a,b,c,d;
while(m--)
{
cin>>a;
if(a==1)
{
cin>>b>>c>>d;
update(b,d);//更新数组左边界
update(c+1,-d);//更新数组右边界
} if(a==2) { cin>>c; cout<<sum(c)<<endl; } } return 0; }
最后一个问题就是区间更新区间查询了,这个问题我们又应该如何构造树状数组呢?这个呢我们还是利用差分的思想,有一些符号我还不会打,分析过程我就写在纸上了,哈哈,下面给大家附上图片:
#include<iostream>
using namespace std;
int a[500010],c[500010],d[500010];
int n,m;
int lowbit(int x)
{
return x&(-x);
}
void update(int x,int k)
{
int t=x-1;
while(x<=n)
{
c[x]+=k;//维护差分数组
d[x]+=t*k;//维护(i-1)*d[i]
x+=lowbit(x);
}
}
int sum(int i)//求和
{
int res=0,x=i;
while(i>0)
{
res+=x*c[i]-d[i];
i-=lowbit(i);
}
return res;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
update(i,a[i]-a[i-1]);
}
int a,b,c,d;
while(m--)
{
cin>>a;
if(a==1)//在b~c区间上加上d
{
cin>>b>>c>>d;
update(b,d);
update(c+1,-d);
}
if(a==2)//输出b~c区间的和
{
cin>>b>>c;
cout<<sum(c)-sum(b-1)<<endl;
}
}
return 0;
}
总结:树状数组功能性不如线段树强,但它的代码比较简单,可用于解决很多关于区间上的基础问题。
码了近三个小时,总算是码完了,码字不易,如果能够帮助到大家的话,希望大家能够动动手点个赞再走,谢谢啦~