【LeetCode Hot100 刷题日记 (88/100)】152. 乘积最大子数组 ——数组、动态规划(维护最大最小值)🧠

2 阅读5分钟

📌 题目链接:152. 乘积最大子数组 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:数组、动态规划

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)


🧠 题目分析

给定一个整数数组 nums,要求找出乘积最大的非空连续子数组(至少包含一个数字),并返回其乘积。

这道题与经典的「53. 最大子序和」看似相似,但关键区别在于:负数 × 负数 = 正数。这意味着:

  • 当前位置的最优解不一定由前一个位置的“最大值”转移而来;
  • 一个很小的负数(比如 -1000)如果后面遇到另一个负数(比如 -2),反而可能变成很大的正数(2000)。

因此,仅维护“以 i 结尾的最大乘积”是不够的,我们还必须同时维护“以 i 结尾的最小乘积”,因为最小值在遇到负数时可能“逆袭”成最大值。

💡 面试考点
这是一道典型的“状态不唯一”的动态规划题。考察你是否能意识到:极值问题中,负数会反转大小关系,从而需要同时追踪最大值和最小值。这也是高频面试题!


⚙️ 核心算法及代码讲解

✅ 动态规划设计

我们定义两个状态变量(滚动更新,无需数组):

  • maxF:以当前元素结尾的最大乘积
  • minF:以当前元素结尾的最小乘积

对于每个 nums[i],有三种选择:

  1. 单独以 nums[i] 作为子数组;
  2. nums[i] 接到前面最大乘积子数组后面:maxF * nums[i]
  3. nums[i] 接到前面最小乘积子数组后面:minF * nums[i]

由于 nums[i] 可能为正、负或零,我们需要在这三者中取最大值更新 maxF,取最小值更新 minF

📌 关键洞察
nums[i] < 0 时,maxF * nums[i] 会变小,而 minF * nums[i] 可能变大(负负得正)。所以必须同时考虑两者!

🧾 状态转移方程

new_max = max(nums[i], maxF * nums[i], minF * nums[i])
new_min = min(nums[i], maxF * nums[i], minF * nums[i])

然后更新 maxF = new_max, minF = new_min,并用 maxF 更新全局答案。

⚠️ 注意:必须先保存旧的 maxFminF,否则在计算 minF 时会用到已经更新的 maxF,导致错误!

🧪 边界处理

  • 初始值:maxF = minF = ans = nums[0]
  • 数组长度 ≥ 1,无需判空

🧩 解题思路(分步)

  1. 初始化:设 maxF, minF, ans 均为 nums[0]

  2. 从 i = 1 开始遍历数组

    • 保存当前 maxFminF 到临时变量 mx, mn
    • 计算新的 maxF = max(mx * nums[i], mn * nums[i], nums[i])
    • 计算新的 minF = min(mx * nums[i], mn * nums[i], nums[i])
    • 更新全局最大值 ans = max(ans, maxF)
  3. 返回 ans

为什么不用判断溢出?
题目保证结果是 32 位整数,且输入范围小(-10 ≤ nums[i] ≤ 10,n ≤ 2e4),最坏情况 10^20000 显然不可能,实际测试用例不会溢出 long long。但官方题解用了 (long) 强转防中间溢出,我们保留此写法。


📊 算法分析

项目分析
时间复杂度O(n):单次遍历数组
空间复杂度O(1):仅使用常数个变量
是否原地
是否稳定不涉及排序,无稳定性概念
面试频率⭐⭐⭐⭐☆(非常高!常与“最大子序和”对比考察)

💬 面试追问可能

  • 如果数组全是正数?→ 退化为累乘,直接返回总乘积。
  • 如果包含 0?→ 0 会重置状态(因为 max(0, ...) 会让 maxF=0,相当于重新开始)。
  • 能否输出具体子数组?→ 需要额外记录起止位置,稍复杂,但思路类似。

💻 代码

C++

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        // 初始化:第一个元素既是最大值也是最小值,也是当前答案
        long maxF = nums[0], minF = nums[0], ans = nums[0];
        // 从第二个元素开始遍历
        for (int i = 1; i < nums.size(); ++i) {
            // 保存上一轮的 maxF 和 minF,避免被覆盖
            long mx = maxF, mn = minF;
            // 更新当前最大乘积:三种可能取最大
            maxF = max(mx * nums[i], max((long)nums[i], mn * nums[i]));
            // 更新当前最小乘积:三种可能取最小
            minF = min(mn * nums[i], min((long)nums[i], mx * nums[i]));
            // 题解中原有保护语句(实际可省略,因题目保证32位整数)
            if(minF < INT_MIN) {
                minF = nums[i];
            }
            // 更新全局最大乘积
            ans = max(maxF, ans);
        }
        return ans;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    vector<int> nums1 = {2,3,-2,4};
    cout << "Test 1: " << sol.maxProduct(nums1) << " (Expected: 6)\n"; // 输出 6

    vector<int> nums2 = {-2,0,-1};
    cout << "Test 2: " << sol.maxProduct(nums2) << " (Expected: 0)\n"; // 输出 0

    vector<int> nums3 = {-2,3,-4};
    cout << "Test 3: " << sol.maxProduct(nums3) << " (Expected: 24)\n"; // 输出 24

    vector<int> nums4 = {-1,-2,-3};
    cout << "Test 4: " << sol.maxProduct(nums4) << " (Expected: 6)\n"; // 输出 6

    return 0;
}

JavaScript

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxProduct = function(nums) {
    let maxF = nums[0];
    let minF = nums[0];
    let ans = nums[0];

    for (let i = 1; i < nums.length; i++) {
        const mx = maxF;
        const mn = minF;

        maxF = Math.max(mx * nums[i], mn * nums[i], nums[i]);
        minF = Math.min(mx * nums[i], mn * nums[i], nums[i]);

        ans = Math.max(ans, maxF);
    }

    return ans;
};

// 测试
console.log(maxProduct([2,3,-2,4]));   // 6
console.log(maxProduct([-2,0,-1]));    // 0
console.log(maxProduct([-2,3,-4]));    // 24
console.log(maxProduct([-1,-2,-3]));   // 6

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!