复赛要来了,群赛已经被吃掉了,我们自己写博客准备关于复赛的基础知识。
关于树状数组
本来我准备做线段树的,结果我发现树状数组标的难度比线段树简单,我就做了树状数组。其实是因为线段树代码长度比树状数组长很多,所以差了一个难度级。
如果要求对一个数组中的元素实时进行求区间和以及对单个元素的修改,则有以下几种方法。举例子(洛谷p3374样例) 1是对后面输入的x加上y,2是输出l,r的区间和。
5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4
当你看到题的时候你第一个会想到的方法是暴力求。如果所有操作全部是2,复杂度是n*m,数据大肯定会挂。
还有你可以用一个二维数组存储所有区间和,O1出答案。但是这样需要的空间非常大,而且当单点修改的时候也非常复杂。
所以这时候你可能就对其望而却步。不!我们有树状数组。不用看都知道,它肯定和log有关。它的预处理复杂度nlogn,单点修改和询问的复杂度都是logn,总复杂度是nlogn+qlogn,可以过500000,500000的需求。
树状数组的实现
现在我以灵魂画师的身份给大家介绍一下树状数组。
这个图画得不怎么样,我给你们解释一下。
什么意思呢?
c[1]=a[1];
c[2]=a[1]+a[2];
c[3]=a[3];
c[4]=a[1]+a[2]+a[3]+a[4];
c[5]=a[5];
c[6]=a[5]+a[6];
c[7]=a[7];
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8];
……
你有没有看出什么规律?对没错,c这个数组所能包含a数组元素的个数为c的下标所能整除的最大2的整数乘方。用通俗一点的话来说,把这些数转化成2进制,2的它后面0的个数的乘方。比如说6=110(2),后面有1个0,它能表示的元素个数为2^1=2.
应该懂了吧。
又有一个问题,我怎样迅速地求出它?接下来给大家介绍一个神奇的函数。
lowbit(x)=x&(-x)
什么?有这么神奇的东西?为什么呢?在电脑中表示负数用的是补码,把该数字每一位0变1,1变0,然后加1就可以得到。这样就不难解释为什么lowbit算出来的是最后一个1所表示的位置。比如说6=110(2),-6=001+1(2)=010(2)所以6&-6=2,成功地算出了它。
这样就简单了。
那么我们可以写出两个函数add和getsum。注意接下来的操作和a数组就没有关系了,只牵扯到c数组。
#define lowbit(x) (x&-x)
inline int getsum(int x)//求1-x的所有元素之和
{
int sum=0;
for (;x>0;x-=lowbit(x)) sum+=c[x];//对于该数的每一位都有一位数字代表着它之前的数字之和,仔细研究
return sum;
}
inline void add(int x,int y)//对a[x]+y
{
for (;x<=n;x+=lowbit(x)) c[x]+=y;//对于含有x的每一个c数组都要加y,仔细研究为什么可以这么做
}
这样复杂度都是logn,比较好求吧。
你问我一开始c数组怎么求?自己想一下,实在想不出看一下题解你就会了。
例题
如果你已经理解了上面的东西,你已经会了树状数组,做一下洛谷p3374证明你学会了。
我把题解贴在下面。由于快读忘了读负数差点挂掉了。
#include<bits/stdc++.h>
using namespace std;
const int boss=5e5;
int a[boss+10],c[boss+10],n,m;
inline int lowbit(int x){return x&(-x);}
inline int getsum(int x){int sum=0;for (;x>0;x-=lowbit(x)) sum+=c[x];return sum;}
inline void add(int x,int y){for (;x<=n;x+=lowbit(x)) c[x]+=y;}
inline void pre()
{
for (int i=1;i<=n;i++)
for (int j=i-lowbit(i)+1;j<=i;j++) c[i]+=a[j];
}
inline int read()
{
int x=0;bool f=0;char c=getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=1;
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x*(f?-1:1);
}
int main()
{
int i;
n=read(),m=read();
for (i=1;i<=n;i++) a[i]=read();
for (pre(),i=1;i<=m;i++)
{
int t=read();
if (t==1)
{
int x=read(),y=read();
add(x,y);
}
else
{
int x=read(),y=read();
printf("%d\n",getsum(y)-getsum(x-1));
}
}
}
树状数组到这里就结束了。但是还有一个东西没完。因为洛谷还有第二个树状数组模板题,它的要求是区间修改,单点询问。
那这个又怎么办呢?add和getsum是不变的,但是操作要变骚。
add的时候,你要用c数组存储改变量而不是数字和。询问的时候你只要用原来的数字加上改变量就可以了。我做完模板1直接就做了模板2,所以基本都没有改。
#include<bits/stdc++.h>
using namespace std;
const int boss=5e5;
int a[boss+10],c[boss+10],n,m,d[boss+10];
inline int lowbit(int x){return x&(-x);}
inline int getsum(int x){int sum=0;for (;x>0;x-=lowbit(x)) sum+=c[x];return sum;}//xianduanshu 1
inline void add1(int x,int y){for (a[x]+=y;x<=n;x+=lowbit(x)) c[x]+=y;}//xianduanshu 1
inline void add2(int x,int k){for (;x<=n;x+=lowbit(x)) d[x]+=k;}//xianduanshu 2
inline int query(int x){int sum=0;for (;x>0;x-=lowbit(x)) sum+=d[x];return sum;}//xianduanshu 2
inline void pre1()//xianduanshu 1
{
for (int i=1;i<=n;i++)
for (int j=i-lowbit(i)+1;j<=i;j++) c[i]+=a[j];
}
inline int read()
{
int x=0;bool f=0;char c=getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=1;
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x*(f?-1:1);
}
int main()
{
int i;
n=read(),m=read();
for (i=1;i<=n;i++) a[i]=read();
// for (pre1(),i=1;i<=m;i++)
// {
// int t=read();
// if (t==1)
// {
// int x=read(),y=read();
// add1(x,y);
// }
// else
// {
// int x=read(),y=read();
// printf("%d\n",getsum(y)-getsum(x-1));
// }
// }
for (;m--;)
{
int k=read();
if (k==1)
{
int x=read(),y=read(),z=read();
add2(x,z),add2(y+1,-z);//把x后面的加上z,把y后面的加上-z,也就是加了xy之间的
}
else
{
int x=read();
printf("%d\n",a[x]+query(x));
}
}
}
好了,树状数组讲到这里。谢谢大家的观看。