“懒”标记の介绍
“懒”标记の好处
“懒”标记的定义方式
区间更新
思想
题目の练习
在此之前,我们先引入一个概念——
懒标记的作用,就是维护每一个节点的参数
懒标记顾名思义,就是要懒嘛!这里的懒是运用拟人的手法,懒标记就是记录当前状态,在更新操作的时候将这个状态下传
形象地说,懒标记就像寄存包裹一样,先把这个包裹(数据)存在这里,等有需要的时候再取(下传)
那么,为什么要用懒标记呢?
试想一下,在还没有引入懒标记的时候,如果要频繁地将线段树的节点更新等做一些无用、重复操作,那么对于线段树而言,其时间复杂度还不如爆搜要小,因此,我们在这里引入了懒标记的概念
直接在结构体里边改就行了
struct Seg{
int l,r;
int val;//维护的值
int lazy;//懒标记,我习惯写作tag,但这里方便理解就用lazy表示
};
回到刚才的问题,那么区间更新应该怎么操作呢?
在没学过下面的内容之前,很容易想到将单点更新套入一个for循环里面嘛!
我可以告诉你这种算法的时间复杂度是O(n^2 log n)很明显碰到稍微大一点的数据肯定爆
那么什么才是区间更新的正确姿势呢?
我们在这里为了方便,将想要更改的区间称之为母区间,包含在这里面的若干个区间称之为子区间
其实核心思想和区间查询差不多,主要是找到一个表示为子区间的结点就行了,没必要再向下更新!
如图所示:(我们将更改 [4,8] )
找到结点[4,4]and[5,8]就行了(图中用红色标识),不用再向下找
我们先设计找到后的代码:
找到一个结点表示的区间包含在目标区间内后,先更改这个结点所维护的信息(因为你要更新它的母区间啊)
问题来了,请思考一下下面两种方案哪种正确呢?
A.当然是一边更新一边下传啊!!!选我!!!
B.当然是先更新,等下次更新的时候再传啊!!!选我!!!
请自行思考1min!
盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~
盯~盯~——————————————————————————————————————————————思考线
盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~盯~
答案是A!
理由很简单
试想一下,如果本次更新的区间和下次不一样呢?!而且下次更新的区间刚好处于结点的两个子节点呢?!
如图:第一次更新[5,6],第二次更新[7,8]
于是乎选B的朋友在下传的时候就会出现混乱,之前要下传的和现在更新的好难处理啊!!!
如果选A的话,在更新的时候直接下传给左右子结点,多好!!!
在更改值的时候请注意,因为有些不是叶节点所以要注意维护的值应该怎么变化!
在“将[l,r]的值全部改为z并且求出某段的和”这种题目中,很明显要维护的值为区间之和
那么val(表示区间之和)=len(查到的子区间长度)* z(要修改的值)
小学乘法别问我为什么
即val=len*z
其中len=r-l+1
紧接着就更改它的懒标记,将其更改为想要更改的值
别忘了return ;
紧接着是下传操作(pushdown)
很多dalao用一个函数写了,这里我喜欢直接丢进更新函数中
就是将lazy标记下传给左右子,将左右子的维护的状态修改
情况和区间查询类似
将区间“腰斩”!!!
从中间折了之后往左右两边查,当然从中间查也分左右两种情况
图片请参照上一篇里面的区间查询
在查找之后记得向上传(pushup)
其实就是让母结点递归更新
于是乎,一个完整的更新代码就出来了!!!
void update(int k,int l,int r,int x,int y,int z)
{
if(x>=l && y<=r)//找到子区间
{
tr[k].val=(r-l+1)*z;
tr[k].lazy=z;
return ;
}
//pushdown
int mid=(l+r)>>1;
if(tr[k].lazy)
{
tr[k>>1|1].lazy=tr[k].lazy,tr[k>>1].lazy=tr[k].lazy;
tr[k>>1|1].val=(mid-l+1)*tr[k].lazy;
tr[k>>1].val=(r-mid)*tr[k].lazy;
tr[k].lazy=0;//记得给母结点的懒标记清零
}
//find,接着找下一个子区间
if(y <= mid)
update(k>>1,l,mid,x,y,z);
else if(x > mid)
update(k>>1|1,mid+1,r,x,y,z);
else
{
update(k>>1,mid,x,y,z);
update(k>>1|1,mid+1,r,x,y,z);
}
//pushup
tr[k].val=tr[k>>2|1].val+tr[k>>1].val;
}
以前我也是直接写k作为当前结点下标,多方便!但是写到后面再加上看了dalao博客我才觉得记录左右端点太浪费空间了!
所以这个函数参数里的l,r就是当前结点的左右端点,建议大家也这样写。
注:
这里的k>>1 相当于 k*2代表的是左子节点的下标
k>>1|1相当于k*2+1代表的是右子节点的下标
下面的都是模板题
hdu1698
luogu3372
luogu3373(相对偏难)
请dalao多多指点!!!