1. 问题背景与题目解析
力扣1749题是一道经典的数组处理问题,题目通常描述为:给定一个整数数组nums,找到其中连续子数组的最大绝对值之和。这道题看似简单,但考察了算法设计中的多个核心概念,特别是动态规划和前缀和这两种常用技巧的灵活运用。
在实际工程中,类似的问题场景比比皆是。比如电商平台需要计算某商品连续30天的销售额波动最大值,金融领域需要分析股票价格的连续波动区间,甚至游戏开发中也需要处理玩家连续登录天数的统计。理解这类问题的解法,对提升编程思维和解决实际问题都大有裨益。
题目给出的示例通常是这样的:
输入:nums = [1,-3,2,3,-4]
输出:9
解释:子数组[2,3,-4]对应的绝对值为|2+3-4|=|1|=1,而子数组[2,3]对应的绝对值为|2+3|=5。但题目要求的其实是所有可能子数组的绝对值中的最大值。
注意:这里容易产生理解偏差。题目实际要求的是"任意连续子数组的和的绝对值的最大值",而不是"绝对值数组的子数组和的最大值"。这个细微差别直接影响解题思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法与问题分析
我们先从最直观的暴力解法入手,这能帮助我们深入理解问题本质。暴力法的思路是枚举所有可能的连续子数组,计算每个子数组的和的绝对值,然后取最大值。
python复制def maxAbsoluteSum(nums):
max_abs = 0
n = len(nums)
for i in range(n):
current_sum = 0
for j in range(i, n):
current_sum += nums[j]
max_abs = max(max_abs, abs(current_sum))
return max_abs
这种方法的时间复杂度是O(n²),当n较大时(比如10^5),性能将无法接受。这就引出了我们需要优化的核心问题:如何避免重复计算子数组和?
在力扣测试中,暴力解法通常会因为超时无法通过所有测试用例。我曾在实际面试中被要求优化一个类似的解法,面试官特意将数组长度设为10^5量级,就是为了考察对算法复杂度的敏感度。
3. 前缀和优化思路
前缀和(Prefix Sum)是处理子数组求和问题的利器。其核心思想是预处理一个前缀和数组prefix,其中prefix[i]表示nums[0..i-1]的和。这样任意子数组nums[i..j]的和就可以表示为prefix[j+1] - prefix[i]。
对于本题,我们可以这样利用前缀和:
- 计算前缀和数组
- 找到前缀和的最大值和最小值
- 结果就是max_prefix - min_prefix的绝对值
python复制def maxAbsoluteSum(nums):
prefix = [0]
for num in nums:
prefix.append(prefix[-1] + num)
return max(prefix) - min(prefix)
这个解法的时间复杂度是O(n),空间复杂度也是O(n)。在实际编码时,我们可以进一步优化空间,只记录当前前缀和以及历史最大值、最小值:
python复制def maxAbsoluteSum(nums):
max_prefix = min_prefix = current = 0
for num in nums:
current += num
max_prefix = max(max_prefix, current)
min_prefix = min(min_prefix, current)
return max_prefix - min_prefix
关键点:为什么最大值减去最小值就是答案?因为子数组和的绝对值最大,要么是某个前缀和很大,要么是某个前缀和很小,两者相减就能覆盖所有可能性。
4. 动态规划解法详解
动态规划是解决这类极值问题的另一把利器。我们可以定义两个状态数组:
- dp_max[i]:以nums[i]结尾的子数组的最大和
- dp_min[i]:以nums[i]结尾的子数组的最小和
状态转移方程为:
dp_max[i] = max(nums[i], dp_max[i-1] + nums[i])
dp_min[i] = min(nums[i], dp_min[i-1] + nums[i])
最终结果是所有dp_max和dp_min中的绝对值的最大值。
python复制def maxAbsoluteSum(nums):
if not nums:
return 0
dp_max = dp_min = nums[0]
res = abs(nums[0])
for i in range(1, len(nums)):
dp_max = max(nums[i], dp_max + nums[i])
dp_min = min(nums[i], dp_min + nums[i])
res = max(res, abs(dp_max), abs(dp_min))
return res
这个解法同样具有O(n)时间复杂度和O(1)空间复杂度(通过滚动变量优化)。动态规划的思路更加通用,可以处理更复杂的子数组问题变种。
5. 两种解法的对比与选择
在实际应用中,前缀和解法和动态规划各有优劣:
| 比较维度 | 前缀和解法 | 动态规划解法 |
|---|---|---|
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(1)优化版 | O(1) |
| 适用场景 | 特定子数组问题 | 更通用的极值问题 |
| 扩展性 | 适合固定模式问题 | 容易修改适应变种 |
| 理解难度 | 需要数学直觉 | 标准DP套路 |
根据我的经验,在面试或竞赛中:
- 如果问题明确与子数组和相关,优先考虑前缀和
- 如果问题涉及更复杂条件或状态,选择动态规划
- 当不确定时,先尝试动态规划思路
6. 常见错误与调试技巧
在解决这类问题时,容易陷入以下几个陷阱:
-
理解偏差:如开头所述,容易误解题目是求"绝对值数组的最大子数组和"。实际样例输入[1,-3,2,3,-4]的正确解是子数组[2,3]的和5,而不是绝对值数组[1,3,2,3,4]的最大子数组和(整个数组和13)。
-
初始化错误:在动态规划解法中,dp_max和dp_min的初始值应该设为nums[0],而不是0。否则对于全负数数组会出错。
-
边界条件:空数组输入应该返回0。单元素数组的返回值就是该元素的绝对值。
-
更新顺序:在优化空间的前缀和解法中,必须先更新current,再更新max_prefix和min_prefix,顺序不能颠倒。
调试时可以构造以下测试用例:
- 全正数数组:[1,2,3] → 6
- 全负数数组:[-1,-2,-3] → 6
- 正负交替数组:[1,-2,3,-4,5] → 5
- 单元素数组:[5] → 5
- 空数组:[] → 0
7. 算法扩展与实际应用
这个问题可以延伸出多个变种,都是面试中的高频题目:
- 最大子数组和:经典的Kadane算法问题,去掉绝对值条件
- 最小子数组和:将max改为min
- 最接近目标的子数组和:前缀和+二分查找或滑动窗口
- 二维矩阵中的最大子矩阵和:将问题扩展到二维
在实际工程中,这类算法常用于:
- 金融领域的最大回撤计算
- 信号处理中的峰值检测
- 时间序列数据分析
- 计算机视觉中的滑动窗口处理
我在电商平台工作期间,曾用类似算法分析用户连续登录天数的波动情况,为运营策略提供数据支持。通过将用户每日登录记录转化为1和0的数组,计算最大连续登录区间,有效识别了核心用户群体。
8. 编码实现与优化技巧
对于Python实现,有几个性能优化点值得注意:
-
避免不必要的abs计算:在动态规划解法中,每次迭代都计算abs会稍微影响性能。可以在最后统一取最大值。
-
使用数学库函数:max()和min()函数在Python中是内置的,速度很快。但在某些语言中,自己实现比较可能更快。
-
空间优化:如前面所示,无论是前缀和还是DP,都可以将空间复杂度优化到O(1)。
-
提前终止:如果当前前缀和已经超过历史最大/最小值很多,可以考虑提前终止循环。不过这种优化通常效果有限。
C++实现示例:
cpp复制int maxAbsoluteSum(vector<int>& nums) {
int max_sum = 0, min_sum = 0, current = 0;
for (int num : nums) {
current += num;
max_sum = max(max_sum, current);
min_sum = min(min_sum, current);
}
return max_sum - min_sum;
}
在力扣竞赛中,这类问题通常需要处理10^5量级的数据,因此O(n²)的解法肯定超时。我建议在平时练习时,就养成分析时间复杂度的习惯,避免在实际面试中写出看似正确但效率低下的代码。
