目录
[一、为什么需要 "线段树 + 分治"?](#一、为什么需要 “线段树 + 分治”?)
[三、实战入门:最大子段和(洛谷 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 核心辅助函数)
[4.2.3 建树、修改、查询操作](#4.2.3 建树、修改、查询操作)
[(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⁵级别的数据直接超时;如果用基础线段树,只维护区间和、最大值,根本无法拼凑出 "连续子段" 的最大和 ------ 因为最大子段可能横跨左右两个子区间,仅靠左右孩子的最大值无法计算。
核心痛点:复杂区间问题的结果,往往需要结合 "左区间内部""右区间内部""跨区间" 三种情况,基础线段树的单一信息维护无法覆盖。
而分治思想恰好能解决这个问题:对于一个区间的最大子段和,无非是三种情况的最大值:
- 完全在左子区间内;
- 完全在右子区间内;
- 横跨左右子区间(左子区间的后缀最大和 + 右子区间的前缀最大和)。
"线段树 + 分治" 的核心思路就是:让线段树的每个节点,维护分治所需的关键信息(如区间和、前缀最大和、后缀最大和、区间内最大子段和),查询时通过分治逻辑整合这三种情况的结果,最终得到答案。
二、核心原理:线段树节点该维护哪些分治信息?
以最大子段和问题为例,我们需要线段树的每个节点维护以下 4 类信息,才能通过分治整合出父区间的结果:
- sum:当前区间的总和 ------ 用于计算跨区间子段和(左后缀 + 右前缀);
- lmax:当前区间以左端点为起点的最大子段和(前缀最大和);
- rmax:当前区间以右端点为终点的最大子段和(后缀最大和);
- max:当前区间内的最大子段和(最终需要的结果)。
这四类信息的分治整合逻辑(pushup 函数)是关键,我们用公式明确:
- 父区间总和 = 左孩子总和 + 右孩子总和;
- 父区间前缀最大和 = max (左孩子前缀最大和,左孩子总和 + 右孩子前缀最大和);
- 父区间后缀最大和 = max (右孩子后缀最大和,右孩子总和 + 左孩子后缀最大和);
- 父区间最大子段和 = max (左孩子最大子段和,右孩子最大子段和,左孩子后缀最大和 + 右孩子前缀最大和)。
通过这些信息,就能完整覆盖分治的三种情况,这就是 "线段树 + 分治" 的核心原理。
三、实战入门:最大子段和(洛谷 P4513 小白逛公园)
题目链接:https://www.luogu.com.cn/problem/P4513

3.1 题目要求
给定 n 个公园的打分,支持两种操作:
- 修改某个公园的打分;
- 查询区间 [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 信息。查询过程是分治的核心:
- 如果当前节点区间完全覆盖查询区间,直接返回该节点的信息;
- 如果查询区间只在左子树或右子树,递归查询并返回结果;
- 如果查询区间横跨左右子树,分别查询左子树的重叠部分(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,2,-3,4,5];
- 第一次查询 [2,3]:区间值为 [2,-3],最大子段和是 2(仅取 2),输出 2;
- 修改位置 2 为 - 1 后,数组变为 [1,-1,-3,4,5];
- 第二次查询 [2,3]:区间值为 [-1,-3],最大子段和是 - 1(仅取 - 1),输出 - 1;
- 输出结果与题目示例一致,代码正确。
四、进阶挑战:01 序列的复杂分治维护(洛谷 P2572 [SCOI2010] 序列操作)
题目链接:https://www.luogu.com.cn/problem/P2572

4.1 题目要求
给定 01 序列,支持五种操作:
- 区间 [L,R] 全部改为 0;
- 区间 [L,R] 全部改为 1;
- 区间 [L,R] 全部取反(0→1,1→0);
- 查询区间 [L,R] 内 1 的个数;
- 查询区间 [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 序列操作,都能迎刃而解。
线段树 + 分治的学习门槛较高,但一旦掌握,就能应对算法竞赛中绝大多数区间类难题。希望本文能帮你建立清晰的思路,在刷题中逐步吃透这种进阶用法,祝你在算法之路上越走越远!
创作不易,如果本文对你有帮助,欢迎点赞、收藏、关注三连~
