树状数组的新构想
westernhan
2022-11-16 22:59:40
(一篇没啥用的博客)
# 前言
由于本人对于树状数组有着特殊的情感(doge),故经过一番思索,想出一种类似于线段树的树状数组,并发此水文。刚学树状数组的时候,我只知道树状数组可以单点修改,用来求前缀和,并由此延伸至区间和。后来学了差分,我又明白树状数组可以用来区间修改+单点查询及区间修改+区间查询(修改仅限加减法,乘除法不行)。看了Chanis大佬的文章[可以代替线段树的树状数组?](https://www.luogu.com.cn/blog/Chanis/super-BIT) 后,我掌握了树状数组求区间最值的方法。
但令我深感麻烦的是,以上几种功能往往不能组合在一起,而且区间修改仅限加减法使得树状数组具有很大的局限性,区间修改+区间查询的公式又过于复杂,很难记住。
那么,是否可以模仿线段树,想出一种带有懒标记的树状数组呢?这便是这篇文章的主题了。
# 基本结构
我们先看看树状数组的结构:
![树状数组](https://cdn.luogu.com.cn/upload/image_hosting/b63lo7a0.png)
观察结构我们可以获得以下特性:
1.设有一结点,序号为e。那么该结点的子结点必然是e-1,e-2,e-4,e-8...(减数小于lowbit(e))。
例如C[2]的子结点是C[2-1]即C[1],C[8]的子结点是C[8-1],C[8-2],C[8-4]。
因此我们可以用一个for循环遍历它的子结点。同时,这个结点所包括的区间就是从e-lowbit(e)+1到e这个区间。
```c
for (int x=1;x<lowbit(e);x<<=1)
```
2.与线段树不同,线段树有一个“顶点”,是整个线段树的根节点,寻求区间可以从顶点逐步向下寻找。但树状数组不一定是一棵树,它可以是多棵树(比如你把上图的C[8]结点删掉看看会长啥样),因此不满足这一特性。所以用树状数组查找区间时,必须将所有根结点都遍历一遍。
有了这些特性,我们就以P3372线段树1为例题进行探讨。
首先得声明一个树状数组。为了实现懒标记,我们用结构体进行声明:
```c
typedef struct
{
ll sum,zhi,lazy;//sum即该结点所包含区间之和,zhi为该结点本身的数值,lazy为懒标记。
}hh;
hh t[100005];
```
值得注意的是之所以要设一个变量存数值,是因为单个的结点数值在线段树中用最底层的结点存储,而这种树状数组的一个结点不仅要存它所管辖的区间和,还要存它本身的数值。
说实话,当我用结构体进行声明时,我就感觉这个树状数组已经不再是一个真正的树状数组了。。。
然后我们要将题目所给的数值输入进去,即:
```c
void add(ll w,ll l)
{
for (int x=l;x<=n;x+=(x&(-x)))
t[x].sum+=w;
}
```
主函数内
```c
for (ll x=1;x<=n;x++)
{
scanf("%lld",&t[x].zhi);
add(t[x].zhi,x);
}
```
然后就剩下区间修改和区间查询的问题了。
区间修改我们可以这样实现。我们先从最高的结点开始遍历,逐步递归:
1.如果该结点所包含的区间被包含在题目所给的区间内,就直接更新区间和、该结点本身的值及懒标记,再返回更新后的区间和。
2.如果存在交集但并不包含,就先下传懒标记,并遍历该结点的所有子结点,对它们进行递归,将它们返回的区间和累加起来,作为该结点更新后的区间和。同时还要特判如果该结点也在题目要求的区间内,则更新该点的数值。最后返回更新后的区间和。
3.如果不存在交集,则返回原区间和。
结合上面那张结构图,我们假设处理一串长度为7(请将图中的C[8]省略)的数列,以将3-6区间内每个数加1为例:
此时树状数组有3个根结点,分别是C[4],C[6],C[7](如果数列长度为8的话,C[8]就能把它们全部包含进去),它们没有父结点,所以是根节点。
用一个循环,将它们分别递归:
```c
for (ll x=n;x>0;x-=(x&(-x)))
insert(k,xi,yi,x);
```
然后我们追踪它递归的过程。
1.当递归到C[7]时(代码中是t[7]),我们发现lowbit(7)=1,所以C[7]结点的左边界就是7-lowbit(7)+1=7,也就是说C[7]结点管辖的区间(7-7)与题目要求的区间(3-6)无交集,直接return。
2.当递归到C[6]时(代码中是t[6]),我们发现lowbit(6)=2,所以C[6]结点的左边界就是6-lowbit(6)+1=5,也就是说C[6]结点管辖的区间(5-6)被完全包含在题目所给的区间(3-6)中,而lowbit(6)=2就是C[6]管辖的区间中的元素数量,所以我们将C[6]的sum加上1乘lowbit(6),lazy和zhi分别加上1,直接return。
3.当递归到C[4]时(代码中是t[4]),我们发现lowbit(4)=4,所以C[4]结点的左边界就是4-lowbit(4)+1=1,也就是说C[4]结点管辖的区间(1-4)与题目给的区间(3-6)有交集,但不完全包含,于是我们先特判C[4]的数值是否在区间内,那么4当然是在3-6之间的,所以我们把C[4]的zhi加1。然后我们把C[4]的sum赋值为C[4]的zhi,并遍历+递归它的各子结点,将它们返回的区间和加进sum,作为sum的更新值。以下为子结点的递归。
3-1.当递归到C[3]时,其管辖的区间完全包含在(3-6)中,因此更新它的sum,lazy和zhi,并返回sum。
3-2.当递归到C[2]时,其管辖的区间(1-2)与(3-6)无交集,不需更改,因此直接返回原sum。
代码如下:
```c
ll insert(ll w,ll l,ll r,ll e)//w是要加的数值,l和r是题目给出的区间,e是当前递归到的树状数组结点。
{
ll left=e-(e&(-e))+1,f=e&(-e);//求左边界left,用f存储lowbit(e),e本身就是区间的右边界。
if (left>r||e<l)
return t[e].sum;//如果无交集,直接返回原区间和。
if (left>=l&&e<=r)
{
t[e].lazy+=w;
t[e].sum+=(e-left+1)*w;
t[e].zhi+=w;
return t[e].sum;//题目要求的区间包含该结点的区间,更新懒标记,区间和及数值,返回更新后的区间和。
}
if (e>=l&&e<=r)
t[e].zhi+=w;//特判如果结点在题目要求的区间内,更新数值。
t[e].sum=t[e].zhi;//先将区间和赋成该结点数值。
pushdown(e);//下传懒标记。
for (ll x=1;x<f;x<<=1)
t[e].sum+=insert(w,l,r,e-x);//累加各子结点区间和。
return t[e].sum;//返回更新后的区间和。
}
```
懒标记是这样下传的:
```c
void pushdown(ll e)
{
if ((e&(-e))>1&&t[e].lazy!=0)//当该结点不是最底层的结点且懒标记不为0时。
{
ll up=e&(-e);//用up记录lowbit(e)。
for (ll x=1;x<up;x<<=1)
{
ll f=e-x,fe;//用f表示子结点,fe表示子结点的lowbit。
fe=(f&(-f));
t[f].sum+=fe*t[e].lazy;
t[f].lazy+=t[e].lazy;
t[f].zhi+=t[e].lazy;
}
t[e].lazy=0;
}
}
```
最后便是区间查询了。
1.如果包含,直接返回区间和。
2.交集但不包含,依次递归子结点并累加区间和,再特判是否需要加上该结点数值,最后返回区间和。
3.无交集则返回0。
关于举例子可以参考上面区间修改的例子,只是无交集的时候,区间修改要返回原sum,而区间查询要返回0。
代码如下:
```c
ll query(ll l,ll r,ll e)
{
ll left=e-(e&(-e))+1,f=(e&(-e));
if (left>r||e<l)
return 0;
pushdown(e);
if (left>=l&&e<=r)
return t[e].sum;
ll summ=0;
if (e>=l&&e<=r)
summ+=t[e].zhi;
for (ll x=1;x<f;x<<=1)
summ+=query(l,r,e-x);
return summ;
}
```
再贴完整代码:
```c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdarg.h>
#include<ctype.h>
typedef long long ll;
ll n,m,xi,yi,k,c;
typedef struct
{
ll sum,zhi,lazy;//sum即该结点所包含区间之和,zhi为该结点本身的数值,lazy为懒标记。
}hh;
hh t[100005];
void add(ll w,ll l)
{
for (int x=l;x<=n;x+=(x&(-x)))
t[x].sum+=w;
}
void pushdown(ll e)
{
if ((e&(-e))>1&&t[e].lazy!=0)
{
ll up=e&(-e);
for (ll x=1;x<up;x<<=1)
{
ll f=e-x,fe;
fe=(f&(-f));
t[f].sum+=fe*t[e].lazy;
t[f].lazy+=t[e].lazy;
t[f].zhi+=t[e].lazy;
}
t[e].lazy=0;
}
}
ll insert(ll w,ll l,ll r,ll e)
{
ll left=e-(e&(-e))+1,f=e&(-e);//求左边界left,用f存储lowbit(e),e本身就是区间的右边界。
if (left>r||e<l)
return t[e].sum;//如果无交集,直接返回原区间和。
if (left>=l&&e<=r)
{
t[e].lazy+=w;
t[e].sum+=(e-left+1)*w;
t[e].zhi+=w;
return t[e].sum;//题目要求的区间包含该结点的区间,更新懒标记,区间和及数值,返回更新后的区间和。
}
if (e>=l&&e<=r)
t[e].zhi+=w;//特判如果结点在题目要求的区间内,更新数值。
t[e].sum=t[e].zhi;//先将区间和赋成该结点数值。
pushdown(e);//下传懒标记。
for (ll x=1;x<f;x<<=1)
t[e].sum+=insert(w,l,r,e-x);//累加各子结点区间和。
return t[e].sum;//返回更新后的区间和。
}
ll query(ll l,ll r,ll e)
{
ll left=e-(e&(-e))+1,f=(e&(-e));
if (left>r||e<l)
return 0;
pushdown(e);
if (left>=l&&e<=r)
return t[e].sum;
ll summ=0;
if (e>=l&&e<=r)
summ+=t[e].zhi;
for (ll x=1;x<f;x<<=1)
summ+=query(l,r,e-x);
return summ;
}
int main(void)
{
scanf("%lld%lld",&n,&m);
for (ll x=1;x<=n;x++)
{
scanf("%lld",&t[x].zhi);
add(t[x].zhi,x);
}
for (ll x=1;x<=m;x++)
{
scanf("%lld%lld%lld",&c,&xi,&yi);
if (c==1)
{
scanf("%lld",&k);
for (ll x=n;x>0;x-=(x&(-x)))
insert(k,xi,yi,x);
}
else
{
ll summ=0;
for (ll x=n;x>0;x-=(x&(-x)))
summ+=query(xi,yi,x);
printf("%lld\n",summ);
}
}
return 0;
}
```
# 乘法范围的区间修改+区间查询
当然,如果只有加减法范围内的区间修改和区间查询,则完全可以用差分公式来解决。
但如果有乘法的区间修改和区间查询呢?这便是P2023维护序列的问题了。这题无法使用差分公式,只能用以上的那种类似于线段树的树状数组。上代码:
```c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdarg.h>
#include<ctype.h>
#define lowbit(e) (e&(-e))
typedef long long ll;
int n,ch,t,g,m;
ll p,c;
typedef struct
{
ll sum,zhi,chen,lzy;
}hh;
hh a[100005];
void add(int w,int l)
{
a[l].sum=w%p;a[l].chen=1;
int r=lowbit(l);
for (int x=1;x<r;x<<=1)
a[l].sum=(a[l].sum+a[l-x].sum)%p;
}
void pushdown(int w)
{
int up=lowbit(w);
if (w>1)
{
for (int x=1;x<up;x<<=1)
{
int f=lowbit((w-x));
a[w-x].sum=(a[w-x].sum*a[w].chen+a[w].lzy*f)%p;
a[w-x].zhi=(a[w-x].zhi*a[w].chen+a[w].lzy)%p;
a[w-x].lzy=(a[w-x].lzy*a[w].chen+a[w].lzy)%p;
a[w-x].chen=(a[w-x].chen*a[w].chen)%p;
}
a[w].lzy=0;a[w].chen=1;
}
}
ll cheng(ll w,int l,int r,int e)
{
int left=e-lowbit(e)+1,up=lowbit(e);
if (left>r||e<l)
{
return a[e].sum;
}
if (left>=l&&e<=r)
{
a[e].sum=(a[e].sum*w)%p;
a[e].zhi=(a[e].zhi*w)%p;
a[e].chen=(a[e].chen*w)%p;
a[e].lzy=(a[e].lzy*w)%p;
return a[e].sum;
}
pushdown(e);
if (e>=l&&e<=r)
a[e].zhi=(a[e].zhi*w)%p;
a[e].sum=a[e].zhi;
for (int x=1;x<up;x<<=1)
{
a[e].sum+=cheng(w,l,r,e-x);
a[e].sum%=p;
}
return a[e].sum;
}
ll insert(ll w,int l,int r,int e)
{
int left=e-lowbit(e)+1,up=lowbit(e);
if (left>r||e<l)
{
return a[e].sum;
}
if (left>=l&&e<=r)
{
a[e].sum=(a[e].sum+(e-left+1)*w)%p;
a[e].zhi=(a[e].zhi+w)%p;
a[e].lzy=(a[e].lzy+w)%p;
return a[e].sum;
}
pushdown(e);
if (e>=l&&e<=r)
a[e].zhi=(a[e].zhi+w)%p;
a[e].sum=a[e].zhi;
for (int x=1;x<up;x<<=1)
{
a[e].sum+=insert(w,l,r,e-x);
a[e].sum%=p;
}
return a[e].sum;
}
ll query(int l,int r,int e)
{
int left=e-lowbit(e)+1,up=lowbit(e);
if (left>r||e<l)
{
return 0;
}
if (left>=l&&e<=r)
{
return a[e].sum;
}
pushdown(e);
ll summ=0;
if (e>=l&&e<=r)
summ+=a[e].zhi;
for (int x=1;x<up;x<<=1)
{
summ+=query(l,r,e-x);
summ%=p;
}
return summ;
}
int main(void)
{
scanf("%d%lld",&n,&p);
for (int x=1;x<=n;x++)
{
scanf("%lld",&a[x].zhi);
add(a[x].zhi,x);
}
scanf("%d",&m);
for (int x=1;x<=m;x++)
{
scanf("%d%d%d",&ch,&t,&g);
if (ch==1)
{
scanf("%lld",&c);
for (int y=n;y>0;y-=lowbit(y))
cheng(c,t,g,y);
}
else if (ch==2)
{
scanf("%lld",&c);
for (int y=n;y>0;y-=lowbit(y))
insert(c,t,g,y);
}
else if (ch==3)
{
ll summ=0;
for (int y=n;y>0;y-=lowbit(y))
summ=(summ+query(t,g,y))%p;
printf("%lld\n",summ);
}
}
return 0;
}
```
# 其它类型:区间平方和
还有P1471方差这道题,考到了区间修改、区间和与区间平方和,做法贴在下面:
```c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdarg.h>
#include<ctype.h>
#define lowbit(e) (e&(-e))
int n,m,ch;
double xi,yi,k;
typedef struct
{
double sum,ping,zhi,lzy;
}hh;
hh c[100005];
void add(int w,double l)
{
int up=lowbit(w);
c[w].sum=l;
c[w].ping=l*l;
for (register int x=1;x<up;x<<=1)
{
c[w].sum+=c[w-x].sum;
c[w].ping+=c[w-x].ping;
}
}
void pushdown(int e)
{
int up=lowbit(e);
if (up>1&&c[e].lzy!=0)
{
for (register int x=1;x<up;x<<=1)
{
c[e-x].ping+=2*c[e-x].sum*c[e].lzy+lowbit((e-x))*c[e].lzy*c[e].lzy;
c[e-x].zhi+=c[e].lzy;
c[e-x].sum+=c[e].lzy*lowbit((e-x));
c[e-x].lzy+=c[e].lzy;
}
c[e].lzy=0;
}
}
void insert(double w,int l,int r,int e)
{
int up=lowbit(e),left=e-lowbit(e)+1;
if (left>r||e<l)
return;
if (left>=l&&e<=r)
{
c[e].ping+=2*c[e].sum*w+lowbit(e)*w*w;
c[e].lzy+=w;
c[e].sum+=w*lowbit(e);
c[e].zhi+=w;
return;
}
pushdown(e);
if (e>=l&&e<=r)
c[e].zhi+=w;
c[e].sum=c[e].zhi;
c[e].ping=c[e].zhi*c[e].zhi;
for (register int x=1;x<up;x<<=1)
{
insert(w,l,r,e-x);
c[e].sum+=c[e-x].sum;
c[e].ping+=c[e-x].ping;
}
}
double getsum(int l,int r,int e)
{
int up=lowbit(e),left=e-lowbit(e)+1;
if (left>r||e<l)
return 0;
if (left>=l&&e<=r)
return c[e].sum;
pushdown(e);
double summ=0;
if (e>=l&&e<=r)
summ+=c[e].zhi;
for (register int x=1;x<up;x<<=1)
summ+=getsum(l,r,e-x);
return summ;
}
double query(double w,int l,int r,int e)
{
int up=lowbit(e),left=e-lowbit(e)+1;
if (left>r||e<l)
return 0;
if (left>=l&&e<=r)
return c[e].ping;
pushdown(e);
double summ=0;
if (e>=l&&e<=r)
summ+=c[e].zhi*c[e].zhi;
for (register int x=1;x<up;x<<=1)
summ+=query(w,l,r,e-x);
return summ;
}
int main(void)
{
scanf("%d%d",&n,&m);
for (register int x=1;x<=n;x++)
{
scanf("%lf",&c[x].zhi);
add(x,c[x].zhi);
}
for (register int x=1;x<=m;x++)
{
scanf("%d%lf%lf",&ch,&xi,&yi);
if (ch==1)
{
scanf("%lf",&k);
for (register int x=n;x>0;x-=lowbit(x))
insert(k,xi,yi,x);
}
else if (ch==2)
{
double summ=0;
for (register int x=n;x>0;x-=lowbit(x))
summ+=getsum(xi,yi,x);
printf("%.4lf\n",summ/(yi-xi+1));
}
else
{
double fangcha=0,summ=0;
for (register int x=n;x>0;x-=lowbit(x))
summ+=getsum(xi,yi,x);
summ/=yi-xi+1;
for (register int x=n;x>0;x-=lowbit(x))
fangcha+=query(summ,xi,yi,x);
fangcha/=yi-xi+1;
fangcha-=summ*summ;
printf("%.4lf\n",fangcha);
}
}
return 0;
}
```
(看了这么多代码,应该能明白我是个C党了吧(逃))
# 时间复杂度分析
可以看出,当我们修改/查询一次时,最坏情况要递归log n层,每层要循环log n次,所以单次修改/查询的时间复杂度为O((log n)^2),同理,修改/查询n次的时间复杂度就是O(n (log n)^2),高于线段树的O(n log n)。
当我提交的时候,它的耗时是线段树做法的2-3倍,内存占用量是线段树做法的二分之一到六分之一不等,可以说,这是一种“时间换空间”的做法。
#### 但考虑到考试中对于时间复杂度的要求很高,但对于空间复杂度的要求较为宽松,因此考试中更推荐用线段树。本文仅作为证明树状数组能够实现线段树功能的一篇文章,无太大实用价值,仅供参考。
```c
//【完】
```