【算法提高篇】(五)线段树 + 分治:解锁区间问题的终极思路,从最大子段和到复杂序列操作


目录

前言

[一、为什么需要 "线段树 + 分治"?](#一、为什么需要 “线段树 + 分治”?)

二、核心原理:线段树节点该维护哪些分治信息?

[三、实战入门:最大子段和(洛谷 P4513 小白逛公园)](#三、实战入门:最大子段和(洛谷 P4513 小白逛公园))

[3.1 题目要求](#3.1 题目要求)

[3.2 结构体与核心函数设计](#3.2 结构体与核心函数设计)

[3.2.1 结构体定义](#3.2.1 结构体定义)

[3.2.2 pushup:分治整合左右孩子信息](#3.2.2 pushup:分治整合左右孩子信息)

[3.2.3 build:建树](#3.2.3 build:建树)

[3.2.4 modify:单点修改](#3.2.4 modify:单点修改)

[3.2.5 query:区间查询(分治整合结果)](#3.2.5 query:区间查询(分治整合结果))

[3.3 完整 AC 代码](#3.3 完整 AC 代码)

[3.4 代码测试与验证](#3.4 代码测试与验证)

题目输入示例:

手动计算过程:

[四、进阶挑战:01 序列的复杂分治维护(洛谷 P2572 [SCOI2010] 序列操作)](#四、进阶挑战:01 序列的复杂分治维护(洛谷 P2572 [SCOI2010] 序列操作))

[4.1 题目要求](#4.1 题目要求)

[4.2 核心分析](#4.2 核心分析)

[4.2.1 结构体设计](#4.2.1 结构体设计)

[4.2.2 核心辅助函数](#4.2.2 核心辅助函数)

(1)pushup:分治整合左右孩子信息

(2)lazy:处理懒标记,更新当前节点信息

(3)pushdown:下放懒标记

[4.2.3 建树、修改、查询操作](#4.2.3 建树、修改、查询操作)

(1)build:建树

[(2)modify:区间修改(支持置 0、置 1、取反)](#(2)modify:区间修改(支持置 0、置 1、取反))

[(3)query:区间查询(支持查询 1 的个数、最长连续 1 长度)](#(3)query:区间查询(支持查询 1 的个数、最长连续 1 长度))

[4.3 完整 AC 代码](#4.3 完整 AC 代码)

[五、"线段树 + 分治" 的通用解题框架](#五、“线段树 + 分治” 的通用解题框架)

[5.1 步骤 1:分析分治所需的信息](#5.1 步骤 1:分析分治所需的信息)

[5.2 步骤 2:设计 pushup 函数](#5.2 步骤 2:设计 pushup 函数)

[5.3 步骤 3:处理懒标记(如有修改操作)](#5.3 步骤 3:处理懒标记(如有修改操作))

[5.4 步骤 4:实现建树、修改、查询](#5.4 步骤 4:实现建树、修改、查询)

六、高频易错点总结

[6.1 分治信息维护不全](#6.1 分治信息维护不全)

[6.2 pushup 函数逻辑错误](#6.2 pushup 函数逻辑错误)

[6.3 懒标记优先级处理错误](#6.3 懒标记优先级处理错误)

[6.4 懒标记下放顺序错误](#6.4 懒标记下放顺序错误)

[6.5 数据类型溢出](#6.5 数据类型溢出)

总结


前言

线段树的核心是分治思想 ------ 将大区间拆分为小区间,通过维护子区间信息拼凑出父区间结果。但当遇到最大子段和这类无法用单一数值维护的问题时,基础线段树就显得力不从心。而 "线段树 + 分治" 的组合,通过让线段树节点维护更丰富的分治相关信息,再利用分治思想整合结果,完美解决了这类复杂区间问题。

本文将从经典的最大子段和问题入手,深入拆解线段树与分治的结合逻辑,手把手实现 "小白逛公园" 模板题,再拓展到更复杂的 01 序列操作,让你彻底掌握这种进阶思路!下面就让我们正式开始吧!


一、为什么需要 "线段树 + 分治"?

先看一个经典问题:给定一个序列,支持两种操作 ------ 修改某个元素的值、查询某个区间内的最大子段和(连续子序列的最大和)。

如果用暴力解法,查询时遍历所有子段,时间复杂度 O (n²),面对 10⁵级别的数据直接超时;如果用基础线段树,只维护区间和、最大值,根本无法拼凑出 "连续子段" 的最大和 ------ 因为最大子段可能横跨左右两个子区间,仅靠左右孩子的最大值无法计算。

核心痛点:复杂区间问题的结果,往往需要结合 "左区间内部""右区间内部""跨区间" 三种情况,基础线段树的单一信息维护无法覆盖

而分治思想恰好能解决这个问题:对于一个区间的最大子段和,无非是三种情况的最大值:

  1. 完全在左子区间内;
  2. 完全在右子区间内;
  3. 横跨左右子区间(左子区间的后缀最大和 + 右子区间的前缀最大和)。

"线段树 + 分治" 的核心思路就是:让线段树的每个节点,维护分治所需的关键信息(如区间和、前缀最大和、后缀最大和、区间内最大子段和),查询时通过分治逻辑整合这三种情况的结果,最终得到答案。

二、核心原理:线段树节点该维护哪些分治信息?

以最大子段和问题为例,我们需要线段树的每个节点维护以下 4 类信息,才能通过分治整合出父区间的结果:

  1. sum:当前区间的总和 ------ 用于计算跨区间子段和(左后缀 + 右前缀);
  2. lmax:当前区间以左端点为起点的最大子段和(前缀最大和);
  3. rmax:当前区间以右端点为终点的最大子段和(后缀最大和);
  4. max:当前区间内的最大子段和(最终需要的结果)。

这四类信息的分治整合逻辑(pushup 函数)是关键,我们用公式明确:

  • 父区间总和 = 左孩子总和 + 右孩子总和;
  • 父区间前缀最大和 = max (左孩子前缀最大和,左孩子总和 + 右孩子前缀最大和);
  • 父区间后缀最大和 = max (右孩子后缀最大和,右孩子总和 + 左孩子后缀最大和);
  • 父区间最大子段和 = max (左孩子最大子段和,右孩子最大子段和,左孩子后缀最大和 + 右孩子前缀最大和)。

通过这些信息,就能完整覆盖分治的三种情况,这就是 "线段树 + 分治" 的核心原理。

三、实战入门:最大子段和(洛谷 P4513 小白逛公园)

题目链接:https://www.luogu.com.cn/problem/P4513

3.1 题目要求

给定 n 个公园的打分,支持两种操作:

  1. 修改某个公园的打分;
  2. 查询区间 [a,b] 内的最大子段和(a 可能大于 b,需交换)。

3.2 结构体与核心函数设计

3.2.1 结构体定义

每个节点维护 sum、lmax、rmax、max 四个分治信息,以及区间边界 l/r:

cpp 复制代码
typedef long long LL;
const int N = 5e5 + 10;
const int INF = 1e18;

struct Node {
    int l, r;
    LL sum;    // 区间总和
    LL lmax;   // 前缀最大和(以l为起点)
    LL rmax;   // 后缀最大和(以r为终点)
    LL max;    // 区间内最大子段和
} tr[N << 2];

int a[N];  // 原始数组(公园打分)

3.2.2 pushup:分治整合左右孩子信息

这是最核心的函数,严格按照之前的公式实现:

cpp 复制代码
// 用左孩子l和右孩子r,更新父节点p的信息
void pushup(Node& p, Node& l, Node& r) {
    // 1. 父区间总和 = 左总和 + 右总和
    p.sum = l.sum + r.sum;
    // 2. 父前缀最大和 = max(左前缀, 左总和+右前缀)
    p.lmax = max(l.lmax, l.sum + r.lmax);
    // 3. 父后缀最大和 = max(右后缀, 右总和+左后缀)
    p.rmax = max(r.rmax, r.sum + l.rmax);
    // 4. 父最大子段和 = max(左max, 右max, 左后缀+右前缀)
    p.max = max(max(l.max, r.max), l.rmax + r.lmax);
}

3.2.3 build:建树

叶子节点的信息的初始化:

  • 叶子节点区间长度为 1,sum = 原始值
  • lmax = rmax = max = 原始值(只有一个元素,前缀、后缀、最大子段和都是它本身);非叶子节点递归构建左右孩子后,调用 pushup 整合信息。
cpp 复制代码
void build(int p, int l, int r) {
    tr[p].l = l;
    tr[p].r = r;
    if (l == r) {  // 叶子节点
        tr[p].sum = a[l];
        tr[p].lmax = a[l];
        tr[p].rmax = a[l];
        tr[p].max = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);     // 构建左子树
    build(p << 1 | 1, mid + 1, r);  // 构建右子树
    pushup(tr[p], tr[p << 1], tr[p << 1 | 1]);  // 整合信息
}

3.2.4 modify:单点修改

修改某个位置的值后,递归更新路径上所有节点的分治信息(通过 pushup):

cpp 复制代码
// 将位置x的 value 改为s
void modify(int p, int x, LL s) {
    if (tr[p].l == x && tr[p].r == x) {  // 找到叶子节点
        tr[p].sum = s;
        tr[p].lmax = s;
        tr[p].rmax = s;
        tr[p].max = s;
        return;
    }
    int mid = (tr[p].l + tr[p].r) >> 1;
    if (x <= mid) modify(p << 1, x, s);  // 左子树包含x
    else modify(p << 1 | 1, x, s);       // 右子树包含x
    pushup(tr[p], tr[p << 1], tr[p << 1 | 1]);  // 更新父节点信息
}

3.2.5 query:区间查询(分治整合结果)

查询区间 [a,b] 时,返回一个 Node 结构体,包含该区间的 sum、lmax、rmax、max 信息。查询过程是分治的核心:

  1. 如果当前节点区间完全覆盖查询区间,直接返回该节点的信息;
  2. 如果查询区间只在左子树或右子树,递归查询并返回结果;
  3. 如果查询区间横跨左右子树,分别查询左子树的重叠部分(L)和右子树的重叠部分(R),然后用 pushup 整合 L 和 R 的信息,返回整合后的结果。
cpp 复制代码
Node query(int p, int x, int y) {
    if (x <= tr[p].l && tr[p].r <= y) {  // 完全覆盖,直接返回
        return tr[p];
    }
    int mid = (tr[p].l + tr[p].r) >> 1;
    if (y <= mid) {  // 查询区间在左子树
        return query(p << 1, x, y);
    } else if (x > mid) {  // 查询区间在右子树
        return query(p << 1 | 1, x, y);
    } else {  // 横跨左右子树,分治查询后整合
        Node L = query(p << 1, x, y);    // 左子树重叠部分
        Node R = query(p << 1 | 1, x, y);  // 右子树重叠部分
        Node res;
        pushup(res, L, R);  // 整合L和R的信息
        return res;
    }
}

3.3 完整 AC 代码

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

typedef long long LL;
const int N = 5e5 + 10;
const LL INF = 1e18;

struct Node {
    int l, r;
    LL sum;
    LL lmax;
    LL rmax;
    LL max;
} tr[N << 2];

int a[N];

void pushup(Node& p, Node& l, Node& r) {
    p.sum = l.sum + r.sum;
    p.lmax = max(l.lmax, l.sum + r.lmax);
    p.rmax = max(r.rmax, r.sum + l.rmax);
    p.max = max(max(l.max, r.max), l.rmax + r.lmax);
}

void build(int p, int l, int r) {
    tr[p].l = l;
    tr[p].r = r;
    if (l == r) {
        tr[p].sum = a[l];
        tr[p].lmax = a[l];
        tr[p].rmax = a[l];
        tr[p].max = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    pushup(tr[p], tr[p << 1], tr[p << 1 | 1]);
}

void modify(int p, int x, LL s) {
    if (tr[p].l == x && tr[p].r == x) {
        tr[p].sum = s;
        tr[p].lmax = s;
        tr[p].rmax = s;
        tr[p].max = s;
        return;
    }
    int mid = (tr[p].l + tr[p].r) >> 1;
    if (x <= mid) modify(p << 1, x, s);
    else modify(p << 1 | 1, x, s);
    pushup(tr[p], tr[p << 1], tr[p << 1 | 1]);
}

Node query(int p, int x, int y) {
    if (x <= tr[p].l && tr[p].r <= y) {
        return tr[p];
    }
    int mid = (tr[p].l + tr[p].r) >> 1;
    if (y <= mid) {
        return query(p << 1, x, y);
    } else if (x > mid) {
        return query(p << 1 | 1, x, y);
    } else {
        Node L = query(p << 1, x, y);
        Node R = query(p << 1 | 1, x, y);
        Node res;
        pushup(res, L, R);
        return res;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    build(1, 1, n);  // 根节点编号1,维护[1,n]

    while (m--) {
        int op, a, b;
        cin >> op >> a >> b;
        if (op == 1) {
            // 操作1:查询区间[a,b]的最大子段和,处理a>b的情况
            if (a > b) swap(a, b);
            Node res = query(1, a, b);
            cout << res.max << endl;
        } else {
            // 操作2:将位置a的打分改为b
            modify(1, a, b);
        }
    }

    return 0;
}

3.4 代码测试与验证

题目输入示例:

复制代码
5 3
1
2
-3
4
5
1 2 3  // 查询[2,3]的最大子段和
2 2 -1 // 将位置2的打分改为-1
1 2 3  // 再次查询[2,3]的最大子段和

手动计算过程:

  1. 初始数组:[1,2,-3,4,5];
  2. 第一次查询 [2,3]:区间值为 [2,-3],最大子段和是 2(仅取 2),输出 2;
  3. 修改位置 2 为 - 1 后,数组变为 [1,-1,-3,4,5];
  4. 第二次查询 [2,3]:区间值为 [-1,-3],最大子段和是 - 1(仅取 - 1),输出 - 1;
  5. 输出结果与题目示例一致,代码正确。

四、进阶挑战:01 序列的复杂分治维护(洛谷 P2572 [SCOI2010] 序列操作)

题目链接:https://www.luogu.com.cn/problem/P2572

4.1 题目要求

给定 01 序列,支持五种操作:

  1. 区间 [L,R] 全部改为 0;
  2. 区间 [L,R] 全部改为 1;
  3. 区间 [L,R] 全部取反(0→1,1→0);
  4. 查询区间 [L,R] 内 1 的个数;
  5. 查询区间 [L,R] 内最长连续 1 的长度。

4.2 核心分析

这道题的复杂度在于:

  • 存在三种区间修改操作(置 0、置 1、取反),需要设计多懒标记,且要确定优先级(置 0 / 置 1 优先级高于取反);
  • 查询 "最长连续 1 的长度",需要分治维护相关信息,同时还要维护 0 的对应信息(因为取反会交换 0 和 1 的属性)。

4.2.1 结构体设计

需要维护的信息(同时维护 0 和 1 的相关分治信息):

  • 基础信息:区间边界 l/r;
  • 计数信息:s0(0 的个数)、s1(1 的个数);
  • 分治信息 (最长连续子段):
    • l0(以左端点为起点的最长连续 0 长度)、r0(以右端点为终点的最长连续 0 长度)、m0(区间内最长连续 0 长度);
    • l1(以左端点为起点的最长连续 1 长度)、r1(以右端点为终点的最长连续 1 长度)、m1(区间内最长连续 1 长度);
  • 懒标记:f(置 0 / 置 1 标记,-1 表示无,0 表示置 0,1 表示置 1)、rev(取反标记,-1 表示无,2 表示需要取反)。

4.2.2 核心辅助函数

(1)pushup:分治整合左右孩子信息

以 1 的信息为例(0 的信息逻辑相同):

  • s1 = 左孩子 s1 + 右孩子 s1;
  • l1:如果左孩子全是 1(s0=0),则 l1 = 左孩子 s1 + 右孩子 l1;否则 l1 = 左孩子 l1;
  • r1:如果右孩子全是 1(s0=0),则 r1 = 右孩子 s1 + 左孩子 r1;否则 r1 = 右孩子 r1;
  • m1 = max (左孩子 m1, 右孩子 m1, 左孩子 r1 + 右孩子 l1)。
cpp 复制代码
void pushup(Node& p, Node& l, Node& r) {
    // 整合0的计数和分治信息
    p.s0 = l.s0 + r.s0;
    p.l0 = (l.s1 == 0) ? (l.s0 + r.l0) : l.l0;
    p.r0 = (r.s1 == 0) ? (r.s0 + l.r0) : r.r0;
    p.m0 = max(max(l.m0, r.m0), l.r0 + r.l0);

    // 整合1的计数和分治信息
    p.s1 = l.s1 + r.s1;
    p.l1 = (l.s0 == 0) ? (l.s1 + r.l1) : l.l1;
    p.r1 = (r.s0 == 0) ? (r.s1 + l.r1) : r.r1;
    p.m1 = max(max(l.m1, r.m1), l.r1 + r.l1);
}
(2)lazy:处理懒标记,更新当前节点信息
  • 置 0 操作:将所有 1 的信息置 0,0 的信息置为区间长度,清空取反标记;
  • 置 1 操作:将所有 0 的信息置 0,1 的信息置为区间长度,清空取反标记;
  • 取反操作:交换 0 和 1 的所有信息,翻转取反标记(有则清,无则加)。
cpp 复制代码
void lazy(Node& p, int op) {
    int len = p.r - p.l + 1;
    if (op == 0) {  // 置0操作
        p.s0 = len; p.l0 = len; p.r0 = len; p.m0 = len;
        p.s1 = 0;  p.l1 = 0;  p.r1 = 0;  p.m1 = 0;
        p.f = 0; p.rev = -1;  // 清空取反标记
    } else if (op == 1) {  // 置1操作
        p.s0 = 0;  p.l0 = 0;  p.r0 = 0;  p.m0 = 0;
        p.s1 = len; p.l1 = len; p.r1 = len; p.m1 = len;
        p.f = 1; p.rev = -1;  // 清空取反标记
    } else if (op == 2) {  // 取反操作
        // 交换0和1的所有信息
        swap(p.s0, p.s1);
        swap(p.l0, p.l1);
        swap(p.r0, p.r1);
        swap(p.m0, p.m1);
        // 翻转取反标记
        p.rev = (p.rev == 2) ? -1 : 2;
    }
}
(3)pushdown:下放懒标记

按优先级下放:先下放置 0 / 置 1 标记(优先级高),再下放取反标记,最后清空当前节点标记。

cpp 复制代码
void pushdown(int p) {
    Node& father = tr[p];
    Node& left = tr[p << 1];
    Node& right = tr[p << 1 | 1];

    // 先下放置0/置1标记
    if (father.f != -1) {
        lazy(left, father.f);
        lazy(right, father.f);
        father.f = -1;  // 清空标记
    }

    // 再下放取反标记
    if (father.rev == 2) {
        lazy(left, 2);
        lazy(right, 2);
        father.rev = -1;  // 清空标记
    }
}

4.2.3 建树、修改、查询操作

(1)build:建树

叶子节点初始化:根据原始值,设置 0 和 1 的相关信息,懒标记初始为 - 1(无标记)。

cpp 复制代码
void build(int p, int l, int r) {
    tr[p].l = l;
    tr[p].r = r;
    tr[p].f = -1;  // 初始无置0/置1标记
    tr[p].rev = -1;  // 初始无取反标记
    if (l == r) {
        int val = a[l];
        if (val == 0) {
            tr[p].s0 = 1; tr[p].l0 = 1; tr[p].r0 = 1; tr[p].m0 = 1;
            tr[p].s1 = 0; tr[p].l1 = 0; tr[p].r1 = 0; tr[p].m1 = 0;
        } else {
            tr[p].s0 = 0; tr[p].l0 = 0; tr[p].r0 = 0; tr[p].m0 = 0;
            tr[p].s1 = 1; tr[p].l1 = 1; tr[p].r1 = 1; tr[p].m1 = 1;
        }
        return;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    pushup(tr[p], tr[p << 1], tr[p << 1 | 1]);
}
(2)modify:区间修改(支持置 0、置 1、取反)
cpp 复制代码
void modify(int p, int x, int y, int op) {
    Node& cur = tr[p];
    if (x <= cur.l && cur.r <= y) {
        lazy(cur, op);
        return;
    }
    pushdown(p);  // 下放标记
    int mid = (cur.l + cur.r) >> 1;
    if (x <= mid) modify(p << 1, x, y, op);
    if (y > mid) modify(p << 1 | 1, x, y, op);
    pushup(cur, tr[p << 1], tr[p << 1 | 1]);  // 整合信息
}
(3)query:区间查询(支持查询 1 的个数、最长连续 1 长度)

返回查询区间的 Node 结构体,按需提取 s1 或 m1。

cpp 复制代码
Node query(int p, int x, int y) {
    Node& cur = tr[p];
    if (x <= cur.l && cur.r <= y) {
        return cur;
    }
    pushdown(p);  // 下放标记
    int mid = (cur.l + cur.r) >> 1;
    if (y <= mid) {
        return query(p << 1, x, y);
    } else if (x > mid) {
        return query(p << 1 | 1, x, y);
    } else {
        Node L = query(p << 1, x, y);
        Node R = query(p << 1 | 1, x, y);
        Node res;
        pushup(res, L, R);
        return res;
    }
}

4.3 完整 AC 代码

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;

struct Node {
    int l, r;
    // 0的相关信息:个数、前缀最长、后缀最长、区间最长
    int s0, l0, r0, m0;
    // 1的相关信息:个数、前缀最长、后缀最长、区间最长
    int s1, l1, r1, m1;
    // 懒标记:f(-1无,0置0,1置1),rev(-1无,2取反)
    int f, rev;
} tr[N << 2];

int a[N];

void pushup(Node& p, Node& l, Node& r) {
    // 整合0的信息
    p.s0 = l.s0 + r.s0;
    p.l0 = (l.s1 == 0) ? (l.s0 + r.l0) : l.l0;
    p.r0 = (r.s1 == 0) ? (r.s0 + l.r0) : r.r0;
    p.m0 = max(max(l.m0, r.m0), l.r0 + r.l0);

    // 整合1的信息
    p.s1 = l.s1 + r.s1;
    p.l1 = (l.s0 == 0) ? (l.s1 + r.l1) : l.l1;
    p.r1 = (r.s0 == 0) ? (r.s1 + l.r1) : r.r1;
    p.m1 = max(max(l.m1, r.m1), l.r1 + r.l1);
}

void lazy(Node& p, int op) {
    int len = p.r - p.l + 1;
    if (op == 0) {  // 置0
        p.s0 = len; p.l0 = len; p.r0 = len; p.m0 = len;
        p.s1 = 0;  p.l1 = 0;  p.r1 = 0;  p.m1 = 0;
        p.f = 0; p.rev = -1;
    } else if (op == 1) {  // 置1
        p.s0 = 0;  p.l0 = 0;  p.r0 = 0;  p.m0 = 0;
        p.s1 = len; p.l1 = len; p.r1 = len; p.m1 = len;
        p.f = 1; p.rev = -1;
    } else if (op == 2) {  // 取反
        swap(p.s0, p.s1);
        swap(p.l0, p.l1);
        swap(p.r0, p.r1);
        swap(p.m0, p.m1);
        p.rev = (p.rev == 2) ? -1 : 2;
    }
}

void pushdown(int p) {
    Node& father = tr[p];
    if (father.f == -1 && father.rev == -1) return;

    Node& left = tr[p << 1];
    Node& right = tr[p << 1 | 1];

    // 先下放置0/置1标记
    if (father.f != -1) {
        lazy(left, father.f);
        lazy(right, father.f);
        father.f = -1;
    }

    // 再下放取反标记
    if (father.rev == 2) {
        lazy(left, 2);
        lazy(right, 2);
        father.rev = -1;
    }
}

void build(int p, int l, int r) {
    tr[p].l = l;
    tr[p].r = r;
    tr[p].f = -1;
    tr[p].rev = -1;
    if (l == r) {
        int val = a[l];
        if (val == 0) {
            tr[p].s0 = 1; tr[p].l0 = 1; tr[p].r0 = 1; tr[p].m0 = 1;
            tr[p].s1 = 0; tr[p].l1 = 0; tr[p].r1 = 0; tr[p].m1 = 0;
        } else {
            tr[p].s0 = 0; tr[p].l0 = 0; tr[p].r0 = 0; tr[p].m0 = 0;
            tr[p].s1 = 1; tr[p].l1 = 1; tr[p].r1 = 1; tr[p].m1 = 1;
        }
        return;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    pushup(tr[p], tr[p << 1], tr[p << 1 | 1]);
}

void modify(int p, int x, int y, int op) {
    Node& cur = tr[p];
    if (x <= cur.l && cur.r <= y) {
        lazy(cur, op);
        return;
    }
    pushdown(p);
    int mid = (cur.l + cur.r) >> 1;
    if (x <= mid) modify(p << 1, x, y, op);
    if (y > mid) modify(p << 1 | 1, x, y, op);
    pushup(cur, tr[p << 1], tr[p << 1 | 1]);
}

Node query(int p, int x, int y) {
    Node& cur = tr[p];
    if (x <= cur.l && cur.r <= y) {
        return cur;
    }
    pushdown(p);
    int mid = (cur.l + cur.r) >> 1;
    if (y <= mid) {
        return query(p << 1, x, y);
    } else if (x > mid) {
        return query(p << 1 | 1, x, y);
    } else {
        Node L = query(p << 1, x, y);
        Node R = query(p << 1 | 1, x, y);
        Node res;
        pushup(res, L, R);
        return res;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

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

    while (m--) {
        int op, l, r;
        cin >> op >> l >> r;
        l++; r++;  // 题目输入下标从0开始,统一转为1开始
        if (op < 3) {
            // 操作0:置0,1:置1,2:取反
            modify(1, l, r, op);
        } else if (op == 3) {
            // 查询1的个数
            Node res = query(1, l, r);
            cout << res.s1 << endl;
        } else {
            // 查询最长连续1的长度
            Node res = query(1, l, r);
            cout << res.m1 << endl;
        }
    }

    return 0;
}

五、"线段树 + 分治" 的通用解题框架

通过以上两个例题,我们可以总结出 "线段树 + 分治" 的通用解题步骤,无论遇到哪种复杂区间问题,都可以按这个思路推导:

5.1 步骤 1:分析分治所需的信息

明确问题的结果需要覆盖哪些分治情况(如最大子段和的三种情况),进而确定每个节点需要维护的信息。例如:

  • 最长连续相同元素长度:需要维护前缀最长、后缀最长、区间最长;
  • 最大子数组乘积:需要维护前缀最大乘积、前缀最小乘积(负负得正)、后缀最大乘积、后缀最小乘积、区间最大乘积、区间乘积。

5.2 步骤 2:设计 pushup 函数

这是分治的核心,明确如何通过左右孩子的信息,整合出父节点的信息。设计时要确保覆盖所有分治情况,避免遗漏(如跨区间的情况)。

5.3 步骤 3:处理懒标记(如有修改操作)

如果涉及区间修改,需要设计对应的懒标记,并明确标记的优先级和下放逻辑。懒标记的处理不能破坏分治信息的正确性,例如取反操作需要交换 0 和 1 的所有分治信息。

5.4 步骤 4:实现建树、修改、查询

  • 建树:初始化叶子节点的分治信息,非叶子节点递归构建后调用 pushup;
  • 修改:找到目标区间后更新懒标记,路径上的节点通过 pushup 更新分治信息;
  • 查询:分治查询目标区间的子区间,通过 pushup 整合结果后返回。

六、高频易错点总结

"线段树 + 分治" 的代码复杂度较高,新手容易踩坑,以下是五大高频易错点:

6.1 分治信息维护不全

例如只维护了 1 的分治信息,忘记维护 0 的信息,导致取反操作无法实现;或遗漏跨区间的情况(如最大子段和忘记左后缀 + 右前缀)。

6.2 pushup 函数逻辑错误

这是最致命的错误,例如前缀最大和的计算错误(误将 "左总和 + 右前缀" 写成 "左前缀 + 右总和"),会导致整个分治逻辑失效。建议手动推导简单案例,验证 pushup 逻辑。

6.3 懒标记优先级处理错误

例如置 0 / 置 1 标记的优先级低于取反标记,导致置 0 后再取反的结果错误。记住:覆盖类操作(置 0 / 置 1)的优先级高于翻转类操作(取反)。

6.4 懒标记下放顺序错误

应按优先级从高到低下放标记,否则会出现标记叠加错误。例如先下放取反标记,再下放置 0 标记,会导致取反操作被置 0 操作覆盖,结果错误。

6.5 数据类型溢出

当序列长度较大或数值较大时,分治信息(如 sum)可能超过 int 范围,需用 long long 存储,避免溢出。


总结

"线段树 + 分治" 是解决复杂区间问题的终极思路,其核心在于用分治思想定义节点信息,用线段树高效维护这些信息。掌握这种思路后,无论是最大子段和、最长连续相同元素、还是复杂的 01 序列操作,都能迎刃而解。

线段树 + 分治的学习门槛较高,但一旦掌握,就能应对算法竞赛中绝大多数区间类难题。希望本文能帮你建立清晰的思路,在刷题中逐步吃透这种进阶用法,祝你在算法之路上越走越远!

创作不易,如果本文对你有帮助,欢迎点赞、收藏、关注三连~

相关推荐
简佐义的博客1 小时前
120万细胞大整合(自测+公共数据):scRNA-seq 构建乳腺细胞图谱的完整思路(附生信复现资源)
人工智能·深度学习·算法·机器学习
季明洵1 小时前
Java实现栈和最小栈
java·开发语言·数据结构·
Wect1 小时前
LeetCode 106. 从中序与后序遍历序列构造二叉树:题解+思路拆解
前端·算法·typescript
qq_454245031 小时前
上下文驱动的 ECS:一种反应式实体组件系统扩展
数据结构·算法·c#
fu的博客1 小时前
【数据结构6】栈的四种形态:递增/递减,满栈/空栈深度解析
数据结构
xiaoye-duck1 小时前
《算法题讲解指南:优选算法-双指针》--03快乐数,04盛水最多的容器
c++·算法
铸人1 小时前
再论自然数全加和 - 质数螺旋
数学·算法·数论·复数
汉克老师1 小时前
GESP2024年3月认证C++二级( 第一部分选择题(1-8))
c++·算法·循环结构·分支结构·gesp二级·gesp2级
坚持就完事了2 小时前
数据结构之堆(Java\Python双语实现)
java·数据结构·算法