线段树(1)

在某些题目中,要求我们对数列的某些区间进行操作,例如求区间和或者区间内每个数加减某个数。一般来说,我们求区间和一般采用前缀和,时间复杂度是 O ( 1 ) O(1) O(1)。而修改区间只能暴力修改,时间复杂度是 O ( n ) O(n) O(n),当我们同时需要做这两件事的时候,每修改一次区间,前缀和也要随之改变,时间复杂度会变成 O ( n ) O(n) O(n)。所以我们引入线段树这种数据结构来帮助我们对区间进行快速操作。
先看一道模板题:洛谷P3327【模板】线段树1

1 如何创建一颗线段树

首先我们要了解线段树的结构,线段树一定是一颗二叉树,每个结点代表了某段区间所有元素的和,例如 [ 1 , 5 ] [1,5] [1,5]代表了 ∑ i = 1 5 a [ i ] \sum^5_{i=1} a[i] i=15a[i];而某个结点 [ l , r ] [l,r] [l,r]的子节点区间是 [ l , l + r 2 ] [l,\frac{l+r}{2}] [l,2l+r](左子节点)和 [ l + r 2 + 1 , r ] [\frac{l+r}{2}+1,r] [2l+r+1,r](右子节点)。
而每个结点都有一个编号 p p p,观察完全二叉树的规律可以知道,左子节点的编号一定是 p ∗ 2 p*2 p2,而右子节点的编号一定是 p ∗ 2 + 1 p*2+1 p2+1,我们用数组 t r e e tree tree来存储该结点的值,即 t r e e [ p ] = ∑ i = l r a [ i ] tree[p]=\sum^r_{i=l} a[i] tree[p]=i=lra[i],所以一定也有 t r e e [ p ] = t r e e [ p ∗ 2 ] + t r e e [ p ∗ 2 + 1 ] tree[p]=tree[p*2]+tree[p*2+1] tree[p]=tree[p2]+tree[p2+1]。所以我们可以递归的创造一颗线段树,递归的边界是 l = r l=r l=r,也就是该结点代表的区间长度为 1 1 1,此时由 t r e e [ p ] = a [ l ] = a [ r ] tree[p]=a[l]=a[r] tree[p]=a[l]=a[r]
代码如下:
void build(int l,int r,int p){
if(l==r){
tree[p]=a[l];
return
}
int mid=(l+r)/2;
build(l,mid,p*2);//构造左子节点
build(mid+1,r,p*2+1);//构造右子节点
tree[p]=tree[p*2]+tree[p*2+1];
}

2 如何进行区间修改

