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

参考灵神

相关推荐
没有bug.的程序员几秒前
Redis 大 Key 与热 Key:生产环境的风险与解决方案
java·数据库·redis·缓存·热key·大key
We....7 分钟前
Java分布式编程:RMI机制
java·开发语言·分布式
玉衡子10 分钟前
七、InnoDB底层原理与日志机制
java·mysql
€81115 分钟前
Java入门级教程17——利用Java SPI机制制作验证码、利用Java RMI机制实现分布式登录验证系统
java·开发语言·java spi机制·远程传输数据
2301_8153577021 分钟前
parameterType和@Param注解的区别
java·开发语言·数据库
七牛云行业应用24 分钟前
深度解析强化学习(RL):原理、算法与金融应用
人工智能·算法·金融
和编程干到底27 分钟前
数据结构 栈和队列、树
数据结构·算法
纪元A梦28 分钟前
贪心算法在GNN邻域采样问题中的深度解析
算法·贪心算法
宇钶宇夕30 分钟前
西门子 S7-200 SMART PLC 核心指令详解:从移位、上升沿和比较指令到流水灯控制程序实战
运维·算法·自动化
We....40 分钟前
Java 分布式缓存实现:结合 RMI 与本地文件缓存
java·分布式·缓存