【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)

参考灵神

相关推荐
浩浩测试一下21 分钟前
06高级语言逻辑结构到汇编语言之逻辑结构转换 for (...; ...; ...)
汇编·数据结构·算法·安全·web安全·网络安全·安全架构
小蒜学长1 小时前
基于实例教学的软件工程专业教学系统
java·spring boot·后端·软件工程
Code_Artist1 小时前
[Java并发编程]3.同步锁的原理
java·后端·面试
渣哥1 小时前
面试必问!JDK动态代理和CGLIB动态代理的核心区别
java
辞--忧1 小时前
K-Means 聚类算法详解与实战指南
算法·kmeans·聚类
尤超宇1 小时前
K 均值聚类(K-Means)演示,通过生成笑脸和爱心两种形状的模拟数据,展示了无监督学习中聚类算法的效果。以下是详细讲解:
算法·均值算法·聚类
天天摸鱼的java工程师1 小时前
如何实现数据实时同步到 ES?八年 Java 开发的实战方案(从业务到代码)
java·后端·面试
掉鱼的猫1 小时前
老码农教你:Solon + EasyExcel 导出工具
java·excel
only-qi1 小时前
Spring Boot 实时广播消息
java·spring boot·后端