维护序列(信息学奥赛一本通- P1551)(洛谷-P2023)

【题目描述】

原题来自:AHOI 2009

老师交给小可可一个维护数列的任务,现在小可可希望你来帮他完成。

有长为 n 的数列,不妨设为 a1,a2,⋯,an 。有如下三种操作形式:

把数列中的一段数全部乘一个值;

把数列中的一段数全部加一个值;

询问数列中的一段数的和,由于答案可能很大,你只需输出这个数模 P 的值。

【输入】

第一行两个整数 n 和 P;

第二行含有 n 个非负整数,从左到右依次为 a1,a2,⋯,an ;

第三行有一个整数 M,表示操作总数;

从第四行开始每行描述一个操作,输入的操作有以下三种形式:

操作 1:1tgc,表示把所有满足 t≤i≤g 的 ai 改为 ai×c;

操作 2:2tgc,表示把所有满足 t≤i≤g 的 ai 改为 ai+c;

操作 3:3tg,询问所有满足 t≤i≤g 的 ai 的和模 P 的值。

同一行相邻两数之间用一个空格隔开,每行开头和末尾没有多余空格。

【输出】

对每个操作 3,按照它在输入中出现的顺序,依次输出一行一个整数表示询问结果。

【输入样例】

复制代码
7 43
1 2 3 4 5 6 7
5
1 2 5 5
3 2 4
2 3 7 9
3 1 3
3 4 7

【输出样例】

复制代码
2
35
8

【提示】

样例说明:

初始时数列为 {1,2,3,4,5,6,7};

经过第 1 次操作后,数列为 {1,10,15,20,25,6,7};

对第 2 次操作,和为 10+15+20=45,模 43 的结果是 2;

经过第 3 次操作后,数列为 {1,10,24,29,34,15,16};

对第 4 次操作,和为 1+10+24=35,模 43 的结果是 35;

对第 5 次操作,和为 29+34+15+16=94,模 43 的结果是 8。

数据范围与提示:

对于全部测试数据,1≤t≤g≤n,0≤c,a[i]≤10^9,1≤P≤10^9 。

测试数据规模如下表所示:

|------|----|-------|-------|---------|---------|---------|---------|-------|
| 数据编号 | 1 | 2,3 | 4 | 5 | 6 | 7 | 8 | 9,10 |
| n= | 10 | 10^3 | 10^4 | 6×10^4 | 7×10^4 | 8×10^4 | 9×10^4 | 10^5 |
| M= | 10 | 10^3 | 10^4 | 6×10^4 | 7×10^4 | 8×10^4 | 9×104^ | 10^5 |

一、 题目分析

本题要求我们维护一个长度为N的数列,支持三种操作:

  1. 区间乘法:将某一段区间内的所有数乘上一个常数c。

  2. 区间加法:将某一段区间内的所有数加上一个常数c。

  3. 区间求和:查询某一段区间内所有数字的和,并对指定的数P取模。

核心痛点 :这是极其经典的"多重懒标记"线段树问题。如果只有加法,或者只有乘法,问题都很简单。但当加法和乘法交织在一起时,一个节点上可能同时存在"历史加法标记"和"新来的乘法标记",先算加法还是先算乘法?标记下放时如何互相影响? 这成为了这道题最大的难点。


二、 思考过程与解题思路(核心数学推导)

面对两个互相干扰的标记,我们需要在数学上确立一个绝对的优先级(铁律)

在线段树中,我们人为规定:永远遵循"先乘后加"的原则。

即对于底层的任意一个真实数字x,它经过当前节点的懒标记作用后,其真实值永远表示为:

真实值 =

交接仪式的代数魔法:

假设当前子节点(儿子)手里原本就有自己的旧账本:旧的乘法标记 和旧的加法标记

此时,父节点执行pushdown,下放了新的指令:整体乘上,再加上

我们把子节点的旧状态作为一个整体,代入父节点的新指令中,利用乘法分配律展开:

最新状态 =

最新状态 =

经过提取公因式,我们得到了极其优美的状态转移方程:

  1. 子节点的新乘法标记 =

  2. 子节点的新加法标记 =

