这是本蒟蒻的第一篇博客,如有不妥,请各位大佬加以指正。
树状数组是什么?
学树状数组首先当然要知道树状数组是什么。下面是我粘过来的定义:
树状数组的查询和修改的时间复杂度都是log(n),空间复杂度则为O(n),这是因为树状数组通过将线性结构转化成树状结构,从而利用位运算进行跳跃式扫描。通常使用在高效的计算数列的前缀和,区间和。
(其实你只需要知道它的时间空间复杂度就行了,应用领域后文会讲)
跳跃式扫描的实现原理
小东西长得真别致
如上图:我们可以把C数组当成管理者,把A数组当成员工。A数组存的是原始数据,C数组存的是它指向的所有元素的和。一个管理者至少管理一名员工。
经过细心观察(鬼才能看出来。。。),我们可以发现管理者的编号的二进制与他管理员工的数目有着一些 微妙 的关系。
管理者编号 | 编号的二进制 | 管理员工数目 |
---|---|---|
1 | 0001 | 1 |
2 | 0010 | 2 |
3 | 0011 | 1 |
4 | 0100 | 4 |
.。。。 | 。。。 | 。。。 |
8 | 1000 | 8 |
你可能会疑惑:有什么鬼关系?看起来二者毫无关系。但是,观察二进制那一栏,你会发现把从右往左数遇到第一个1的途中经过的0的个数作为2的次幂恰好是管理员工的个数。
拿3号管理者为例:它的二进制数从右往左数遇到第一个1所经过的0的个数为0,2的0次幂是1,所以3号管理者管一名员工。又如,8号管理者的二进制数从右往左数遇到第一个1所经过的0的个数为3,2的3次幂是8,所以8号管理者管八名员工。知道了这个,我们就能很方便的实现跳跃搜索。
跳跃式搜索的关键:lowbit函数
我们知道了管理者编号与管理员工数目之间的关系,下面我们要用代码来实现。
创建一个lowbit(),函数括号里面是管理者编号,返回值是管理员工数目。具体实现如下。
int lowbit(int x)
{
return x&-x;
}
这段代码很简单,记住就行,并不用深究其原理。(我也不清楚)
有了lowbit,我们就可以轻松的利用树状数组进行各种骚操作了。
树状数组的应用领域
下面我会对单点修改,区间查询(对数组中某个值进行修改,查询某一段区间的和),区间修改,单点或区间查询(对数组中某一段区间进行修改,查询某个值的和或某一个区间的和)三大类问题进行说明与解决。
单点修改,区间查询
题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某一个数加上x
2.求出某区间每一个数的和
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3个整数,表示一个操作,具体如下:
操作1: 格式:1 x k 含义:将第x个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和
输出格式:
输出包含若干行整数,即为所有操作2的结果。
输入样例:
5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4
输出样例:
14
16
来源:洛谷。
题目链接
https://www.luogu.org/problemnew/show/P3374
如果不用树状数组,这道题直接暴力从头到尾扫一遍就行了。但是每一次修改和查询的时间复杂度都是O(n),如果有很多次询问,那么耗费的时间是无法忍受的。但是用树状数组的话,情况就会大大不一样。
(我把图又粘了过来,方便看)
首先我们来讨论修改单点的值的做法,也就是修改员工的值。因为管理者的值是他管理的所有员工的值的和,所以如果修改员工的值的话,那么所有管理此名员工的管理者的值都要修改。举个栗子:如果把一号员工的值加7,那么首先1号管理者的值要加7,通过lowbit发现1号管理者管1个,下一个修改1+1=2号管理者的值。2号管理者管2个,下一个修改2+2=4号管理者。一直修改到结尾。代码如下:
void add(int k,int h)
{
while(k<=n) //修改到结尾
{
c[k]+=h; //相关的管理者的值要修改
k+=lowbit(k); //寻找下一个相关管理者的位置
}
}
而区间求和则要更简单。假设我们要求前6个员工的值的和,我们先调出6号管理者,他的值是5号和6号员工的值的和,把他加上。之后我们通过lowbit函数发现他管2名员工,于是接着我们调出4号管理者,他管从1到4号员工,再把它加上。这时整个员工数组就已经遍历完了,而我们也就求出前6个员工的和,也就顺利完成任务了。代码如下
int sum(int k)
{
int ans=0;
while(k>=1)
{
ans+=c[k];
k-=lowbit(k);
}
return ans;
}
这就是一个简单的树状数组模板题,后文会有应用题。代码我贴下面了。
#include <bits/stdc++.h>
using namespace std;
int m,n;
int c[500000+100];
int lowbit(int x)
{
return x & -x;
}
void add(int k,int h)
{
while(k<=n)
{
c[k]+=h;
k+=lowbit(k);
}
}
int sum(int k)
{
int ans=0;
while(k>=1)
{
ans+=c[k];
k-=lowbit(k);
}
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int l;
cin>>l;
add(i,l); //我没有定义a数组,而是直接将a数组的值添加到c数组中。
}
for(int i=1;i<=m;i++)
{
int x,a,b;
cin>>x>>a>>b;
if(x==1) add(a,b);
else cout<<sum(b)-sum(a-1)<<endl;
}
}
区间修改,单点查询
这个要用到差分数组,我把详解地址贴上,这里就简单叙述一下。
https://www.cnblogs.com/kickit/p/9172189.html
举个栗子:
原数组 | 1 | 5 | 2 | 9 | -3 |
---|---|---|---|---|---|
差分数组 | 1 | 4 | -3 | 7 | -12 |
差分数组的前缀和 | 1 | 5 | 2 | 9 | -3 |
– | – | – | – | – | – |
我们可以发现差分数组某一位的前缀和就是这一位的原数组的值。因此我们可以利用差分数组的前缀和来求某点的值,完成了单点查询的要求。
而区间修改不需详细了解其原理,记住就行。如果我要在原数组2到4的区间上加5(数组下标从1开始,对不起,我的习惯)。我只需在差分数组的第2位加上5,第4位的后一位(第5位)减去5就行。过程如下。
原数组 | 1 | 5 | 2 | 9 | -3 |
---|---|---|---|---|---|
差分数组 | 1 | 4 | -3 | 7 | -12 |
差分数组的前缀和 | 1 | 5 | 2 | 9 | -3 |
修改后数组 | 1 | 10 | 7 | 14 | -3 |
修改后差分数组 | 1 | 9 | -3 | 7 | -17 |
修改后差分数组前缀和 | 1 | 10 | 7 | 14 | -3 |
仔细观察上表,跟着理解一下,记住就行了。上题!!!:
题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数数加上x
2.求出某一个数的值
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含2或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作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
题目链接:https://www.luogu.org/problemnew/show/P3368
上代码!!!
#include <bits/stdc++.h>
#define MAX 100050
using namespace std;
typedef long long ll;
int m,n;
int a[MAX],c[MAX];
int lowbit(int x)
{
return x&-x;
}
void add(int t,int da)
{
while(t<=n)
{
c[t]+=da;
t+=lowbit(t);
}
}
ll ask(int t)
{
ll sum=0;
while(t>0)
{
sum+=c[t];
t-=lowbit(t);
}
return sum;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
add(i,a[i]-a[i-1]);
}
cin>>m;
for(int i=1;i<=m;i++)
{
char st[10];
scanf("%s",st);
int aa,bb,cc;
if(st[0]=='Q')
{
cin>>aa;
cout<<ask(aa)<<endl;
}else
{
cin>>aa>>bb>>cc;
add(aa,cc);
add(bb+1,-cc);
}
}
return 0;
}
区间修改,区间查询的情况可以结合前面讲的两大类。树状数组简单的应用就是这些,更加复杂的用法需要多加练习,熟能生巧。
好啦,这篇博客就到这里了,顺便安利一篇维护区间最大值的树状数组方法。
https://blog.csdn.net/zhouzi2018/article/details/81108940
如果这篇博客有错误之处,请尽快联系博主,或留言,谢谢各位大佬!!