洛谷 P4513:小白逛公园 ← 线段树(区间合并)

【题目来源】
https://www.luogu.com.cn/problem/P4513
https://www.acwing.com/problem/content/246/

【题目描述】
小新经常陪小白去公园玩,也就是所谓的遛狗啦…
在小新家附近有一条“公园路”,路的一边从南到北依次排着 n 个公园,小白早就看花了眼,自己也不清楚该去哪些公园玩了。
一开始,小白就根据公园的风景给每个公园打了分。小新为了省事,每次遛狗的时候都会事先规定一个范围,小白只可以选择第 a 个和第 b 个公园之间(包括 a,b 两个公园)选择连续的一些公园玩。小白当然希望选出的公园的分数总和尽量高咯。同时,由于一些公园的景观会有所改变,所以,小白的打分也可能会有一些变化。
那么,就请你来帮小白选择公园吧。

【输入格式】
第一行,两个整数 n 和 m,分别表示表示公园的数量和操作(遛狗或者改变打分)总数。
接下来 n 行,每行一个整数,依次给出小白开始时对公园的打分。
接下来 m 行,每行三个整数。其中第一个整数 k 为 1 或 2。
● k=1 表示,小新要带小白出去玩,接下来的两个整数 a 和 b 给出了选择公园的范围(1≤a,b≤n)。测试数据可能会出现 a>b 的情况,需要进行交换;
● k=2表示,小白改变了对某个公园的打分,接下来的两个整数 p 和 s,表示小白对第 p 个公园的打分变成了 s(1≤|s|≤1000)。

【输出格式】
小白每出去玩一次,都对应输出一行,只包含一个整数,表示小白可以选出的公园得分和的最大值。

【输入样例】
5 3
1
2
-3
4
5
1 2 3
2 2 -1
1 2 3

【输出样例】
2
-1​​​​​​​

【数据范围】
对于 100% 的数据,1≤n≤5×10^5,1≤m≤10^5,所有打分都是绝对值不超过 1000 的整数。