这就是本题的灵魂:当父节点下放乘法标记时,子节点的旧加法标记必须跟着一起翻倍。


三、 算法设计

基于上述推导,我们的线段树设计如下:

  1. 节点结构 :维护 val(区间和)、lazym(乘法标记,初始化为1)、lazys(加法标记,初始化为0)。

  2. 下放操作 (pushdown)

    • 先用父节点的标记更新左右儿子的val。注意:加法标记对区间和的影响必须乘以区间长度

    • 利用推导出的公式更新左右儿子的lazymlazys

    • 清空父节点标记(lazym=1, lazys=0)。

  3. 更新操作 (update)

    • 乘法更新:当前节点的val, lazym, lazys 统统乘上常数c。

    • 加法更新:当前节点的val加上c区间长度,lazys 加上c。

    • 只要跨越节点(分裂),必须先 pushdown。


四、 易错点总结

  1. 加法标记 :在pushdown时,更新子节点的lazys必须写成 子lazys=子lazys*父lazym+父lazys,千万不能忘记乘上父节点的乘法标记。

  2. 区间长度 :加法操作对区间和(val)的影响,必须乘以当前区间的长度 (r-l+1),而乘法操作直接对val乘即可。

  3. 取模大爆炸 :本题数据范围极大,每一次加法和乘法运算后(包括更新标记时),都必须立刻取模 %p ,并且所有涉及数值的变量务必开long long,防止中间过程溢出。

  4. 局部变量覆盖 :在分裂区间找中点时,有同学习惯性写int m=(l+r)>>1; 容易与全局变量 m(操作次数)发生冲突,强烈建议计算中点统一使用int mid


五、 时空复杂度分析

  • 时间复杂度 :建树操作为O(N);得益于懒标记和势能剪枝,每次区间修改和区间求和的时间复杂度均为 。对于M次操作,总时间复杂度为 ,完美通过 10^5级别的数据。

  • 空间复杂度:线段树需要开辟原数组4倍的空间,空间复杂度为O(N)。


六、 完整代码

cpp 复制代码
//线段树 区间修改 区间查询 
//这道题要用到分配律 永远遵循先乘后加
#include <iostream>
using namespace std;
int n,p,m;
const int maxn=100010;
int a[maxn];//原数列
//线段树节点封装
struct node{
    long long val;
    long long lazym;//乘懒标记
    long long lazys;//加懒标记
}tree[maxn<<2];//线段树要开四倍元素大小

//懒标记下放 当前节点rt 管辖区间为[l,r]
void pushdown(int rt,int l,int r){
    int mid=(l+r)>>1;
    //先修改左右儿子的值
    tree[rt<<1].val=(tree[rt<<1].val*tree[rt].lazym+tree[rt].lazys*(mid-l+1))%p;
    tree[rt<<1|1].val=(tree[rt<<1|1].val*tree[rt].lazym+tree[rt].lazys*(r-mid))%p;
    //再修改左右儿子懒标记 然后清空rt懒标记
    tree[rt<<1].lazym=(tree[rt<<1].lazym*tree[rt].lazym)%p;
    tree[rt<<1|1].lazym=(tree[rt<<1|1].lazym*tree[rt].lazym)%p;
    tree[rt<<1].lazys=(tree[rt<<1].lazys*tree[rt].lazym+tree[rt].lazys)%p;
    tree[rt<<1|1].lazys=(tree[rt<<1|1].lazys*tree[rt].lazym+tree[rt].lazys)%p;
    //清空父节点的懒标记
    tree[rt].lazym=1;
    tree[rt].lazys=0;
}


//向上更新
void pushup(int rt){
    tree[rt].val=(tree[rt<<1].val+tree[rt<<1|1].val)%p;
}

//当前节点是rt,rt管辖的区间为[l,r]
void build(int l,int r,int rt){
    //初始化乘懒标记为1
    tree[rt].lazym=1;
    //初始化加懒标记为0
    tree[rt].lazys=0;
    if(l==r){
        tree[rt].val=a[l]%p;
        return;
    }
    int m=(l+r)>>1;
    build(l,m,rt<<1);
    build(m+1,r,rt<<1|1);
    //通过子节点更新当前节点
    pushup(rt);
}

