任意子数组和的绝对值的最大值 图解题解
这道题到底在问什么
- 输入
- nums=[1,-3,2,3,-4]
- 输出
- 5
- 输入
- nums=[2,-5,1,-4,3,-2]
- 输出
- 8
- 输入
- nums=[-4,3,2,-3,1]
- 输出
- 5
最优解:为什么这么做
一句话答案:LeetCode 1749 任意子数组和的绝对值最大值:绝对值最大要么正得最多要么负得最多,同时跑最大与最小两个 Kadane,答案取二者绝对值较大者,一遍扫描,时间 O(n)、空间 O(1)。
1749 要的『绝对值最大』和求最大和差在哪
给一个整数数组 nums。子数组是数组里挨着的一小段(必须连续、不能跳着挑),把它加起来再取绝对值(负变正、正不变),题目要找绝对值最大的那一段。子数组允许取空、空和为 0,答案至少是 0。题面的 [2,-5,1,-4,3,-2] 答案是 8,来自 [-5,1,-4],和 -8 取绝对值正好 8。
n²/2 条子数组全枚举,为什么大数组吃不消
长度 n 的数组里挨着的子数组约有 n²/2 条,全枚举、逐段取绝对值比大小就是 O(n²)(大 O 记号描述数据规模变大时操作数怎么涨);每段再从头加一遍求和会退化到 O(n³)。数据一大就跑不动,且相邻子数组的和大量重叠、白白重算。
绝对值最大,该往正的追还是往负的追
绝对值最大有两种来路:这段和要么正得最多,要么负得最多、取绝对值后反而大。只找最大和会漏掉很负的段,反过来又漏掉纯正的段,所以最大和、最小和各算一遍,答案取绝对值更大的那个。
还有个等价看法:任何一段的和 = 两个前缀和相减(前缀和就是从开头一直加到某个位置的累计和)。把见过的前缀和最大值记 maxP、最小值记 minP,答案就是 maxP 减 minP。maxP、minP 谁前谁后无所谓:minP 在后时这段和是负的,取绝对值恰好还是 maxP 减 minP。「一个数都不加」的空前缀 P0 = 0 也要算进候选。
参考代码为什么用 f、g 两个变量、不画前缀和
参考代码不存整条前缀和,而是用两个滚动变量(只留最近状态、循环覆盖旧值)边扫边追。f 记「以当前元素结尾的最大子数组和」,每步 f = max(f, 0) + x:前面攒的是负担(负数)就丢掉、从当前元素重开,否则接着加;g 记「以当前元素结尾的最小子数组和」,对称写成 g = min(g, 0) + x。这套扫法就是 Kadane(求最大子数组和的经典一遍扫描)。
每一步 ans = max(ans, f, |g|):f 取原值、g 取绝对值,和 maxP 减 minP 算的是同一个答案。
拿 [2,-5,1,-4,3,-2] 亲手滚一遍前缀和
从空前缀 P0 = 0 起步一格格累加:P1 = 0+2 = 2;P2 = 2+(-5) = -3;P3 = -3+1 = -2;P4 = -2+(-4) = -6;P5 = -6+3 = -3;P6 = -3+(-2) = -5。一路只盯两个数:maxP 停在 2(在 P1),minP 降到 -6(在 P4)。
答案 = maxP 减 minP = 2 减 -6 = 8。这个差对应 P1 和 P4 之间的子数组 [-5,1,-4],和 -8 取绝对值就是 8。
漏掉空前缀 P0 = 0,单个负数为什么会算错
全程只扫一遍,时间 O(n)(n 是数组长度);只用 maxP、minP(或 f、g)和 ans 几个变量,空间 O(1)。
漏掉空前缀 P0 = 0 是最常见的坑:只有一个负数 [-7] 时,全靠空前缀 0 兜底让答案取到 |-7| = 7,少了这一票就答成 -7。另一处是只追一个方向:答案来自最负那段时,光找最大和就把它漏掉了——此时 maxP 恰好是空前缀 0,同样得靠它才减得出。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢一句:子数组和 = 两个前缀和相减,所以答案 = 最大前缀和 maxP 减 最小前缀和 minP。累加器从左扫一遍,顺手追 maxP 和 minP,别漏空前缀 P0 = 0。下面每帧都在套这句。
- 4先看清画面。上面是源数组 nums = [2,-5,1,-4,3,-2],一共 6 个数。右边那张表记的是前缀和序列 P,现在只有一个 P0 = 0,代表「一个数都不加」的空前缀。我们准备一个累加器 prefix,起点是 0,再准备两个记录器 maxP 和 minP,都先等于 0。接下来紫色指针从下标 0 往右一格一格走,每走一格就把当前数加进 prefix,得到新的前缀和记进右表,同时看它有没有刷新最大或最小。先从第 0 位开始。
- 5轮到第 0 位。紫色指针落在源数组下标 0,值是 2。右边表里最后一格 P0 现在是 0,它就是把前 0 个数加起来的前缀和,也是我的累加器此刻的值。接下来只要把当前这个 2 累加进去,就能得到下一个前缀和 P1。
- 6把累加器 0 加上当前的 2,得到新的前缀和 2,把它作为 P1 记进右表最后一行。这个 2 就是「从开头一直加到第 0 位」的总和。它同时也是下一轮的累加器起点。现在还差一步:看看这个新前缀和有没有刷新历史的最大或最小。
- 7拿新前缀和 2 和已记录的最大 maxP、最小 minP 比一比。它比原来的 maxP 还大,于是 maxP 被刷新成 2。此刻答案候选 ans 等于 maxP 减 minP,也就是 2 减 0 = 2。maxP 抬高,意味着找到了一个更靠上的前缀和,未来它减去某个很小的前缀和,差可能更大。源数组这一格标绿表示处理完,继续往右走。
- 8轮到第 1 位。紫色指针落在源数组下标 1,值是 -5。右边表里最后一格 P1 现在是 2,它就是把前 1 个数加起来的前缀和,也是我的累加器此刻的值。接下来只要把当前这个 -5 累加进去,就能得到下一个前缀和 P2。
- 9把累加器 2 加上当前的 -5,得到新的前缀和 -3,把它作为 P2 记进右表最后一行。这个 -3 就是「从开头一直加到第 1 位」的总和。它同时也是下一轮的累加器起点。现在还差一步:看看这个新前缀和有没有刷新历史的最大或最小。
- 10拿新前缀和 -3 和已记录的最大 maxP、最小 minP 比一比。它比原来的 minP 还小,于是 minP 被刷新成 -3。此刻答案候选 ans 等于 maxP 减 minP,也就是 2 减 -3 = 5。minP 压低,意味着找到了一个更靠下的谷底,让某个较大前缀和减去它时,差被拉得更大。源数组这一格标绿表示处理完,继续往右走。
- 11轮到第 2 位。紫色指针落在源数组下标 2,值是 1。右边表里最后一格 P2 现在是 -3,它就是把前 2 个数加起来的前缀和,也是我的累加器此刻的值。接下来只要把当前这个 1 累加进去,就能得到下一个前缀和 P3。
- 12把累加器 -3 加上当前的 1,得到新的前缀和 -2,把它作为 P3 记进右表最后一行。这个 -2 就是「从开头一直加到第 2 位」的总和。它同时也是下一轮的累加器起点。现在还差一步:看看这个新前缀和有没有刷新历史的最大或最小。
- 13拿新前缀和 -2 和已记录的最大 maxP = 2、最小 minP = -3 比一比。它落在两者之间,既没超过最大,也没低于最小,所以 maxP 和 minP 都不动。答案候选 ans 依旧是 2 减 -3 = 5。不是每一步都会刷新极值,但每一步都要比一比才安心。源数组这一格标绿表示处理完,继续往右走。
- 14轮到第 3 位。紫色指针落在源数组下标 3,值是 -4。右边表里最后一格 P3 现在是 -2,它就是把前 3 个数加起来的前缀和,也是我的累加器此刻的值。接下来只要把当前这个 -4 累加进去,就能得到下一个前缀和 P4。
- 15把累加器 -2 加上当前的 -4,得到新的前缀和 -6,把它作为 P4 记进右表最后一行。这个 -6 就是「从开头一直加到第 3 位」的总和。它同时也是下一轮的累加器起点。现在还差一步:看看这个新前缀和有没有刷新历史的最大或最小。
- 16拿新前缀和 -6 和已记录的最大 maxP、最小 minP 比一比。它比原来的 minP 还小,于是 minP 被刷新成 -6。此刻答案候选 ans 等于 maxP 减 minP,也就是 2 减 -6 = 8。minP 压低,意味着找到了一个更靠下的谷底,让某个较大前缀和减去它时,差被拉得更大。源数组这一格标绿表示处理完,继续往右走。
- 17轮到第 4 位。紫色指针落在源数组下标 4,值是 3。右边表里最后一格 P4 现在是 -6,它就是把前 4 个数加起来的前缀和,也是我的累加器此刻的值。接下来只要把当前这个 3 累加进去,就能得到下一个前缀和 P5。
- 18把累加器 -6 加上当前的 3,得到新的前缀和 -3,把它作为 P5 记进右表最后一行。这个 -3 就是「从开头一直加到第 4 位」的总和。它同时也是下一轮的累加器起点。现在还差一步:看看这个新前缀和有没有刷新历史的最大或最小。
- 19拿新前缀和 -3 和已记录的最大 maxP = 2、最小 minP = -6 比一比。它落在两者之间,既没超过最大,也没低于最小,所以 maxP 和 minP 都不动。答案候选 ans 依旧是 2 减 -6 = 8。不是每一步都会刷新极值,但每一步都要比一比才安心。源数组这一格标绿表示处理完,继续往右走。
- 20轮到第 5 位。紫色指针落在源数组下标 5,值是 -2。右边表里最后一格 P5 现在是 -3,它就是把前 5 个数加起来的前缀和,也是我的累加器此刻的值。接下来只要把当前这个 -2 累加进去,就能得到下一个前缀和 P6。
- 21把累加器 -3 加上当前的 -2,得到新的前缀和 -5,把它作为 P6 记进右表最后一行。这个 -5 就是「从开头一直加到第 5 位」的总和。它同时也是下一轮的累加器起点。现在还差一步:看看这个新前缀和有没有刷新历史的最大或最小。
- 22拿新前缀和 -5 和已记录的最大 maxP = 2、最小 minP = -6 比一比。它落在两者之间,既没超过最大,也没低于最小,所以 maxP 和 minP 都不动。答案候选 ans 依旧是 2 减 -6 = 8。不是每一步都会刷新极值,但每一步都要比一比才安心。六个数全部扫完了。
- 23扫完看结果。整条前缀和序列里,最大值 maxP = 2 出现在 P1,最小值 minP = -6 出现在 P4。答案就是 maxP 减 minP = 2 减 -6 = 8。这个差对应的是哪一段?正是位于 P1 和 P4 之间的那几个元素,也就是绿色高亮的子数组 [-5,1,-4]。它的和是 -8,是一段很负的和,取绝对值正好是 8。这说明和的绝对值最大,来自一段特别负的子数组,而不是一段很正的。
- 24回看全程:累加器从空前缀 0 出发,滚出前缀和序列 [0,2,-3,-2,-6,-3,-5]。一路上我们只盯着两个数,见过的最大前缀和 maxP 一路涨到 2,见过的最小前缀和 minP 一路降到 -6。最后把这两个极值一减,2 减 -6 = 8,就是任意子数组和的绝对值最大值。整道题没有嵌套循环,一遍扫描、三个变量,线性时间就搞定。
⚠️ 容易写错的地方
✗ 错:只跑一遍求最大子数组和(单向 Kadane)就当答案,漏了负向
✓ 对:同时追最大前缀和 maxP 和最小前缀和 minP 两个方向,答案取 maxP 减 minP
和的绝对值最大,可能来自一段很正的子数组,也可能来自一段很负的。本例答案 8 就来自和为 -8 的负段 [-5,1,-4]。只追最大会把这段漏掉,必须两个方向都盯
✗ 错:用前缀和时纠结 maxP 的下标一定要在 minP 前面才合法
✓ 对:maxP 减 minP 恒为非负,且必对应某个真实子数组和的绝对值,不用管谁先谁后
若 minP 在前、maxP 在后,这个差就是一段正子数组的和;若 maxP 在前、minP 在后,差就是一段负子数组和的绝对值。两种都是合法子数组,直接相减即可,不必额外判断顺序
✗ 错:忘了把空前缀 P0 = 0 纳入 maxP、minP 的比较
✓ 对:前缀和序列从 0 起步,0 本身也是候选极值
子数组允许为空、空和为 0,答案至少是 0;而且以开头 nums[0] 起步的子数组,它的和要靠减去 P0 = 0 才算得出。漏掉这个 0,遇到「从头开始最优」的情形就会算错
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maxAbsoluteSum(self, nums: List[int]) -> int:
f = g = 0
ans = 0
for x in nums:
f = max(f, 0) + x
g = min(g, 0) + x
ans = max(ans, f, abs(g))
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int maxAbsoluteSum(vector<int>& nums) {
int f = 0, g = 0;
int ans = 0;
for (int& x : nums) {
f = max(f, 0) + x;
g = min(g, 0) + x;
ans = max({ans, f, abs(g)});
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int maxAbsoluteSum(int[] nums) {
int f = 0, g = 0;
int ans = 0;
for (int x : nums) {
f = Math.max(f, 0) + x;
g = Math.min(g, 0) + x;
ans = Math.max(ans, Math.max(f, Math.abs(g)));
}
return ans;
}
}复杂度
时间
O(n)
n 是数组长度。一个指针从左扫到右,每个位置只做一次累加、一次和 maxP、minP 的比较,都是常数操作,总共 n 次,所以是线性的 O(n)。参考代码的 f/g 写法同样是一遍扫描,复杂度一致。对比枚举所有子数组再求和的 O(n^2) 甚至 O(n^3) 笨办法,这套线性扫描优势明显
空间
O(1)
按峰值算。真正的实现只需要几个变量:一个累加器 prefix(或 f、g)、一个 maxP、一个 minP、一个答案 ans,不管数组多长,占用都是常数,所以额外空间 O(1)。动画右边画出整条前缀和序列 P 只是为了让你看清推导过程,算法本身并不需要把它们全存下来,滚动着比就行。三种语言都是 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 任意子数组和的绝对值的最大值 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么不能把最大子数组和与最小子数组和直接相加,或者只看一个?+
绝对值最大只会来自一个方向——要么一段正得最多、要么一段负得最多,两者不会同时贡献同一段,把它们相加没有意义(还可能把不相邻的两段凑一起,那根本不是一个子数组)。只看最大和会漏掉像 [-5,1,-4] 这种很负的段,只看最小和又会漏掉纯正的段。所以正确做法是两个各求一遍,答案在最大和与 |最小和| 里取大,等价于 maxP 减 minP。
参考代码里 f = max(f, 0) + x 的那个 0 是干嘛用的?+
它对应 Kadane 的核心决策:以当前元素结尾的最大子数组和,要么把前面攒的 f 接上、要么把前面全丢掉从当前元素重开。max(f, 0) 就是「前面攒成正的就留着、攒成负担(负数)就归零丢掉」,再加上当前 x。g 那边对称成 min(g, 0) + x,这里的负担指的是「正的前缀」,留负丢正。少了这个和 0 的取舍,遇到前面是负担的情况就没法重开,答案会偏。
这题和最大子数组和 LeetCode 53 是什么关系?+
53 只求最大子数组和,一个 Kadane 扫完就是答案。1749 要的是绝对值最大,多了「负得最多」这一路,于是在 53 的基础上再并排跑一个求最小子数组和的 Kadane(把每步的 max 换成 min),最后答案在最大和与最小和的绝对值里取大。会了 53,1749 就是多挂一条对称的最小 Kadane 加一个取绝对值。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 任意子数组和的绝对值的最大值 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。