浅谈线段树 ——因为是蒟蒻
置顶 树状数组 基础篇
首先,先解决一个问题,线段树的结构以及工作原理。先放个图片看看。
##这是我们梦中的线段树
这是一个满二叉树,挺好看的 。再向下延伸的节点就代表[1,1],[2,2]~~~~[n,n]的单点
那么我们为什么要用它,只因为他速度快且有树状数组所代替不了的功能,(但没有树状数组快有时)
用法与区别
线段树和树状数组的基本功能都是在某一满足结合律的操作(比如加法,乘法,最大值,最小值)下,O(logn)的时间复杂度内修改单个元素并且维护区间信息。不同的是,树状数组只能维护前缀“操作和”(前缀和,前缀积,前缀最大最小),而线段树可以维护区间操作和。但是某些操作是存在逆元的,这样就给人一种树状数组可以维护区间信息的错觉:维护区间和,模质数意义下的区间乘积,区间xor和。能这样做的本质是取右端点的前缀和,然后对左端点左边的前缀和的逆元做一次操作,所以树状数组的区间询问其实是在两次前缀和询问。所以我们能看到树状数组能维护一些操作的区间信息但维护不了另一些的:最大/最小值,模非质数意义下的乘法,原因在于这些操作不存在逆元,所以就没法用两个前缀和做。
那么回归正题,现实中的线段树呢?
假设有一个数组tree[16]
## 现实中的线段树
可以看到,tree数组下标为1的代表着2,3下标的和,那么2又代表着4,5的和,以此类推。8,9,10等叶子节点代表的就是原数组的值。
注意:那么以叶子节点为原数组可以看到,一个空间为0~15的tree数组现在能存下8个,那么是不是开两倍与它的空间就可以存下原数组里的所有数,告诉你,绝对不是,由于是2的几次方存储,万一数据有意卡你,再原本下标就大的叶子节点向下拓展2倍就会爆炸,但在原数组的数据中只会多一个数据,所以干脆来个简单粗暴的解决方法,开到4*n的空间,也就是把下一层的空间全给开了。
那么用递归建树就好了。
g为原数组,t为树的数组
void bulid(int d,int l,int r)
{
if(l==r) {t[d]=g[l];return;}
int mid=(l+r)>>1;
bulid(d<<1,l,mid);
bulid(d<<1|1,mid+1,r);
t[d]=t[d<<1]+t[d<<1|1];
}
注意:代码中d<<1|1意思是将d*2+1,由于刚刚左移以为,所以末尾必定是0,或上1等效于加一
insert函数,往一个区间插入值
注释 d代表的是树的数组的下标,l和r代表的是该数组所管的区间。
xx和yy代表的是需要加入的值k的区间
void ins(int xx,int yy,int d,int l,int r)
{
if(xx>=l&&yy<=r) t[d]+=k*(yy-xx+1);
if(xx==l&&yy==r){lt[d]+=k;return ;}
int mid=(l+r)>>1;
if(xx>mid) ins(xx,yy,d<<1|1,mid+1,r);
else if(yy<=mid) ins(xx,yy,d<<1,l,mid);
else ins(xx,mid,d<<1,l,mid),ins(mid+1,yy,d<<1|1,mid+1,r);
}
那么里面的lt数组便是~~~~
咳咳,现在介绍一个非常nbnb的东西--------懒标记(lazy_bag)
顾名思义,懒嘛,就是少做点东西,那么它又是怎么运作的呢?
我们发现,,如果每次加入值进去的话,那么每次都要进行修改,但是有的时候这些修改的值又用不到所以我们可以用一个数组先暂时存下可能要进行修改的值。且其下标的意思和树的数组的下标意思是一样的。
那么如果要跟新值怎么做呢,那就到查询数函数中体现了。
add为最终的结果
void query(int xx,int yy,int d,int l,int r)
{
if(xx==l&&yy==r){add+=t[d];return;}
if(lt[d]) dn(d,l,r,lt[d]);
int mid=(l+r)>>1;
if(xx>mid) query(xx,yy,d<<1|1,mid+1,r);
else if(yy<=mid) query(xx,yy,d<<1,l,mid);
else query(xx,mid,d<<1,l,mid),query(mid+1,yy,d<<1|1,mid+1,r);
}
那么dn函数就是懒标记发挥作用的函数了。我们知道,如果我们不满足第一个if,那么其意思就是还要向下查询,这时如果你这个树节点的懒标记有值的话,就要往下一层赋值
上代码!
lt1是要赋的值。
void dn(int d,int q,int p,int lt1)
{
int mi=(q+p)>>1;
lt[d<<1]+=lt1;
lt[d<<1|1]+=lt1;
t[d<<1]+=lt1*(mi-q+1);
t[d<<1|1]+=lt1*(p-mi);
lt[d]=0;
}
好,那么大家可以去看看luoguP3372,并尝试一下线段树的乐趣。
以下附上AC代码:
#include<iostream>
#include<cstdio>
#define N 100001
#define ll long long
using namespace std;
ll n,m;
ll g[N],t[N<<2],s,x,y,k,lt[N<<2],add;
void bulid(int d,int l,int r)
{
if(l==r) {t[d]=g[l];return;}
int mid=(l+r)>>1;
bulid(d<<1,l,mid);
bulid(d<<1|1,mid+1,r);
t[d]=t[d<<1]+t[d<<1|1];
}
//d代表的是树的数组的下标,l和r代表的是该数组所管的区间。
//xx和yy代表的是需要加入的值k的区间
void ins(int xx,int yy,int d,int l,int r)
{
if(xx>=l&&yy<=r) t[d]+=k*(yy-xx+1);
if(xx==l&&yy==r){lt[d]+=k;return ;}
int mid=(l+r)>>1;
if(xx>mid) ins(xx,yy,d<<1|1,mid+1,r);
else if(yy<=mid) ins(xx,yy,d<<1,l,mid);
else ins(xx,mid,d<<1,l,mid),ins(mid+1,yy,d<<1|1,mid+1,r);
}
void dn(int d,int q,int p,int lt1)
{
int mi=(q+p)>>1;
lt[d<<1]+=lt1;
lt[d<<1|1]+=lt1;
t[d<<1]+=lt1*(mi-q+1);
t[d<<1|1]+=lt1*(p-mi);
lt[d]=0;
}
void query(int xx,int yy,int d,int l,int r)
{
if(xx==l&&yy==r){add+=t[d];return;}
if(lt[d]) dn(d,l,r,lt[d]);
int mid=(l+r)>>1;
if(xx>mid) query(xx,yy,d<<1|1,mid+1,r);
else if(yy<=mid) query(xx,yy,d<<1,l,mid);
else query(xx,mid,d<<1,l,mid),query(mid+1,yy,d<<1|1,mid+1,r);
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",&g[i]);
bulid(1,1,n);
for(int i=1;i<=m;i++)
{
scanf("%lld",&s);
if(s==1)
{
scanf("%lld%lld%lld",&x,&y,&k);
ins(x,y,1,1,n);
}
else
{
scanf("%lld%lld",&x,&y);
query(x,y,1,1,n);
printf("%lld\n",add);
add=0;
}
}
return 0;
}
如有不足,请多指教!