1 条题解
-
0
自动搬运
来自洛谷,原作者为

Moon_Night
五里月徘徊搬运于
2025-08-24 22:30:26,当前版本为作者最后更新于2023-07-18 13:20:12,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
题目戳这里
本篇题解会详细讲解本题的思考过程,虽然并不是新方法,但是讲解详细,请耐心食用。
这道题需要用用线段树实现
区间加,区间乘,区间和的查询,以及锁定区间(忽略其某时间范围内的修改)。与模板有区别的地方只有最后一个功能,也就是操作3。我们很容易想到,线段树多维护一个封锁结束的时间,然后在修改时候判断封锁是否结束。于是我们有了:错误解法--多维护一个封锁时间
(赶时间请跳过此部分)
我们考虑在线段树中维护一个封锁结束时间,然后这道题就变得非常简单(小看了这道题),只需在下传懒标记/修改时判断节点是否解封,决定是否进行操作。于是:
我在修改操作的
if(x<=l&&r<=y)中添加了以下代码:if(tim<=block[root]) return; // tim 为现在的时间 block[i] 为 i 节点结束封锁时间在判断是否进行
pushdown时,我们需要分类:- 未封锁的父亲,已封锁的儿子。此时显然不需要
pushdown,在封锁前,就把父亲需要传给儿子的下传完了,现在父亲的懒标记里是封锁后的操作,所以不需要pushdown。 - 已封锁的父亲,已封锁的儿子。此时可以
pushdown,在封锁后,所有不应下传的都没有下传到父亲,所以父亲能传给儿子的只有封锁前的懒标记 - 其余情况无需下传。
于是有以下代码:
// add 为加法懒标记,times 为乘法懒标记 // block 为封锁时间 void pushdown(ll root,ll l,ll r,ll tim){ block[ls]=max(block[ls],block[root]); block[rs]=max(block[rs],block[root]); if(block[ls]<tim||(block[root]>=tim)){ (tree[ls]*=times[root])%=P; (tree[ls]+=add[root]*(mid-l+1)%P)%=P; (add[ls]*=times[root])%=P; (add[ls]+=add[root])%=P; (times[ls]*=times[root])%=P; } if(block[rs]<tim||(block[root]>=tim)){ (tree[rs]*=times[root])%=P; (tree[rs]+=add[root]*(r-mid)%P)%=P; (add[rs]*=times[root])%=P; (add[rs]+=add[root])%=P; (times[rs]*=times[root])%=P; } add[root]=0; times[root]=1; return; }看起来十分合理,然后我们再简单地维护一下
block[i]void update_block(ll root,ll l,ll r,ll x,ll y,ll tim,ll z){ if(l>r) return; if(x<=l&&r<=y){ block[root]=max(block[root],z); return; } pushdown(root,l,r,tim); if(x<=mid) update_block(ls,l,mid,x,y,tim,z); if(y>mid) update_block(rs,mid+1,r,x,y,tim,z); return; }然后我们高高兴兴地跑样例,然后大悲!样例二怎么 了?!
交上去:
为什么?!
我们冷静思考,惊恐的发现:如果在节点 i解封前,父节点被修改,但懒标记未下传,在解封后,父节点将懒标记传给节点i,那么i就成功地在封锁的时间被修改了!除了这个问题,还有很多问题。如:当一个区间有一部分被修改时,区间加加的还是r-l+1个吗?我们又要多维护一个值。那么继续修改就会像打补丁一样,十分困难,这里改完那里有问题,因为我们小看了这道题。于是,我们的尝试宣告失败。正确解法--拆分封锁与解封操作
那么我们怎么处理
封锁区间呢?我们认真思考或参考题解,得到了另一个方法:将这个操作变成两个操作,封锁+解封。我们来思考一下细节。考虑一次区间加(
[l,r]+=z):这个整区间里若有一部分还在封锁中,那么就不能将区间直接
+=z*(r-l+1), 而是+=未封锁元素个数*z于是,我们再维护一个未封锁元素个数。考虑一次区间乘(
[l,r]*=z):这个整区间里若有一部分还在封锁中,那么就不能直接
*=z,而是只把未封锁的部分*=z,而为了复杂度,我们不可能现场统计未封锁元素的和,于是我们决定分开维护封锁部分的区间和,未封锁部分的区间和。然后,考虑多个封锁操作重叠:
我们不是很愿意将封锁操作合并,这很麻烦,于是就直接将多个封锁叠加,等到没有一个封锁才继续下传操作。所以维护的
封锁情况不应该是0/1而是--/++。至此,我们要维护的所有东西如下:
struct segment{ ll one,two,len,add,times,block; /* one 代表未封锁的和 two 代表封锁区域的和 len 代表未封锁元素个数 add 为加法懒标记 times为乘法懒标记 block 为目前被封锁次数 */ };想自己尝试的同志可以先自己去写代码了。
接下来,配合代码食用,详细的注释与
有些晦涩难懂的代码结合,比口述好理解得多。#include<iostream> #include<cmath> #include<cstring> #include<algorithm> #include<vector> #define ls (2*root) #define rs (2*root+1) #define mid (l+((r-l)>>1)) using std::cin; using std::cout; using std::max; using std::swap; using std::vector; typedef long long ll; const ll N=2e5+5,P=1e9+7; struct Block{ ll l,r; }; // 封锁的区间 ll n,m,a[N]; vector<Block> que[N]; // que[i]保存在 i 时刻,需要解封的区间 struct segment{ ll one,two,len,add,times,block; /* one 代表未封锁的和 two 代表封锁区域的和 len 代表未封锁元素个数 add 为加法懒标记 times为乘法懒标记 block 为目前被封锁次数 */ }; struct Segment_Tree{ segment tr[4*N]; void pushdown(ll root){ // 下传懒标记 if(tr[ls].block==0){ // 若整个区间被封锁,不能下传懒标记 tr[ls].one=(tr[ls].one*tr[root].times%P+tr[root].add*tr[ls].len%P)%P; tr[ls].add=(tr[ls].add*tr[root].times%P+tr[root].add)%P; tr[ls].times=(tr[ls].times*tr[root].times)%P; } if(tr[rs].block==0){ tr[rs].one=(tr[rs].one*tr[root].times%P+tr[root].add*tr[rs].len%P)%P; tr[rs].add=(tr[rs].add*tr[root].times%P+tr[root].add)%P; tr[rs].times=(tr[rs].times*tr[root].times)%P; } tr[root].add=0,tr[root].times=1; return; } void pushup(ll root){ // 子节点更新父亲 if(tr[root].block==0){ // 若整个区间被封锁,那么子节点的one没有被修改,不能更新父亲 tr[root].one=(tr[ls].one+tr[rs].one)%P; tr[root].two=(tr[ls].two+tr[rs].two)%P; tr[root].len=tr[ls].len+tr[rs].len; }// 不过在add等函数中,tr[root].block>0已经return了 return; } void build(ll root,ll l,ll r){ // 初始化 if(l>r) return; tr[root].times=1; // 不要忘了把乘法懒标记初始化 if(l==r){ tr[root].one=a[l]%P; tr[root].len=1; // 一定不要忘初始未封锁元素个数 return; } build(ls,l,mid); build(rs,mid+1,r); pushup(root); return; } void add(ll root,ll l,ll r,ll x,ll y,ll z){ // 区间加 if(l>r||tr[root].block) return; if(x<=l&&r<=y){ tr[root].one=(tr[root].one+tr[root].len*z%P)%P; tr[root].add=(tr[root].add+z)%P; return; } pushdown(root); if(x<=mid) add(ls,l,mid,x,y,z); if(y>mid) add(rs,mid+1,r,x,y,z); return pushup(root); } void times(ll root,ll l,ll r,ll x,ll y,ll z){ // 区间乘 if(l>r||tr[root].block) return; if(x<=l&&r<=y){ tr[root].one=(tr[root].one*z)%P; tr[root].add=(tr[root].add*z)%P; tr[root].times=(tr[root].times*z)%P; return; } pushdown(root); if(x<=mid) times(ls,l,mid,x,y,z); if(y>mid) times(rs,mid+1,r,x,y,z); return pushup(root); } void block(ll root,ll l,ll r,ll x,ll y){ // 封锁区间 if(l>r) return; if(x<=l&&r<=y){ if(l!=r) pushdown(root); if(tr[root].block==0){ // 第一次封锁 tr[root].two=(tr[root].two+tr[root].one)%P; tr[root].one=0; // 封锁整个区间,所以把所有和记到two里,one清零 tr[root].len=0; // 解锁元素个数为零了 } ++tr[root].block; // 封锁次数+1,后面解锁时不要改回零,而是-1 return; } pushdown(root); if(x<=mid) block(ls,l,mid,x,y); if(y>mid) block(rs,mid+1,r,x,y); return pushup(root); } void deblock(ll root,ll l,ll r,ll x,ll y){ //解封区间 if(l>r) return; if(x<=l&&r<=y){ --tr[root].block; // 呼应上文++,解除一重封锁 if(tr[root].block==0){ // 若已经全部解封 if(l==r) swap(tr[root].one,tr[root].two),tr[root].len=1; // 由于整体解封,原来未解封的变成解封的,没有未解封的元素,因为这里是叶节点所以len=1 else pushup(root); // 若不是叶节点接受子节点信息即可 } return; } pushdown(root); if(x<=mid) deblock(ls,l,mid,x,y); if(y>mid) deblock(rs,mid+1,r,x,y); return pushup(root); } ll inque(ll root,ll l,ll r,ll x,ll y){ //询问,函数名有点奇怪,但是我习惯了 if(l>r) return 0; if(x<=l&&r<=y) return (tr[root].one+tr[root].two)%P; // 询问整个区间的和,自然包括解封的和未解封的 pushdown(root); ll ret=0; if(x<=mid) ret=inque(ls,l,mid,x,y)%P; if(y>mid) ret=(ret+inque(rs,mid+1,r,x,y))%P; return ret; } } tree; int main(){ std::ios::sync_with_stdio(0); cin>>n>>m; for(ll i=1;i<=n;cin>>a[i],++i); // 码风较奇怪…… tree.build(1,1,n); for(ll i=1,type,l,r,x;i<=m;++i){ cin>>type>>l>>r; if(type==4){ cout<<tree.inque(1,1,n,l,r)<<"\n"; for(Block &j:que[i]) tree.deblock(1,1,n,j.l,j.r); // continue 前,一定要把操作做完啊啊啊,卡了我好久 continue; } cin>>x; if(type==1) tree.add(1,1,n,l,r,x); else if(type==2) tree.times(1,1,n,l,r,x); else{ tree.block(1,1,n,l,r); que[i+x].push_back({l,r}); // 记录每一次解封操作 } for(Block &j:que[i]) tree.deblock(1,1,n,j.l,j.r); // 于所有操作之后解封,因为 i+x 时刻也是被封锁的 } return 0; }然后,就结束了。。感谢看到这里,不喜勿喷。
这道题是一道很有趣的题,希望大家,学习愉快。。。。
其余参考 巨佬的题解
- 未封锁的父亲,已封锁的儿子。此时显然不需要
- 1
信息
- ID
- 6427
- 时间
- 1000ms
- 内存
- 512MiB
- 难度
- 6
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者