文章目录
前言
本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。
一、题目
1、原题链接
2、题目描述
二、个人思路整理
1、思路分析
核心思路:同时维护最大值与最小值
如果只维护以 n u m s i numsi numsi 结尾的最大子数组乘积,遇到负数时就会丢失潜在的最大值。因此在遍历过程中,必须同时维护两个状态:
cur_max:以当前位置结尾的最大连续子数组乘积cur_min:以当前位置结尾的最小连续子数组乘积(绝对值最大的负数)
状态转移
到达第 i i i 个数 n u m s i numsi numsi 时,新的子数组乘积只有三种可能来源:
- 自己单开一段: n u m s i numsi numsi
- 延续前一个最大值: c u r _ m a x × n u m s i cur\_max \times numsi cur_max×numsi(当前为正数时可能产生最大值)
- 延续前一个最小值: c u r _ m i n × n u m s i cur\_min \times numsi cur_min×numsi(负负得正,当前为负数时可能产生最大值)因此转移方程为: n e x t _ m a x = max ( n u m s i , c u r _ m a x × n u m s i , c u r _ m i n × n u m s i ) next\_max = \max(numsi,\; cur\_max \times numsi,\; cur\_min \times numsi) next_max=max(numsi,cur_max×numsi,cur_min×numsi) n e x t _ m i n = min ( n u m s i , c u r _ m a x × n u m s i , c u r _ m i n × n u m s i ) next\_min = \min(numsi,\; cur\_max \times numsi,\; cur\_min \times numsi) next_min=min(numsi,cur_max×numsi,cur_min×numsi)
技巧提示:如果 n u m s i < 0 numsi < 0 numsi<0,乘以负数会让最大值变最小值、最小值变最大值,此时直接在计算前交换
cur_max和cur_min,转移逻辑会更简洁。
2、解题代码
cpp
class Solution {
public:
int maxProduct(vector<int>& nums) {
// ans:全局最大子数组乘积
// cur_max:以当前元素 nums[i] 结尾的连续子数组的最大乘积
// cur_min:以当前元素 nums[i] 结尾的连续子数组的最小乘积(用于捕捉负负得正的可能)
int ans = nums[0];
int cur_max = nums[0];
int cur_min = nums[0];
for (int i = 1; i < nums.size(); i++) {
// 当当前元素为负数时:
// 原来的最大值乘负数会变成极小值,原来的最小值乘负数会变成极大值。
// 提前交换两者的状态,可以极大简化后续的状态转移表达式。
if (nums[i] < 0) {
swap(cur_max, cur_min);
}
// 状态转移:
// 以 nums[i] 结尾的最大值只能来自于两种情况:
// 1. nums[i] 自身独立成段(应对前序乘积拖累或包含 0 的情况)
// 2. 延续前序乘积:cur_max * nums[i]
cur_max = max(nums[i], cur_max * nums[i]);
// 同理,更新以 nums[i] 结尾的最小值
cur_min = min(nums[i], cur_min * nums[i]);
// 每次循环结束,用当前结尾的最大乘积更新全局最优解
ans = max(ans, cur_max);
}
return ans;
}
};
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),只需遍历一次数组。
- 空间复杂度: O ( 1 ) O(1) O(1),仅需常数级别的滚动变量。
三、知识风暴
动态规划(Dynamic Programming) 是本题的核心算法思想。它通过将原问题拆解为若干重叠子问题,并利用「最优子结构」性质,用子问题的最优解递推得到全局最优解。对于「乘积最大子数组」这类具有明显递推关系的问题,动态规划能以 O ( n ) O(n) O(n) 的复杂度高效求解。
算法核心思想:
- 最优子结构 :以 n u m s i numsi numsi 结尾的最大子数组乘积,可以由「以 n u m s i − 1 numsi-1 numsi−1 结尾的最大/最小乘积」与「 n u m s i numsi numsi 自身」共同决定。只要子问题 c u r _ m a x cur\_max cur_max、 c u r _ m i n cur\_min cur_min 已知,就能递推得到当前位置的最优解。
- 重叠子问题 :在递推过程中,前一个位置的 c u r _ m a x cur\_max cur_max 和 c u r _ m i n cur\_min cur_min 会被反复使用。例如计算 d p i dpi dpi 和 d p i + 1 dpi+1 dpi+1 时都会用到 d p i − 1 dpi-1 dpi−1 的状态,因此用滚动变量缓存结果可避免重复计算。
- 与贪心的区别:贪心每一步只做当前最优选择、不回溯;而动态规划会同时维护最大值与最小值两个状态,枚举「独立成段」「延续最大值」「延续最小值」三种可能来源,从而保证结果的正确性。
常见对比:动态规划 vs 贪心
- 动态规划 :时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)。适合需要同时考虑正负号影响、且局部最优不能直接决定全局最优的场景,通用性更强。
- 贪心算法 :时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)。适合每一步的局部最优能直接推导全局最优的场景,代码简洁高效,但本题无法直接证明贪心成立(如
nums = [-2, 3, -4],若贪心只保留当前最大值,遇到负数时就会丢失「负负得正」的潜在最大值)。 - 共同点:两者都依赖「最优子结构」性质。区别在于贪心只保留一个当前最优状态,而动态规划需要同时维护最大值与最小值两个状态。
动态规划的设计思想:
- 核心思想 :把大问题拆成小问题,先解决小问题,再用小问题的答案拼出大问题的答案。本题中,先求出以 n u m s 0 nums0 nums0 结尾的 c u r _ m a x cur\_max cur_max、 c u r _ m i n cur\_min cur_min,再逐个递推出后续位置的状态。
- 与本题的联系 :乘积最大子数组问题天然具有递推结构------每个位置 i i i 的最优解都可以由前一个位置 i − 1 i-1 i−1 的最大值、最小值与 n u m s i numsi numsi 共同得到。因此无需回溯或搜索,只需按顺序滚动更新即可。
- 注意事项 :动态规划的正确性依赖于「最优子结构」与「无后效性」。本题中 c u r _ m a x cur\_max cur_max、 c u r _ m i n cur\_min cur_min 只由前一个位置的状态决定,与未来的状态无关,因此递推顺序合法。
使用要点:
- 状态变量 :
cur_max记录以当前元素结尾的连续子数组的最大乘积,cur_min记录以当前元素结尾的连续子数组的最小乘积(用于捕捉负负得正的可能)。 - 初始化 :
cur_max = cur_min = ans = nums[0],以第一个元素作为递推基准。 - 转移时机 :外层循环遍历 i i i 从 1 1 1 到 n − 1 n-1 n−1,当 n u m s i < 0 numsi < 0 numsi<0 时先交换
cur_max与cur_min,再执行cur_max = max(nums[i], cur_max * nums[i])、cur_min = min(nums[i], cur_min * nums[i])。 - 结果返回 :遍历结束后,返回
ans,表示整个数组中的最大连续子数组乘积。
算法变体与扩展:
- 最大子数组和(LeetCode 53):一维动态规划的经典入门题,只需维护一个最大值状态即可,思路与本题一脉相承。
- 乘积为正数的最长子数组长度(LeetCode 1567):同样需要同时维护正负两个状态,与本题的「双状态」思想高度一致。
- 环形子数组的最大和(LeetCode 918):在最大子数组和的基础上引入环形结构,需要同时考虑「最大子段」与「最小子段」两种情形。
- 最佳买卖股票时机含冷冻期(LeetCode 309):多状态动态规划的经典题目,通过维护多个状态变量来刻画不同阶段的约束。
相关 LeetCode 例题:
- 53. 最大子数组和(一维动态规划 + 单状态)
- 1567. 乘积为正数的最长子数组长度(双状态动态规划)
- 918. 环形子数组的最大和(环形结构 + 最大/最小子段)
- 309. 最佳买卖股票时机含冷冻期(多状态动态规划)

