「力扣」第 152 题:乘积最大子序列(中等)


「力扣」第 152 题:乘积最大子序列(中等)

给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。

示例 1:

输入: [2, 3, -2, 4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

示例 2:

输入: [-2, 0, -1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

方法:动态规划

思路分析

这道题思考“状态”以及“状态转移方程”的推导思路类似 「力扣」第 53 题:“最大子序和” ,感兴趣的朋友可以先做一下。我感觉这个问题还是有点难的,不过只要把一些特殊的情况考虑清楚,状态转移方程就不是那么难写了。

数组的动态规划问题、子序列、连续子序列的一个通用的状态设置是:

以索引 i 结尾的连续子序列的乘积的最大值。

最后把整个 dp 数组看一遍求最大值即可。因此状态转移方程可能是:

dp[i] = max(dp[i - 1] * nums[i], nums[i])

说明:牢记状态的设置,一定以索引 i 结尾,即乘积数组中 nums[i] 必须被选取。

  • 如果 dp[i - 1] 是负数,乘上 nums[i] 还是负数,倒不如另起炉灶。
  • 如果 nums[i] 是负数该怎么办呢?dp[i - 1] 是正数的时候,越乘越小,dp[i - 1] 是负数的时候,越乘越大,于是我们可能就需要记录一下负数的那个最小数。

遇到这样的问题,其实就在提示我们状态不够用了。因此,需要在原来的一维 dp 后面新增一个状态。

针对这道题,第 2 维状态只需要两个:

  • 0 表示遍历的过程中得到的以 nums[i] 结尾的连续子序列的乘积的最小值;
  • 1 表示遍历的过程中得到的以 nums[i] 结尾的连续子序列的乘积的最大值。

nums[i] = 0 的时候包含在上面二者之中,无需单独讨论。

状态转移方程写在了参考代码 1 中。即使用二维状态数组同时记录乘积的最大值和最小值,本来写了一堆文字的,后来看太长了,好多废话,直接看代码比较清楚一些。

这里就声明一下状态:

  • dp[i][1] 表示:以 nums[i] 结尾的连续子序列的乘积的最大值;
  • dp[i][0] 表示:以 nums[i] 结尾的连续子序列的乘积的最小值。

参考代码 1

public class Solution {


    public int maxProduct(int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return 0;
        }

        // 状态定义:以索引 i 结尾
        int[][] dp = new int[len][2];

        dp[0][0] = nums[0];
        dp[0][1] = nums[0];

        for (int i = 1; i < len; i++) {
            if (nums[i] >= 0) {
                dp[i][1] = Math.max(nums[i], dp[i - 1][1] * nums[i]);
                dp[i][0] = Math.min(nums[i], dp[i - 1][0] * nums[i]);
            } else {
                dp[i][1] = Math.max(nums[i], dp[i - 1][0] * nums[i]);
                dp[i][0] = Math.min(nums[i], dp[i - 1][1] * nums[i]);
            }
        }

        int res = dp[0][1];
        for (int i = 1; i < len; i++) {
            res = Math.max(res, dp[i][1]);
        }
        return res;
    }

}

复杂度分析

  • 时间复杂度:$O(N)$,这里 $N$ 表示数组的长度。
  • 空间复杂度:$O(N)$,使用了两个状态数组,每一个数组的规模是 $N$。

或者设置两个状态数组,这样语义会更清晰一些:

参考代码 2

public class Solution {

    // 参考:力扣第53 题思路

    public int maxProduct(int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return 0;
        }

        // 状态定义:以索引 i 结尾
        // 思考清楚一种特例: [2, -1 ,3],前面乘起来是负数的话,倒不如另起炉灶
        int[] maxDp = new int[len];
        int[] minDp = new int[len];

        maxDp[0] = nums[0];
        minDp[0] = nums[0];

        for (int i = 1; i < len; i++) {
            if (nums[i] >= 0) {
                maxDp[i] = Math.max(nums[i], maxDp[i - 1] * nums[i]);
                minDp[i] = Math.min(nums[i], minDp[i - 1] * nums[i]);
            } else {
                maxDp[i] = Math.max(nums[i], minDp[i - 1] * nums[i]);
                minDp[i] = Math.min(nums[i], maxDp[i - 1] * nums[i]);
            }
        }

        int res = maxDp[0];
        for (int i = 1; i < len; i++) {
            res = Math.max(res, maxDp[i]);
        }
        return res;
    }
}

复杂度分析:(同上)。

因为每一个状态只与前一个状态有关,可以使用「滚动变量」技巧,把状态数组压缩到常数。

参考代码 3

public class Solution {

    public int maxProduct(int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return 0;
        }

        int preMax = nums[0];
        int preMin = nums[0];
        int curMax;
        int curMin;

        int res = nums[0];
        for (int i = 1; i < len; i++) {
            if (nums[i] >= 0) {
                curMax = Math.max(preMax * nums[i], nums[i]);
                curMin = Math.min(preMin * nums[i], nums[i]);
            } else {
                curMax = Math.max(preMin * nums[i], nums[i]);
                curMin = Math.min(preMax * nums[i], nums[i]);
            }
            res = Math.max(res, curMax);

            // 滚动变量
            preMax = curMax;
            preMin = curMin;
        }
        return res;
    }
}

复杂度分析

  • 时间复杂度:$O(N)$,这里 $N$ 表示数组的长度。
  • 空间复杂度:$O(1)$,使用了常数个变量。

文章作者: liweiwei419
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 liweiwei419 !
评论
 上一篇
「力扣」第 188 题:买卖股票的最佳时机 IV 「力扣」第 188 题:买卖股票的最佳时机 IV
「力扣」第 188 题:买卖股票的最佳时机 IV 链接 题解链接 给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。 注意: 你不能同时参与多笔交易
下一篇 
「力扣」第 139 题:单词拆分 「力扣」第 139 题:单词拆分
「力扣」第 139 题:单词拆分 链接 题解链接 给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict*,判定 *s 是否可以被空格拆分为一个或多个在字典中出现的单词。 说明: 拆分时可以重复使用字典中的单词。 你可
  目录