【动态规划】LC 152.乘积最大子数组

文章目录

前言

本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。

一、题目

1、原题链接

152.乘积最大子数组

2、题目描述

二、个人思路整理

1、思路分析

核心思路:同时维护最大值与最小值

如果只维护以 n u m s i numsi numsi 结尾的最大子数组乘积,遇到负数时就会丢失潜在的最大值。因此在遍历过程中,必须同时维护两个状态:

  • cur_max:以当前位置结尾的最大连续子数组乘积
  • cur_min:以当前位置结尾的最小连续子数组乘积(绝对值最大的负数)

状态转移

到达第 i i i 个数 n u m s i numsi numsi 时,新的子数组乘积只有三种可能来源:

  1. 自己单开一段: n u m s i numsi numsi
  2. 延续前一个最大值: c u r _ m a x × n u m s i cur\_max \times numsi cur_max×numsi(当前为正数时可能产生最大值)
  3. 延续前一个最小值: 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 例题:

相关推荐
积流成海1 小时前
一致性哈希原理与实现:分布式系统负载均衡
算法·负载均衡·哈希算法·哈希函数·盘子工具站
Nil20811 小时前
leetcode 118杨辉三角
算法·leetcode·职场和发展
wanderist.14 小时前
线性筛法详解:从筛质数到欧拉函数、Möbius 函数与约数函数
java·数据结构·算法
All for pursuit.14 小时前
【设计-1】208.实现Trie (前缀树)
数据结构·c++·算法·leetcode
weixin_3077791314 小时前
C++代码实现MATLAB中的crossval函数功能
开发语言·c++·算法·matlab
青少儿编程课堂14 小时前
背包问题(0/1 背包与完全背包)解题精讲——动态规划入门
c++·python·算法·bfs·信息学竞赛
光影少年15 小时前
为什么 JavaScript 中 0.1 + 0.2 !== 0.3,如何让其相等?
前端·javascript·算法
HEJOO915 小时前
Linux 设备驱动系列——使用全局工作队列
java·linux·算法
Code_Solitude15 小时前
c语言:递归
java·c语言·算法