1 条题解

  • 0
    @ 2025-8-24 22:30:26

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar 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时,我们需要分类:

    1. 未封锁的父亲,已封锁的儿子。此时显然不需要pushdown,在封锁前,就把父亲需要传给儿子的下传完了,现在父亲的懒标记里是封锁后的操作,所以不需要pushdown
    2. 已封锁的父亲,已封锁的儿子。此时可以pushdown,在封锁后,所有不应下传的都没有下传到父亲,所以父亲能传给儿子的只有封锁前的懒标记
    3. 其余情况无需下传。

    于是有以下代码:

    // 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;
    }
    

    然后我们高高兴兴地跑样例,然后大悲!样例二怎么 WA\textcolor{red}{WA} 了?!

    交上去: 为什么?! 我们冷静思考,惊恐的发现:如果在节点 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;
    }
    
    

    然后,就结束了。。感谢看到这里,不喜勿喷。

    这道题是一道很有趣的题,希望大家,学习愉快。。。。

    其余参考 littleKtianlittleKtian 巨佬的题解

    • 1

    信息

    ID
    6427
    时间
    1000ms
    内存
    512MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者