
线段树的核心是分治思想 —— 将大区间拆分为小区间,通过维护子区间信息拼凑出父区间结果。但当遇到最大子段和这类无法用单一数值维护的问题时,基础线段树就显得力不从心。而 “线段树 + 分治” 的组合,通过让线段树节点维护更丰富的分治相关信息,再利用分治思想整合结果,完美解决了这类复杂区间问题。 本文将从经典的最大子段和问题入手,深入拆解线段树与分治的结合逻辑,手把手实现 “小白逛公园” 模板题,再拓展到更复杂的 01 序列操作,让你彻底掌握这种进阶思路!下面就让我们正式开始吧!

先看一个经典问题:给定一个序列,支持两种操作 —— 修改某个元素的值、查询某个区间内的最大子段和(连续子序列的最大和)。
如果用暴力解法,查询时遍历所有子段,时间复杂度 O (n²),面对 10⁵级别的数据直接超时;如果用基础线段树,只维护区间和、最大值,根本无法拼凑出 “连续子段” 的最大和 —— 因为最大子段可能横跨左右两个子区间,仅靠左右孩子的最大值无法计算。
核心痛点:复杂区间问题的结果,往往需要结合 “左区间内部”“右区间内部”“跨区间” 三种情况,基础线段树的单一信息维护无法覆盖。
而分治思想恰好能解决这个问题:对于一个区间的最大子段和,无非是三种情况的最大值:
“线段树 + 分治” 的核心思路就是:让线段树的每个节点,维护分治所需的关键信息(如区间和、前缀最大和、后缀最大和、区间内最大子段和),查询时通过分治逻辑整合这三种情况的结果,最终得到答案。
以最大子段和问题为例,我们需要线段树的每个节点维护以下 4 类信息,才能通过分治整合出父区间的结果:
这四类信息的分治整合逻辑(pushup 函数)是关键,我们用公式明确:
通过这些信息,就能完整覆盖分治的三种情况,这就是 “线段树 + 分治” 的核心原理。
题目链接:https://www.luogu.com.cn/problem/P4513
给定 n 个公园的打分,支持两种操作:
每个节点维护 sum、lmax、rmax、max 四个分治信息,以及区间边界 l/r:
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]; // 原始数组(公园打分)这是最核心的函数,严格按照之前的公式实现:
// 用左孩子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);
}叶子节点的信息的初始化:
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]); // 整合信息
}修改某个位置的值后,递归更新路径上所有节点的分治信息(通过 pushup):
// 将位置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]); // 更新父节点信息
}查询区间 [a,b] 时,返回一个 Node 结构体,包含该区间的 sum、lmax、rmax、max 信息。查询过程是分治的核心:
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;
}
}#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;
}5 3
1
2
-3
4
5
1 2 3 // 查询[2,3]的最大子段和
2 2 -1 // 将位置2的打分改为-1
1 2 3 // 再次查询[2,3]的最大子段和题目链接:https://www.luogu.com.cn/problem/P2572

给定 01 序列,支持五种操作:
这道题的复杂度在于:
需要维护的信息(同时维护 0 和 1 的相关分治信息):
以 1 的信息为例(0 的信息逻辑相同):
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) { // 取反操作
// 交换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;
}
}按优先级下放:先下放置 0 / 置 1 标记(优先级高),再下放取反标记,最后清空当前节点标记。
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; // 清空标记
}
}叶子节点初始化:根据原始值,设置 0 和 1 的相关信息,懒标记初始为 - 1(无标记)。
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]);
}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 结构体,按需提取 s1 或 m1。
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;
}
}#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;
}通过以上两个例题,我们可以总结出 “线段树 + 分治” 的通用解题步骤,无论遇到哪种复杂区间问题,都可以按这个思路推导:
明确问题的结果需要覆盖哪些分治情况(如最大子段和的三种情况),进而确定每个节点需要维护的信息。例如:
这是分治的核心,明确如何通过左右孩子的信息,整合出父节点的信息。设计时要确保覆盖所有分治情况,避免遗漏(如跨区间的情况)。
如果涉及区间修改,需要设计对应的懒标记,并明确标记的优先级和下放逻辑。懒标记的处理不能破坏分治信息的正确性,例如取反操作需要交换 0 和 1 的所有分治信息。
“线段树 + 分治” 的代码复杂度较高,新手容易踩坑,以下是五大高频易错点:
例如只维护了 1 的分治信息,忘记维护 0 的信息,导致取反操作无法实现;或遗漏跨区间的情况(如最大子段和忘记左后缀 + 右前缀)。
这是最致命的错误,例如前缀最大和的计算错误(误将 “左总和 + 右前缀” 写成 “左前缀 + 右总和”),会导致整个分治逻辑失效。建议手动推导简单案例,验证 pushup 逻辑。
例如置 0 / 置 1 标记的优先级低于取反标记,导致置 0 后再取反的结果错误。记住:覆盖类操作(置 0 / 置 1)的优先级高于翻转类操作(取反)。
应按优先级从高到低下放标记,否则会出现标记叠加错误。例如先下放取反标记,再下放置 0 标记,会导致取反操作被置 0 操作覆盖,结果错误。
当序列长度较大或数值较大时,分治信息(如 sum)可能超过 int 范围,需用 long long 存储,避免溢出。
“线段树 + 分治” 是解决复杂区间问题的终极思路,其核心在于用分治思想定义节点信息,用线段树高效维护这些信息。掌握这种思路后,无论是最大子段和、最长连续相同元素、还是复杂的 01 序列操作,都能迎刃而解。 线段树 + 分治的学习门槛较高,但一旦掌握,就能应对算法竞赛中绝大多数区间类难题。希望本文能帮你建立清晰的思路,在刷题中逐步吃透这种进阶用法,祝你在算法之路上越走越远! 创作不易,如果本文对你有帮助,欢迎点赞、收藏、关注三连~