leetcode 152乘积最大子数组

一、完整代码(精简注释)

复制代码
class Solution {
public:
    int maxProduct(vector<int>& nums) {
        // 初始化当前最大、最小乘积及全局最大值
        long long maxfn = nums[0];
        long long minfn = nums[0];
        long long ans = nums[0];

        for (int i = 1; i < nums.size(); i++) {
            // 保存上一轮的最大、最小乘积
            long long mx = maxfn, mn = minfn;

            // 三种情况取最大乘积
            maxfn = max((long long)nums[i],
                        max(mx * nums[i], mn * nums[i]));

            // 三种情况取最小乘积
            minfn = min((long long)nums[i],
                        min(mx * nums[i], mn * nums[i]));

            // 更新全局最大乘积
            ans = max(maxfn, ans);
        }

        return (int)ans;
    }
};

二、动态规划五步总结

1. 确定 DP 状态

这道题使用动态规划的空间优化形式,只需要三个变量:

  • maxfn:以当前元素结尾的连续子数组的最大乘积。

  • minfn:以当前元素结尾的连续子数组的最小乘积。

  • ans:遍历过程中出现的全局最大乘积。

为什么需要维护最小乘积?

因为负数乘负数可能得到最大的正数,所以之前的最小乘积也可能产生当前的最大乘积。

2. 确定递推公式

对于每个 nums[i],考虑三种情况:

  1. 当前元素自己作为一个子数组:nums[i]

  2. 前一个最大乘积乘以当前元素:mx * nums[i]

  3. 前一个最小乘积乘以当前元素:mn * nums[i]

最大乘积:

复制代码
maxfn = max((long long)nums[i],
            max(mx * nums[i], mn * nums[i]));

最小乘积:

复制代码
minfn = min((long long)nums[i],
            min(mx * nums[i], mn * nums[i]));

注意: mx 和 mn 保存的是上一轮的状态,避免更新 maxfn 后影响 minfn 的计算。

3. 初始化

复制代码
long long maxfn = nums[0];
long long minfn = nums[0];
long long ans = nums[0];

只有第一个元素时,最大乘积、最小乘积和全局最大乘积都等于 nums[0]。

4. 确定遍历顺序

复制代码
for(int i = 1; i < nums.size(); i++)

从左往右遍历,因为当前位置的最大、最小乘积依赖前一个位置的状态。

5. 举例推导

假设:

复制代码
nums = [-2, 3, -4]
i numsi maxfn minfn ans
0 -2 -2 -2 -2
1 3 3 -6 3
2 -4 24 -12 24

当 i = 2 时:

复制代码
mx = 3;
mn = -6;
nums[i] = -4;

计算:

复制代码
maxfn = max(-4, max(-12, 24)); // 24
minfn = min(-4, min(-12, 24)); // -12
ans = max(24, 3);              // 24

最终返回 24。

三、复杂度分析

  • 时间复杂度: \(O(n)\),只需要遍历一次数组。

  • 空间复杂度: \(O(1)\),只使用固定数量的变量,不需要额外的 DP 数组。

四、核心易错点

  1. 为什么需要同时维护最大值和最小值?

负数会改变乘积的大小关系,之前的最小乘积乘以负数可能成为最大乘积。

  1. 为什么需要 mx 和 mn?

因为更新当前最大、最小乘积时,都必须使用上一轮的状态,不能使用已经更新的 maxfn。

  1. 为什么 ans 不能直接等于 maxfn?

因为最大乘积子数组不一定以最后一个元素结尾,所以需要记录遍历过程中的最大值:

复制代码
ans = max(ans, maxfn);
  1. 为什么使用 long long?

为了扩大中间乘积的表示范围,降低整数溢出的风险。但 long long 也有范围限制,并不能保证任意输入都不溢出。

最终记忆:

这道题的核心就是 维护两个状态(最大乘积、最小乘积),每次从三种情况中更新当前状态,再通过 ans 保存全局最大值。

普通 DP 需要 \(O(n)\) 空间,而这里每次只依赖上一轮状态,因此可以优化到 \(O(1)\) 空间。

相关推荐
夏幻灵12 小时前
LeetCode 21:合并两个有序链表——双指针与哑节点详解
算法·leetcode·链表
Navigator_Z14 小时前
LeetCode //C - 1286. Iterator for Combination
c语言·算法·leetcode
圣保罗的大教堂15 小时前
leetcode 301. 删除无效的括号 困难
leetcode
夏幻灵17 小时前
每日一题:LeetCode 206:反转链表——三指针与原地反转详解
算法·leetcode·链表
Tisfy20 小时前
LeetCode 0301.删除无效的括号:二进制枚举 / 回溯
算法·leetcode·字符串·题解·回溯·括号匹配·二进制枚举
Nil20820 小时前
leetcode 322零钱兑换
算法·leetcode·职场和发展
yi01121 小时前
DAY24: LeetCode 167:两数之和 II|从暴力枚举到左右双指针
笔记·python·算法·leetcode·二分查找·双指针
木井巳21 小时前
【BFS 解决拓扑排序】课程表
java·算法·leetcode·广度优先·宽度优先·推荐算法·图搜索算法
yi01121 小时前
LeetCode 15:三数之和|排序 + 双指针,如何避免重复答案?
笔记·python·算法·leetcode·双指针