引入
如果我要不断访问一个区间的最大值,而且要不断更新点的值,那么树状数组就难办了。所以我们引入线段树这一数据结构来解决这些问题。
线段树是基于分治思想的二叉树,是用来解决在线区间更改查询等问题的。让我们来了解一下它的原理吧。
原理
给出一个区间[1,5],我们将它二分,mid=(1+5)/2=3,左区间[l,mid]=[1,3],右区间[mid+1,r]=[4,5]。
又将左区间二分,mid=(1+3)/2=2,左区间的左区间是[1,2],右区间是[2,3],又二分,直到分到l==r为止,以此类推,我们建一个树。
我们建立这样的一棵二叉树,而且除了叶子节点外,是一棵满二叉树,那么我们可以把它们的下标轻松写出来,像这样:
可以看出,父节点为i,那么左儿子就是i2(i<<1),右儿子是i2+1(i<<1|1),这样就表示出了每一个点的下标了。我们发现这一棵二叉树,那么它的空间是很大的,所以我们开tree[MAXN]要是序列数的至少4倍 才不会爆。
简单应用
题目描述:给出一个序列,可以对它的某个点更新,查询某个区间的Max。
要让我们单点更新,区间求Max。那么我们来分步骤解释此题。
1.建树
对于它给的初始值我们建立出线段树。
在这里,我用#define lc k<<1,#define rc k<<1|1,表示左右儿子
struct node
{
int l,r,mx;
}t[N<<2];//开4倍大小
inline void build(int k,int l,int r)//k是下标,l是该节点的区间的左节点,r是右节点
{
t[k].l=l;t[k].r=r;
if(l==r) //如果是叶子节点,赋值,区间最大值就是它本身
{
t[k].mx=a[l];
return;
}
int mid=(l+r)>>1;
build(lc,l,mid);//递归建树左右儿子
build(rc,mid+1,r);
t[k].mx=max(t[lc].mx,t[rc].mx);//回溯回来更新最大值
}
2.单点更新
要更新点,我们肯定要去找到这个点,还是用递归的方法找。
inline void update(int k,int x,int val)
{
if(t[k].l==t[k].r) //如果到了叶子节点,就找到了该点
{
t[k].mx=val;
return;
}
int mid=(t[k].l+t[k].r)>>1;
if(x<=mid) update(lc,x,val);//在左区间找
else update(rc,x,val);//在右区间找
t[k].mx=max(t[lc].mx,t[rc].mx);//更新
}
3.查询
查询某个区间的最大值,考虑是否线段树的区间包含了这个区间或这个区间跨越了多个线段树的区间。
inline int query(int k,int l,int r)
{
if(l<=t[k].l && t[k].r<=r) return t[k].mx;//查询的区间包含了这段t[k].l~t[k].r,那么返回这段的最大值
int mid=(l+r)>>1;
int Max=0;
if(l<=mid) Max=max(query(lc,l,r)); //如果有在左区间
if(r>mid) Max=max(query(rc,l,r));//如果有在右区间
return Max;
}
这样我们就已经完成了这个题目。
下面我们来看一道新的题。
lazy_tag
lazy_tag懒惰标记,如果有一个题,要让我们区间更新,区间求和。每次更新我们都要更改这里面所有的值,而有些值我们可能更新了却不查询,那就白白浪费了时间,所以我们引入lazy_tag来进行标记,等到要用这个点后才去更新,(的确很懒) ,那么我们来看看代码实现。
如果我更新了遇到了标记,那么我就把标记向2个左右儿子传,并取消自己的标记。
传送门luoguP3372
我们还是分步骤进行
1.建树
struct node
{
int l,r,w,lz;//增加一个lz标记
}t[N<<2];
inline void build(int k,int l,int r)
{
t[k].l=l;t[k].r=r;
if(l==r)
{
t[k].w=a[l];
return;
}
int mid=(l+r)>>1;
build(lc,l,mid);
build(rc,mid+1,r);
t[k].w=t[lc].w+t[rc].w; //改为求和
}
2.更新
更新就要用到lazy_tag了。
另外还要一个函数pushdown来处理lazy_tag。
PS:#define ll long long
inline void pushdown(int k)
{
if(t[k].lz)//如果有标记
{
t[lc].w+=t[k].lz*(ll)(t[lc].r-t[lc].l+1);//因为是更新整个区间,所以就将加的数提出来乘上总的数的个数
t[rc].w+=t[k].lz*(ll)(t[rc].r-t[rc].l+1);
t[lc].lz+=t[k].lz;//下传
t[rc].lz+=t[k].lz;
t[k].lz=0;//清空自己的
}
}
inline void update(int k,int l,int r,int v)
{
if(t[k].l>=l && t[k].r<=r) //如果更新的区间包含了这段区间
{
t[k].w+=(ll)v*(t[k].r-t[k].l+1);更新这段区间的和
t[k].lz+=v;得到标记
return;
}
pushdown(k);//下传标记
int mid=(t[k].l+t[k].r)>>1;
if(l<=mid) update(lc,l,r,v);
if(r>mid) update(rc,l,r,v);
t[k].w=t[lc].w+t[rc].w;
}
3.查询
查询也差不多,仍然要更新标记。
ll query(int k,int l,int r)
{
if(t[k].l>=l && t[k].r<=r) return t[k].w;
pushdown(k);
int mid=(t[k].l+t[k].r)>>1;
ll ans=0;
if(l<=mid) ans+=query(lc,l,r);
if(r>mid) ans+=query(rc,l,r);
return ans;
}
最后我们就完美的解决了这道题。
线段树还有很多功能,任重而道远 。