最大子段和
Kadane·环形·两段不相交
本课摘要
最大子段和课程回答“如何用一个局部状态维护最大连续子段”。内容以Kadane·环形·两段不相交为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断最大子段和的适用条件与状态边界
- 围绕“Kadane·环形·两段不相交”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
什么是「最大子段和」
给一串数 (可正可负),子段是原序列里连续的一段 —— 注意和「子序列」不同,子段必须挨着取、不能跳。我们要找的,是所有子段里和最大的那一段(一般要求非空,至少含一个数)。
盯住上图那段 :它跨过了一个负数 ,可总和 仍是最优。 这就是难点所在——要不要把当前这个数接进来,取决于前面攒下的和是正是负:前面若攒了正的一坨(如 ),哪怕眼下遇到 也该接上去滚成 ; 可前面若攒成了负数,那这负担就该果断丢掉、从当前数重新起一段。
最笨的办法是枚举左右端点 再累加,那是 ;加前缀和也要 。 当 ,这些都会超时。下面用一个只扫一遍的 DP——Kadane 算法——把它压到 。
状态与转移:接续,还是另起
沿用 LIS 那把母题抓手:最大子段落在哪儿事先不知道,与其对「全局最优段」直接设状态,不如钉住它的结尾。 设 表示:以 为最后一个元素的最大子段和。于是 个不同结尾把所有候选段分门别类地兜住,一个也不漏、一个也不重。
怎么算 ?既然它以 结尾,那 左边紧挨着的那段,只有两种可能:
接续:把 接在「以 结尾的最优段」后面,得 。
另起:前面那段是负担(),干脆扔掉,让 自己单独成一段,得 。
两条路取较大,就是转移方程:
边界:(第一个数只能自成一段)。答案不是 ——因为最大子段可在任何位置收尾——而是整个 数组的最大值:
本质
「以 结尾」这个限定,把「求全局最大子段」拆成了 个可顺序递推的小问题。转移只回看一格 ,于是一趟 扫描就够,连数组都能省成一个滚动变量。★答案取全行最大,且全为负数时 初值必须设成 而非 ,否则会误答 0。
跟着算一遍
用序列 走几步(下标从 1 记),把方程跑起来:
看它一格一格长出来
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
深化 · 环形:断环与补集
换个设定:这串数首尾相接成一个环,子段可以跨过末尾绕回开头(例如 是合法的一段)。最大子段和又该怎么求?
分两种情况。其一,最优段不跨首尾——那它就是普通的一段,直接跑一遍上面的 Kadane 即可。其二,最优段跨过了首尾——这时它由「结尾的一截」和「开头的一截」拼成,绕过了中间某一段。 关键一步是反着看:一个「绕首尾的段」和它「中间被绕过的那段」正好互补,两者拼起来是整个环。记 ,则「绕首尾的最大段」等于总和减去中间被绕过的那段:
要让绕首尾的段最大,就要让中间挖掉的那段最小——而这个 (最小子段和)把 Kadane 里的 换成 就能一趟求出。最终答案取两种情况的较大者:
常见陷阱 · 全正数会绕整圈
用 时,若数组全为正数,最小子段会退化成「最小的单个元素」, 相当于「几乎绕整整一圈」,把同一个环重复计入——非法。 稳妥写法:仅当最小子段没有吃掉整个数组(还留下至少一个元素)时才采用补集;实践中若普通 Kadane 的结果已 ,直接取两者较大即可自然避开这个坑。
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
例题
#include <algorithm>
#include <iostream>
using namespace std;
int n, a;
int dp, ans; // dp:以当前数结尾的最大子段和;ans:全局最大
int main()
{
cin >> n;
cin >> a;
dp = a; // 第一个数:只能自成一段
ans = a; // ★ans 初值设成 a[1],别设 0(全负时会错)
for (int i = 2; i <= n; i++)
{
cin >> a;
dp = max(dp + a, a); // 接续 dp+a 还是另起 a,取较大
ans = max(ans, dp); // 子段可在任意位置结尾,随时刷新全局最大
}
cout << ans << endl;
return 0;
}
// TAG: 线性DP 最大子段和 Kadane#include <algorithm>
#include <iostream>
using namespace std;
const int MX = 1000005;
long long a[MX];
long long pre[MX], suf[MX]; // pre[i]:以 i 结尾的最大子段;suf[i]:以 i 开头的最大子段
long long bp[MX], bs[MX]; // bp[i]:前缀 [1..i] 里的最大子段;bs[i]:后缀 [i..n] 里的最大子段
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
pre[1] = a[1];
for (int i = 2; i <= n; i++) // 正向 Kadane,落成「以 i 结尾」
{
pre[i] = max(pre[i - 1] + a[i], a[i]);
}
bp[1] = pre[1];
for (int i = 2; i <= n; i++) // 前缀最大子段(可结尾于 ≤ i 处)
{
bp[i] = max(bp[i - 1], pre[i]);
}
suf[n] = a[n];
for (int i = n - 1; i >= 1; i--) // 反向 Kadane,落成「以 i 开头」
{
suf[i] = max(suf[i + 1] + a[i], a[i]);
}
bs[n] = suf[n];
for (int i = n - 1; i >= 1; i--) // 后缀最大子段(可开头于 ≥ i 处)
{
bs[i] = max(bs[i + 1], suf[i]);
}
long long ans = -0x3f3f3f3f3f3f3f3f;
for (int i = 2; i < n; i++) // 枚举被跳过的中间数 i:左段收尾于 ≤ i-1,右段起于 ≥ i+1
{
ans = max(ans, bp[i - 1] + bs[i + 1]); // 两段不相交且至少隔开中间的 i,各取自己那侧的最大
}
cout << ans << endl;
return 0;
}
// TAG: 线性DP 最大子段和 两段不相交 前后缀#include <algorithm>
#include <iostream>
using namespace std;
const int MX = 200005;
const long long INF = 0x3f3f3f3f3f3f3f3f;
int n;
long long a[MX], tot;
// 序列 b[l..r] 里选「恰好 K 段不相交、非空」子段的最大总和。
// f[j]=已选 j 段的最大和;g[j]=已选 j 段且第 j 段延伸到当前位的最大和。
long long kmax(long long b[], int l, int r, int K)
{
long long f[3], g[3];
for (int j = 0; j <= K; j++)
{
f[j] = -INF, g[j] = -INF;
}
f[0] = 0;
for (int i = l; i <= r; i++)
{
for (int j = K; j >= 1; j--) // ★逆序 j,防第 j 段在本轮被重复计入
{
g[j] = max(f[j - 1], g[j]) + b[i]; // 新开一段 或 延续第 j 段
f[j] = max(f[j], g[j]);
}
}
return f[K];
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
tot += a[i];
}
// 情况一:两段都不跨首尾 —— 序列 a[1..n] 上直接选最大两段。
long long ans = kmax(a, 1, n, 2);
// 情况二:有段跨首尾 —— 剩下的绕首尾两段 = 总和 − (中间挖掉的最小两段)。
// 挖掉的两段必须落在「掐头去尾」的 a[2..n−1] 内,才能把环切成两段弧(首尾各留 ≥1)。
// 最小两段 = −(在 −a 上的最大两段),故 ans 候选 = tot + kmax(−a, 2..n−1, 2)。
if (n >= 4) // 内层至少 2 个元素才能挖两段
{
for (int i = 1; i <= n; i++)
{
a[i] = -a[i];
}
ans = max(ans, tot + kmax(a, 2, n - 1, 2));
}
cout << ans << endl;
return 0;
}
// TAG: 线性DP 环状最大两段子段和 K段DP 补集练习
小字说明:最大子段和的洛谷原生练习池偏窄(多数同类题是本页例题本身)。除上面两题外,建议把例题 P1121 / P2642 当自测——先合上参考代码独立写、再对照,是巩固「环形 / 两段不相交」最有效的方式。