【算法分析】
● 本题“洛谷 P4513:小白逛公园”的代码与“AcWing 245. 你能回答这些问题吗”(https://www.acwing.com/problem/content/246/)的代码一样,可以视为同一个问题。

● 核心变量 sum、lmax、rmax、tmax 解析
sum,整个区间的总和
lmax,从区间左端点开始的各个子段的最大子段和(前缀最大和)
rmax,以区间右端点结束的各个子段的最大子段和(后缀最大和)
tmax,整个区间内各个子段的最大子段和各个子段,即前文各个前缀、后缀子段

● 核心变量 sum、lmax、rmax、tmax 示例分析
假设区间是 [1,4],数值为 [-1, 3, -2, 6],则:
sum = -1+3-2+6 = 6(整个区间的总和);
lmax = max (-1, -1+3, -1+3-2, -1+3-2+6) = 6(从区间左端点 1 开始的各个子段的最大子段和);
rmax = max (6, -2+6, 3-2+6, -1+3-2+6) = 6(以区间右端点 4 结束的各个子段的最大子段和);
tmax = 6(整个区间里各个子段的最大子段和是 6)。

● 核心代码 pushup 解析

void pushup(node &u,node &le,node &ri) {
    // 1.父区间总和 = 左子区间总和 + 右子区间总和(最基础的合并)
    u.sum = le.sum + ri.sum;

    // 2.父区间的前缀最大和(lmax):两种情况取最大值
    // 情况1:只在左子区间里的前缀最大和(le.lmax)
    // 情况2:左子区间全部取完 + 右子区间的前缀最大和(le.sum + ri.lmax)
    u.lmax = max(le.lmax, le.sum + ri.lmax);

    // 3.父区间的后缀最大和(rmax):两种情况取最大值
    // 情况1:只在右子区间里的后缀最大和(ri.rmax)
    // 情况2:右子区间全部取完 + 左子区间的后缀最大和(ri.sum + le.rmax)
    u.rmax = max(ri.rmax, ri.sum + le.rmax);

    // 4.父区间的最大子段和(tmax):三种情况取最大值
    // 情况1:跨越左右子区间的最大和(左子区间的后缀 + 右子区间的前缀)
    // 情况2:左子区间内部的最大子段和(le.tmax)
    // 情况3:右子区间内部的最大子段和(ri.tmax)
    u.tmax = max(le.rmax + ri.lmax, max(le.tmax, ri.tmax));
}

● 核心代码 pushup 示例解析
用一个具体例子 [-1, 3, -2, 6] 验证:
左子区间 le:[1,2],数值 [-1,3] → le.sum=2,le.lmax=2,le.rmax=3,le.tmax=3;
右子区间 ri:[3,4],数值 [-2,6] → ri.sum=4,ri.lmax=4,ri.rmax=6,ri.tmax=6;
合并成父区间 u:[1,4],数值 [-1,3,-2,6]:
u.sum = -1+3-2+6 = 6 = le.sum + ri.sum = 2 + 4 = 6(正确);
u.lmax = max(le.lmax,le.sum+ri.lmax) = max(2, 2+4) = 6(正确);
u.rmax = max(ri.rmax,ri.sum+le.rmax) = max(6, 4+3) = 7(正确);
u.tmax = max(le.rmax+ri.lmax,max(le.tmax,ri.tmax)) = max(3+4, max(3,6)) = max(7,6) = 7(正确,对应子段 [3, -2, 6])。

● pushup 里的 sum / lmax / rmax / tmax 的公式到底是怎么推导出来的?
其实这些公式不是凭空来的,而是基于「分治思想」和「子段的位置特征」推导的必然结果。
这些公式的本质是把 “父区间的问题” 拆解为 “子区间的子问题”,符合线段树 “分治 + 合并” 的核心思想 —— 只要子区间的信息足够,就能通过固定的逻辑合并出父区间的信息,这也是线段树能高效解决区间问题的关键。
(零)推导前的核心前提
我们把父区间记为 U,左子区间记为 L,右子区间记为 R(U = L + R,区间连续)。
所有子段的位置只有三种可能:
(1)完全落在 L 内;
(2)完全落在 R 内;
(3)跨越 L 和 R(一部分在 L 右部,一部分在 R 左部)。
所有公式的推导,都是围绕这三种位置特征展开的。
(一)推导 u.sum(父区间总和)
sum,表示整个区间的总和。
这个是最基础的,没有任何技巧:u.sum = le.sum + ri.sum
(二)推导 u.lmax(父区间的前缀最大和)
lmax,表示从区间左端点开始的各个子段的最大子段和(前缀最大和)。
以 U 的左端点开头的子段,只有两种可能:
(1)不跨区间:子段完全在 L 内 → 对应 le.lmax(L 的前缀最大和);
(2)跨区间:子段包含整个 L,再延伸到 R 的一部分 → 对应 le.sum + ri.lmax(L 的全部和 + R 的前缀最大和)。
要找最大值,就取这两种情况的较大值:u.lmax = max(le.lmax, le.sum + ri.lmax)
(三)推导 u.rmax(父区间的后缀最大和)
rmax,表示以区间右端点结束的各个子段的最大子段和(后缀最大和)。
以 U 的右端点结尾的子段,只有两种可能:
(1)不跨区间:子段完全在 R 内 → 对应 ri.rmax(R 的后缀最大和);
(2)跨区间:子段包含整个 R,再延伸到 L 的一部分 → 对应 ri.sum + le.rmax(R 的全部和 + L 的后缀最大和)。
要找最大值,就取这两种情况的较大值:u.rmax = max(ri.rmax, ri.sum + le.rmax)
(四)推导 u.tmax(父区间的最大子段和)
tmax,表示整个区间内各个子段的最大子段和。
根据开头的 “三种位置”,最大子段和的候选来源有三个:
(1)完全在 L 内:对应 le.tmax(L 的最大子段和);
(2)完全在 R 内:对应 ri.tmax(R 的最大子段和);
(3)跨越 L 和 R:这种子段一定是「L 的后缀最大和 + R 的前缀最大和」→ 对应 le.rmax + ri.lmax(因为跨区间的子段,左边要取 L 的后缀最大,右边要取 R 的前缀最大,才能凑出最大和)。
要找最大值,就取这三种情况的较大值:u.tmax = max(le.rmax + ri.lmax, max(le.tmax, ri.tmax))

【算法代码】

#include <bits/stdc++.h>
using namespace std;

const int N=5e5+5;
int a[N];
int n,m;

struct node {
    int le,ri;
    int tmax,lmax,rmax,sum;
} tr[N*4];

void pushup(node &u,node &le,node &ri) {
    u.sum=le.sum+ri.sum;
    u.lmax=max(le.lmax,le.sum+ri.lmax);
    u.rmax=max(ri.rmax,ri.sum+le.rmax);
    u.tmax=max(le.rmax+ri.lmax,max(le.tmax,ri.tmax));
}

void pushup(int u) {
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}

void build(int u,int le,int ri) {
    if(le==ri) tr[u]= {le,ri,a[ri],a[ri],a[ri],a[ri]};
    else {
        tr[u]= {le,ri};
        int mid=le+ri>>1;
        build(u<<1,le,mid);
        build(u<<1|1,mid+1,ri);
        pushup(u);
    }
}

void modify(int u,int p,int v) {
    if(tr[u].le==p && tr[u].ri==p) tr[u]= {p,p,v,v,v,v};
    else {
        int mid=tr[u].le+tr[u].ri>>1;
        if(p<=mid) modify(u<<1,p,v);
        else modify(u<<1|1,p,v);
        pushup(u);
    }
}

node query(int u,int le,int ri) {
    if(tr[u].le>=le && tr[u].ri<=ri) return tr[u];
    int mid=tr[u].le+tr[u].ri>>1;
    if(ri<=mid) return query(u<<1,le,ri);
    else if(le>mid) return query(u<<1|1,le,ri);
    else {
        auto left=query(u<<1,le,ri);
        auto right=query(u<<1|1,le,ri);
        node t;
        pushup(t,left,right);
        return t;
    }
}

int main() {
    cin>>n>>m;
    for(int i=1; i<=n; i++) cin>>a[i];
    build(1,1,n);

    int op,x,y;
    while(m--) {
        cin>>op>>x>>y;
        if(op==1) {
            if(x>y) swap(x,y);
            cout<<query(1,x,y).tmax<<endl;
        } else modify(1,x,y);
    }

    return 0;
}

/*
in:
5 3
1
2
-3
4
5
1 2 3
2 2 -1
1 2 3

out:
2
-1
*/





【参考文献】
https://www.acwing.com/solution/content/110854/
https://www.acwing.com/video/649/


 

posted @ 2026-01-24 20:59  Triwa  阅读(0)  评论(0)    收藏  举报