区间修改是线段树真正节省时间复杂度的地方,我们并不需要真正修改数列中每一个数的值,我们的修改只是为了欺骗查询区间和。譬如我们现在有数列 a [ 1 ] 到 a [ 5 ] , 假 设 值 全 为 1 a[1]到a[5],假设值全为1 a[1]a[5]1,我们显然可以构建如下树关系
[ 1 , 5 ] ( 1 ) → [ 1 , 3 ] ( 2 ) , [ 4 , 5 ] ( 3 ) [ 1 , 3 ] ( 2 ) → [ 1 , 2 ] ( 4 ) , [ 3 , 3 ] ( 5 ) [ 4 , 5 ] ( 3 ) → [ 4 , 4 ] ( 6 ) , [ 5 , 5 ] ( 7 ) [ 1 , 2 ] ( 4 ) → [ 1 , 1 ] ( 8 ) , [ 2 , 2 ] ( 9 ) ( p ) 为 结 点 编 号 [1,5](1)\rightarrow[1,3](2),[4,5](3)\\ [1,3](2)\rightarrow[1,2](4),[3,3](5)\\ [4,5](3)\rightarrow[4,4](6),[5,5](7)\\ [1,2](4)\rightarrow[1,1](8),[2,2](9)\\ (p)为结点编号 [1,5](1)[1,3](2),[4,5](3)[1,3](2)[1,2](4),[3,3](5)[4,5](3)[4,4](6),[5,5](7)[1,2](4)[1,1](8),[2,2](9)p
我们现在要将 [ 1 , 4 ] [1,4] [1,4]内的所有值加上 1 1 1,我们想要查询的时候 [ 1 , 4 ] [1,4] [1,4]这个区间的元素确实表现出都加上 1 1 1的特证即可,而不是真的将 a [ 1 ] 到 a [ 4 ] a[1]到a[4] a[1]a[4]都加 1 1 1,我们发现结点 3 3 3的区间包含在我们的目标区间内,且结点 3 3 3的长度为 3 − 1 + 1 = 3 3-1+1=3 31+1=3,所以只需把 t r e e [ 3 ] + = 3 ∗ 1 tree[3]+=3*1 tree[3]+=31即可对 a [ 1 ] 到 a [ 3 ] a[1]到a[3] a[1]a[3]一起修改,再效仿修改结点 6 6 6即可。所以我们在递归线段树的时候有三种区间,一种是该区间和目标区间无交集,这种区间什么都不需要操作,可以直接 r e t u r n return return,一种区间包含在目标区间中,这时可以对整个区间进行操作,第三种情况是区间和目标区间有交集,但不被包含,则把该区间不断对半分,便可以变成前两种区间。
void update(int l,int r,int cl,int cr,int d,int p){//目前正在处理的区间是[cl,cr],目的是[l,r]上的元素加上d
if(cl>r||cr<l) return;//第一种区间
if(cl>l&&cr<r) tree[p]+=d*(cr-cl+1);//第二种区间
else{
int mid=(cl+cr)/2;
update(l,r,cl,mid,d,p*2);
update(l,r,mid+1,cr,d,p*2+1);
tree[p]=tree[p*2]+tree[p*2+1]; }
在查询时,可以效仿这种方法将区间分为三种(因为查询需要返回值,函数类型不再用void.
int query(int l,int r,int cl,int cr,int p){
if(cl>r||cr<l) return 0;//第一种区间
if(cl>l&&cr<r) return tree[p];//第二种区间
else{
int mid=(cl+cr)/2;
return query(l,r,cl,mid,p*2)+query(l,r,mid+1,cr,p*2+1;
}
我们马上会发现这种做法的问题所在,如上举例
[ 1 , 5 ] ( 1 ) → [ 1 , 3 ] ( 2 ) , [ 4 , 5 ] ( 3 ) [ 1 , 3 ] ( 2 ) → [ 1 , 2 ] ( 4 ) , [ 3 , 3 ] ( 5 ) [ 4 , 5 ] ( 3 ) → [ 4 , 4 ] ( 6 ) , [ 5 , 5 ] ( 7 ) [ 1 , 2 ] ( 4 ) → [ 1 , 1 ] ( 8 ) , [ 2 , 2 ] ( 9 ) ( p ) 为 结 点 编 号 [1,5](1)\rightarrow[1,3](2),[4,5](3)\\ [1,3](2)\rightarrow[1,2](4),[3,3](5)\\ [4,5](3)\rightarrow[4,4](6),[5,5](7)\\ [1,2](4)\rightarrow[1,1](8),[2,2](9)\\ (p)为结点编号 [1,5](1)[1,3](2),[4,5](3)[1,3](2)[1,2](4),[3,3](5)[4,5](3)[4,4](6),[5,5](7)[1,2](4)[1,1](8),[2,2](9)p
我们现在要将 [ 1 , 4 ] [1,4] [1,4]内的所有值加上 1 1 1,我们想要查询的时候 [ 1 , 4 ] [1,4] [1,4]这个区间的元素确实表现出都加上 1 1 1的特证即可,而不是真的将 a [ 1 ] 到 a [ 4 ] a[1]到a[4] a[1]a[4]都加 1 1 1,我们发现结点 3 3 3的区间包含在我们的目标区间内,且结点 3 3 3的长度为 3 − 1 + 1 = 3 3-1+1=3 31+1=3,所以只需把 t r e e [ 3 ] + = 3 ∗ 1 tree[3]+=3*1 tree[3]+=31即可对 a [ 1 ] 到 a [ 3 ] a[1]到a[3] a[1]a[3]一起修改,再效仿修改结点 6 6 6即可。如果我现在需要查询区间 [ 1 , 2 ] [1,2] [1,2]的值,我们发现这段区间根本没有被我们修改,我们整体修改完 [ 1 , 3 ] [1,3] [1,3]就停止了 u p d a t e update update程序。为了解决这个问题,我们引入了延迟标记(懒标记)。

3 延迟标记

遇到上述问题,暴力的想法是将区间包含于 [ 1 , 4 ] [1,4] [1,4]的所有结点的值都改变,但这样显然时间复杂度比线性数组操作还大,我们引入延迟标记来解决问题,先看代码再讲解
void update(int l,int r,int cl,int cr,int d,int p){//目前正在处理的区间是[cl,cr],目的是[l,r]上的元素加上d
if(cl>r||cr<l) return;//第一种区间
if(cl>l&&cr<r){
tree[p]+=d*(cr-cl+1);//第二种区间
mark[p]=d;
}
else{
int mid=(cl+cr)/2;
mark[p*2]+=mark[p];
mark[p*2+1]+=mark[p];//懒标记的传递
tree[p*2]+=mark[p];
tree[p*2+1]+=mark[p];
mark[p]=0;//不需要懒标记维护
update(l,r,cl,mid,d,p*2);
update(l,r,mid+1,cr,d,p*2+1);
tree[p]=tree[p*2]+tree[p*2+1]; }

int query(int l,int r,int cl,int cr,int p){//查询区间从lr内所有元素的和
if(cl>r||cr<l){
return 0;
}
if(cl>=l&&cr<=r){
return tree[p];
}
else{
int mid=(cl+cr)/2;
mark[p*2]+=mark[p];
mark[p*2+1]+=mark[p];//懒标记的传递
tree[p*2]+=mark[p];
tree[p*2+1]+=mark[p];
mark[p]=0;//不需要懒标记维护
return query(l,r,cl,mid,p*2)+query(l,r,mid+1,cr,p*2+1);
}
}
我们来模拟一下延迟标记的作用,仍沿用上面那个例子
[ 1 , 5 ] ( 1 ) → [ 1 , 3 ] ( 2 ) , [ 4 , 5 ] ( 3 ) [ 1 , 3 ] ( 2 ) → [ 1 , 2 ] ( 4 ) , [ 3 , 3 ] ( 5 ) [ 4 , 5 ] ( 3 ) → [ 4 , 4 ] ( 6 ) , [ 5 , 5 ] ( 7 ) [ 1 , 2 ] ( 4 ) → [ 1 , 1 ] ( 8 ) , [ 2 , 2 ] ( 9 ) ( p ) 为 结 点 编 号 [1,5](1)\rightarrow[1,3](2),[4,5](3)\\ [1,3](2)\rightarrow[1,2](4),[3,3](5)\\ [4,5](3)\rightarrow[4,4](6),[5,5](7)\\ [1,2](4)\rightarrow[1,1](8),[2,2](9)\\ (p)为结点编号 [1,5](1)[1,3](2),[4,5](3)[1,3](2)[1,2](4),[3,3](5)[4,5](3)[4,4](6),[5,5](7)[1,2](4)[1,1](8),[2,2](9)p当我们对 [ 1 , 3 ] [1,3] [1,3]区间进行区间修改时同时对其进行标记, m a r k [ 2 ] = 1 mark[2]=1 mark[2]=1它的意义是,标记 2 2 2结点上的所有元素 + 1 +1 +1,同时根据我们的程序,我们标记 m a r k [ 6 ] = 1 , m a r k [ 7 ] = 1 mark[6]=1,mark[7]=1 mark[6]=1,mark[7]=1,当我们查询区间 [ 1 , 2 ] [1,2] [1,2]时,当我们目前区间是 [ 1 , 3 ] [1,3] [1,3]区间时,根据我们的程序我们会将延迟标记向下传递 m a r k [ 4 ] = 1 , m a r k [ 5 ] = 1 mark[4]=1,mark[5]=1 mark[4]=1,mark[5]=1并将 t r e e [ 4 ] tree[4] tree[4] t r e e [ 5 ] tree[5] tree[5]的每一个元素加上 m a r k [ 2 ] mark[2] mark[2],同时清零该层标记 m a r k [ 2 ] = 0 mark[2]=0 mark[2]=0,我们对 t r e e [ 4 ] tree[4] tree[4]的修改成功修改了 [ 1 , 2 ] [1,2] [1,2]的值,这就是延迟标记的作用,这一层的标记延迟作用于下一层,所以称之为延迟标记,我们在修改区间时用 m a r k mark mark记着我们这个区间内的所有元素要加上一个数,而在我们查询的时候再将 m a r k mark mark拿出来使用。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值