【LeetCode 热题 100】152. 乘积最大子数组——(解法一)递推

Problem: 152. 乘积最大子数组

文章目录

整体思路

这段代码旨在解决经典的 "乘积最大子数组" (Maximum Product Subarray) 问题。问题要求在一个包含正数、负数和零的整数数组中,找到一个连续子数组,使得该子数组内所有元素的乘积最大,并返回这个最大乘积。

与"最大子数组和"不同,乘积问题因为负数的存在而变得复杂:一个当前很小的负数(最小值)乘以另一个负数,可能会变成一个很大的正数(最大值)。因此,只维护最大值是不够的。

该算法采用了一种非常巧妙的 动态规划 方法。它在每一步都同时维护以当前元素结尾的最大乘积最小乘积

  1. 状态定义

    • 算法定义了两个DP数组:
      • dpMax[i]nums[i] 为结尾的连续子数组的最大乘积
      • dpMin[i]nums[i] 为结尾的连续子数组的最小乘积(这个主要是为了处理负数)。
  2. 状态转移方程

    • 为了计算 dpMax[i]dpMin[i],我们需要考虑 nums[i] 与前一个状态的关系。以 nums[i] 结尾的子数组,要么只包含 nums[i] 本身,要么是 nums[i] 连接在以 nums[i-1] 结尾的子数组后面。
    • nums[i] 与前面的子数组(dpMax[i-1]dpMin[i-1])相乘时,会出现以下情况:
      • 如果 nums[i] 是正数:dpMax[i-1] * nums[i] 可能是新的最大值,dpMin[i-1] * nums[i] 可能是新的最小值。
      • 如果 nums[i] 是负数:dpMin[i-1] * nums[i](负负得正)可能变成新的最大值,而 dpMax[i-1] * nums[i](正负得负)可能变成新的最小值。
    • 因此,dpMax[i] 的候选值有三个:
      1. dpMax[i-1] * nums[i]:前一个最大值乘以当前数。
      2. dpMin[i-1] * nums[i]:前一个最小值乘以当前数(处理负负得正)。
      3. nums[i]:不与前面连接,子数组只包含当前数自身。
        dpMax[i] 就是这三者中的最大值。
    • 同理,dpMin[i] 的候选值也是这三个,但取的是最小值。
    • 状态转移方程:
      dpMax[i] = max(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i])
      dpMin[i] = min(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i])
  3. 最终结果

    • dpMax[i] 仅代表以 nums[i] 结尾的最大乘积,不一定是全局的最大乘积。
    • 全局的最大乘积必然是所有 dpMax[i] 中的某一个。
    • 因此,在计算完整个 dpMax 数组后,需要遍历它来找到其中的最大值作为最终答案。

完整代码

java 复制代码
import java.util.Arrays;

class Solution {
    /**
     * 找到一个具有最大乘积的连续子数组,并返回其乘积。
     * @param nums 整数数组
     * @return 最大乘积
     */
    public int maxProduct(int[] nums) {
        int n = nums.length;
        // dpMax[i]: 以 nums[i] 结尾的连续子数组的最大乘积。
        int[] dpMax = new int[n];
        // dpMin[i]: 以 nums[i] 结尾的连续子数组的最小乘积。
        int[] dpMin = new int[n];
        
        // 基础情况:以 nums[0] 结尾的子数组只有一个,其最大和最小乘积都是 nums[0]。
        dpMax[0] = dpMin[0] = nums[0];
        
        // 从第二个元素开始,应用状态转移方程
        for (int i = 1; i < n; i++) {
            int x = nums[i];
            
            // 计算 dpMax[i]:
            // 它的候选值有三个:
            // 1. dpMax[i-1] * x: 前一个最大值乘以当前数。
            // 2. dpMin[i-1] * x: 前一个最小值乘以当前数 (处理负负得正的情况)。
            // 3. x:             子数组只包含当前数自身。
            dpMax[i] = Math.max(Math.max(dpMax[i - 1] * x, dpMin[i - 1] * x), x);
            
            // 计算 dpMin[i],逻辑同上,只是取最小值。
            dpMin[i] = Math.min(Math.min(dpMax[i - 1] * x, dpMin[i - 1] * x), x);
        }
        
        // 全局最大乘积是所有 dpMax[i] 中的最大值。
        // 使用 stream API 来方便地找到数组中的最大值。
        return Arrays.stream(dpMax).max().getAsInt();
    }
}

时空复杂度

时间复杂度:O(N)

  1. 循环 :算法的主体是一个 for 循环,从 i=1 遍历到 n-1。算上初始化,整个 nums 数组被访问了一次。循环执行了 N-1 次。
  2. 循环内部操作
    • 在循环的每一次迭代中,执行的都是基本的乘法、Math.max/Math.min 和数组访问操作。这些操作的时间复杂度都是 O(1)
  3. 结果查找Arrays.stream(dpMax).max().getAsInt() 需要遍历整个 dpMax 数组一次来找到最大值。这部分的时间复杂度是 O(N)

综合分析

算法的总时间复杂度由两个独立的线性扫描组成:O(N) (填充DP数组) + O(N) (查找最大值)。因此,最终的时间复杂度是 O(N)

空间复杂度:O(N)

  1. 主要存储开销 :算法创建了两个名为 dpMaxdpMin 的整型数组来存储动态规划的所有中间状态。
  2. 空间大小 :每个数组的长度都与输入数组 nums 的长度 N 相同。因此,总的空间占用为 O(N) + O(N) = O(N)。

综合分析

算法所需的额外空间主要由 dpMaxdpMin 两个数组决定。因此,其空间复杂度为 O(N)

参考灵神

相关推荐
JIngJaneIL14 分钟前
篮球论坛|基于SprinBoot+vue的篮球论坛系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·篮球论坛系统
毕设源码-赖学姐22 分钟前
【开题答辩全过程】以 安卓的服装销售APP为例,包含答辩的问题和答案
java·eclipse·tomcat
青云交1 小时前
Java 大视界 -- Java 大数据在智能农业温室环境调控与作物生长模型构建中的应用
java·机器学习·传感器技术·数据处理·作物生长模型·智能农业·温室环境调控
曾经的三心草1 小时前
SpringAI5-智能聊天机器⼈
java·springai
微露清风2 小时前
系统性学习C++-第八讲-vector类
java·c++·学习
ゞ 正在缓冲99%…2 小时前
leetcode2826.将三个组排序
算法·leetcode·动态规划
roykingw2 小时前
【思想比实现更重要】高并发场景下如何保证接口幂等性
java·web安全·面试
尘觉2 小时前
面试-浅复制和深复制?怎样实现深复制详细解答
javascript·面试·职场和发展
qq_401700412 小时前
matlab学习
学习·算法·matlab
864记忆3 小时前
Spring Boot 项目标准目录结构
java