题目描述
如题,已知一个数列,你需要进行下面两种操作:
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
说明/提示
时空限制:1000ms,128M
数据规模:
对于30%的数据:N<=8,M<=10
对于70%的数据:N<=10000,M<=10000
对于100%的数据:N<=500000,M<=500000
样例说明:
故输出结果为6、10
思路
准备在树状数组模版2详细讲解树状数组。
目录
1.1 概述/前言
1.2 实现树状数组
1.3 树状数组模板1
2.1 差分
2.2 树状数组模版2
3 总结
1.1 概述/前言
树状数组是个好东西,树状数组很完美地将树形结构和二进制联系在一起。树状数组的复杂度都为O(logn),且常数比线段树小,一般为1/2。
刚学树状数组的同学都会认为线段树比树状数组简单。我承认是,我自己也这样觉得,因为你想想,从十进制的角度去看二进制,肯定很别脑。
树状数组用来干嘛呢?树状数组是用数据压缩的思想由二进制实现的数据结构。有单点修改+区间查询或区间修改+单点查询的作用。(可以扩展功能,但是这里就不提及)
树状数组和线段树有什么关系呢?就是弟弟和哥哥的关系。线段树可以实现的功能和扩展功能比树状数组多。本模版还可以使用zkw线段树这个高级的东西,这里也不提及。
好了闲话扯完,进入正题。
1.2 实现树状数组
咱先不管树状数组这种数据结构到底的结构什么,先来了解下lowbit
这个函数。
电脑中有一种叫做补码的操作(由于电脑是二进制,它们存的相反数是它的取反+1),一个数与它的相反数做与操作时会返回二进制下最右边的1的位置。lowbit
这个函数的功能就是求某一个数的二进制表示中最低的一位1。
听起来有点抽象,举个例子。
例如有一个数x=6,它的二进制就为110。那么lowbit(x)返回的是6的二进制下,也就是110中10的值:2。因为110最后一个1表示2。
本蒟蒻智商有限,只会套模版:
inline int lowbit(int x)
{
return x&-x;
}
然后,为了简化区间修改的效率,我们需要建立这样的一个数组bit,换句话说是数据结构:数组bit中第k位的值为原数组中的一段区间和,这个区间的长度是lowbit(k),终点是k。即用循环枚举出与它相关的位置,都增加(减少)即可。
我们就把这种数组bit称为树状数组。
代码
inline void update(int x,int k)
{
while(x<=n)//修改区间[x,n]的值
{
bit[x]+=k;//第k位数组加上k
x+=lowbit(x);
}
}
听起来有点雾?我们来模拟一下样例。
输入:
5
1 5 4 2 3
输入第1个数a[1]时,x=1,k=1;加上的树状数组数组位数分别为第1位,第2位,第4位。树状数组为:1 1 0 1 0。
输入第2个数a[2]时,x=2,k=5;加上的树状数组数组位数分别为第2位,第4位。树状数组为:1 6 0 6 0。
输入第3个数a[3]时,x=3,k=4;加上的树状数组数组位数分别为第3位,第4位。树状数组为:1 6 4 10 0。
输入第4个数a[4]时,x=4,k=2;加上的树状数组数组位数是第4位。树状数组为:1 6 4 12 0。
输入第5个数a[5]时,x=5,k=3;加上的树状数组数组位数是第5位。树状数组为:1 6 4 12 3。完成。
可以发现的是:
树状数组bit[1]=a[1];
树状数组bit[2]=a[1]+a[2];
树状数组bit[3]=a[3];
树状数组bit[4]=a[1]+a[2]+a[3]+a[4];
树状数组bit[5]=a[5]。
怎么样?是不是对lowbit这个函数影响加深了呢?其实就是上面那张图。a[x]+k同时也会使bit[x]+k,bit[x]+k时bit[x+x的二进制表示中最低的一位1所在位置]同时也会+k。
我们可以发现这个操作的本质就是修改树状数组的值,所以它是单点修改的函数。
接下来是查询。注意我们不能直接查询某个数的前缀和,而是要减去区间前缀和差。举个例子。
一个数组={1,5,4,2,3},求区间[3,5]的和。
那我们就要求区间[1,5]与区间[1,3-1]的差。sum([1,5])-sum([1,3-1])=sum([3,5])。
模拟一下。
先求5的前缀和。所求的就是第5(101)项+第4(100)项就是3+12=15。
再求2的前缀和。所求的就是第2(10)项就是6。
最后作差就是15-6=9。
用a数组检验一下,发现a[3...5]=4+2+3=9,是对的。所以求区间[x,y]的前缀和,与求区间[1,x-1]和区间[1,y]的前缀和是一样的。
如果理解了update那么sum也不难理解。
inline ll sum(ll k)
{
ll i(0);
while(k>0)
{
i+=bit[k];
k-=lowbit(k);
}
return i;
}
1.3 树状数组模版1
#include <stdio.h>
#include <iostream>
#define ll long long int
using namespace std;
ll n,m,s,a[500001],bit[500001];
inline ll lowbit(ll x)
{
return x&-x;
}
inline void update(ll x,ll k)
{
while(x<=n)//修改区间[x,n]的值
{
bit[x]+=k;//第k位数组加上k
x+=lowbit(x);
}
}
inline ll sum(ll k)
{
ll i(0);
while(k>0)
{
i+=bit[k];
k-=lowbit(k);
}
return i;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
register ll i,j;
cin>>n>>m;
for(i=1;i<=n;i++)
{
cin>>a[i];
update(i,a[i]);//在区间[i,n]上加上a[i]的值
}
for(i=1;i<=m;i++)
{
int x,y,z;
cin>>x>>y>>z;
if(x==1) update(y,z);//虽然只是单点修改,但是树状数组是前缀和思想,所以[y,n]都有影响,全部加上z的值。
if(x==2) cout<<sum(z)-sum(y-1)<<endl;
}
return 0;
}
其中update的效率为O(NlogN),单点修改效率为O(logN),查询的效率为O(logN)。本模版程序总效率为O(MlogN)。
至此,树状数组模版1已经讲完。
2.1 差分
我们可以看到树状数组模版1的本质是单点修改&区间查询,而树状数组模版2中是区间修改&单点查询,该怎么实现呢?
看起来与之前模版1差不多。但是你自己想一下就会发现模版1直接套模版2会TLE。因为复杂度达到了O(NM)。
突破口:差分。
我其实之前也不会实现差分和它的原理,今天才学的。
下面的文字转载于一位洛谷大佬。
来介绍一下差分
设数组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;
2.2 树状数组模版2
#include <stdio.h>
#include <iostream>
#define ll long long int
using namespace std;
ll n,m,s,a[500001],bit[500001];
inline ll lowbit(ll x)
{
return x&-x;
}
inline void update(ll x,ll k)
{
while(x<=n)//修改区间[x,n]的值
{
bit[x]+=k;//第k位数组加上k
x+=lowbit(x);
}
}
inline ll sum(ll k)
{
ll i(0);
while(k>0)
{
i+=bit[k];
k-=lowbit(k);
}
return i;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
register ll i,j;
cin>>n>>m;
for(i=1;i<=n;i++)
{
cin>>a[i];
update(i,a[i]-a[i-1]);//a[i]-a[i-1]=b[i]
//现在加入树状数组的都是差分数组了
}
for(i=1;i<=m;i++)
{
int x,y,z,Case;
cin>>Case;
if(Case==1)
{
cin>>x>>y>>z;
update(x,z);//在b数组区间[x,y]进行修改,本质上只要修改b[x]和b[y+1]
update(y+1,-z);
//b[x]=b[x]+z
//b[y+1]=b[y+1]-z
}
if(Case==2)
{
cin>>x;
cout<<sum(x)<<endl;
}
}
return 0;
}
3 总结
可以看出,树状数组的码量比线段树少,且执行效率比线段树略快,主要原因是树状数组利用到了二进制思想和数组树化,线段树的常数主要是由于递归带来的开销导致的。有一种数据结构叫做zkw线段树,专门把递归版线段树改成了非递归的,很牛逼。当然,树状数组的功能是有限的,并没有线段树多。还有一个数据结构叫做“超级树状数组”,扩展了树状数组的功能,我们以后具体研究。
最后,一定要记住:如果看不懂也不要紧,会用就可以。