//乘更新操作
//[t,g]区间每个数都*c,当前节点为rt,rt管辖区间为[l,r]
void updatem(int t,int g,int c,int l,int r,int rt){
    //如果当前节点管辖区间属于[t,g]
    if(t<=l&&g>=r){
        tree[rt].val=(tree[rt].val*c)%p;
        tree[rt].lazym=(tree[rt].lazym*c)%p;
        tree[rt].lazys=(tree[rt].lazys*c)%p;
        return;
    }
    if(t>r||g<l) return;
    //递归求左半区间和右半区间时先懒标记下放
    pushdown(rt,l,r);
    int m=(l+r)>>1;
    if(t<=m) updatem(t,g,c,l,m,rt<<1);
    if(g>m) updatem(t,g,c,m+1,r,rt<<1|1);
    pushup(rt);
}

//加更新操作
//[t,g]区间每个数都+c,当前节点为rt,rt管辖区间为[l,r]
void updates(int t,int g,int c,int l,int r,int rt){
    //如果当前节点管辖区间属于[t,g]
    if(t<=l&&g>=r){
        tree[rt].val=(tree[rt].val%p+1ll*(r-l+1)*c%p)%p;
        tree[rt].lazys=(tree[rt].lazys+c)%p;
        return;
    }
    if(t>r||g<l) return;
    //懒标记下放
    pushdown(rt,l,r);
    int m=(l+r)>>1;
    if(t<=m) updates(t,g,c,l,m,rt<<1);
    if(g>m) updates(t,g,c,m+1,r,rt<<1|1);
    pushup(rt);
}


//查询[t,g]区间的和%p的值 当前节点为rt rt所管辖区间为[l,r]
long long query(int t,int g,int l,int r,int rt){
    if(t<=l&&g>=r){
        return tree[rt].val%p;
    }
    if(t>r||g<l) return 0;
    //懒标记下放
    pushdown(rt,l,r);
    int m=(l+r)>>1;
    long long ans=0;
    if(t<=m) ans=(ans+query(t,g,l,m,rt<<1))%p;
    if(g>m) ans=(ans+query(t,g,m+1,r,rt<<1|1))%p;
    return ans%p;
}

int main(){
    //io加速
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>p;
    for(int i=1;i<=n;i++) cin>>a[i];
    cin>>m;
    //建树
    build(1,n,1);
    //总共m次操作
    while(m--){
        int k;
        cin>>k;
        if(k==1){//表示把所有满足t≤i≤g的a[i]改为a[i]×c
            int t,g,c;
            cin>>t>>g>>c;
            updatem(t,g,c,1,n,1);
        }
        else if(k==2){//表示把所有满足t≤i≤g的a[i]改为a[i]+c
            int t,g,c;
            cin>>t>>g>>c;
            updates(t,g,c,1,n,1);
        }
        else{//询问所有满足t≤i≤g的a[i]的和模P的值。
            int t,g;
            cin>>t>>g;
            cout<<query(t,g,1,n,1)<<"\n";
        }
    }
    return 0;
}
相关推荐
沉鱼.442 小时前
进制转换题
开发语言·c++·算法
Boop_wu2 小时前
[Java 算法] 归并排序
数据结构·算法·排序算法
今儿敲了吗2 小时前
49| 枚举排列
数据结构·c++·笔记·学习·算法
-凌凌漆-2 小时前
【C语言】大小端判断
linux·c语言·算法
We་ct2 小时前
LeetCode 67. 二进制求和:详细题解+代码拆解
前端·数据结构·算法·leetcode·typescript
炽烈小老头2 小时前
【每天学习一点算法 2026/04/01】零钱兑换
学习·算法
Morwit2 小时前
【力扣hot100】 70. 爬楼梯
c++·算法·leetcode·职场和发展
yuanyuan2o22 小时前
你可能需要的算法思想——动态规划
数据结构·python·算法·动态规划
唯创知音2 小时前
WTK6900FC鼾声识别芯片:基于DNN-HMM算法的高性能鼾声识别检测处理方案
人工智能·算法·dnn·鼾声识别芯片·鼾声检测芯片