zoukankan      html  css  js  c++  java
  • 乘积最大子数组 java语言实现 c++语言实现

    乘积最大子数组

    给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

    示例 1:

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

    示例 2:

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

    思路
    这个问题很像「力扣」第 53 题:最大子序和,只不过当前这个问题求的是乘积的最大值;
    「连续」这个概念很重要,可以参考第 53 题的状态设计,将状态设计为:以 nums[i]结尾的连续子数组的最大值;
    类似状态设计的问题还有「力扣」第 300 题:最长上升子序列,「子数组」、「子序列」问题的状态设计的特点是:以 nums[i] 结尾,这是一个经验,可以简化讨论。
    提示:以 nums[i] 结尾这件事情很重要,贯穿整个解题过程始终,请大家留意。

    分析与第 53 题的差异
    求乘积的最大值,示例中负数的出现,告诉我们这题和 53 题不一样了,一个正数乘以负数就变成负数,即:最大值乘以负数就变成了最小值;
    因此:最大值和最小值是相互转换的,这一点提示我们可以把这种转换关系设计到「状态转移方程」里去;
    如何解决这个问题呢?这里常见的技巧是在「状态设计」的时候,在原始的状态设计后面多加一个维度,减少分类讨论,降低解决问题的难度。
    这里是百度百科的「无后效性」词条的解释:

    无后效性是指如果在某个阶段上过程的状态已知,则从此阶段以后过程的发展变化仅与此阶段的状态有关,而与过程在此阶段以前的阶段所经历过的状态无关。利用动态规划方法求解多阶段决策过程问题,过程的状态必须具备无后效性。

    再翻译一下就是:「动态规划」通常不关心过程,只关心「阶段结果」,这个「阶段结果」就是我们设计的「状态」。什么算法关心过程呢?「回溯算法」,「回溯算法」需要记录过程,复杂度通常较高。

    而将状态定义得更具体,通常来说对于一个问题的解决是满足「无后效性」的。这一点的叙述很理论化,不熟悉朋友可以通过多做相关的问题来理解「无后效性」这个概念。

    第 1 步:状态设计(特别重要)
    dp[i][j]:以 nums[i] 结尾的连续子数组的最值,计算最大值还是最小值由 j 来表示,j 就两个值;
    当 j = 0 的时候,表示计算的是最小值;
    当 j = 1 的时候,表示计算的是最大值。
    这样一来,状态转移方程就容易写出。

    第 2 步:推导状态转移方程(特别重要)
    由于状态的设计 nums[i] 必须被选取(请大家体会这一点,这一点恰恰好也是使得子数组、子序列问题更加简单的原因:当情况复杂、分类讨论比较多的时候,需要固定一些量,以简化计算);

    nums[i] 的正负和之前的状态值(正负)就产生了联系,由此关系写出状态转移方程:

    当 nums[i] > 0 时,由于是乘积关系:
    最大值乘以正数依然是最大值;
    最小值乘以同一个正数依然是最小值;
    当 nums[i] < 0 时,依然是由于乘积关系:
    最大值乘以负数变成了最小值;
    最小值乘以同一个负数变成最大值;
    当 nums[i] = 0 的时候,由于 nums[i] 必须被选取,最大值和最小值都变成 00 ,合并到上面任意一种情况均成立。
    但是,还要注意一点,之前状态值的正负也要考虑:例如,在考虑最大值的时候,当 nums[i] > 0 是,如果 dp[i - 1][1] < 0 (之前的状态最大值) ,此时 nums[i] 可以另起炉灶(这里依然是第 53 题的思想),此时 dp[i][1] = nums[i] ,合起来写就是:

    dp[i][1] = max(nums[i], nums[i] * dp[i - 1][1]) if nums[i] >= 0
    其它三种情况可以类似写出,状态转移方程如下:

    dp[i][0] = min(nums[i], nums[i] * dp[i - 1][0]) if nums[i] >= 0
    dp[i][1] = max(nums[i], nums[i] * dp[i - 1][1]) if nums[i] >= 0

    dp[i][0] = min(nums[i], nums[i] * dp[i - 1][1]) if nums[i] < 0
    dp[i][1] = max(nums[i], nums[i] * dp[i - 1][0]) if nums[i] < 0
    第 3 步:考虑初始化
    由于 nums[i] 必须被选取,那么 dp[i][0] = nums[0],dp[i][1] = nums[0]。

    第 4 步:考虑输出
    题目问连续子数组的乘积最大值,这些值需要遍历 dp[i][1] 获得。

    参考代码 1:

    Java
    public class Solution {

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

    // dp[i][0]:以 nums[i] 结尾的连续子数组的最小值
    // dp[i][1]:以 nums[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][0] = Math.min(nums[i], nums[i] * dp[i - 1][0]);
    dp[i][1] = Math.max(nums[i], nums[i] * dp[i - 1][1]);
    } else {
    dp[i][0] = Math.min(nums[i], nums[i] * dp[i - 1][1]);
    dp[i][1] = Math.max(nums[i], nums[i] * dp[i - 1][0]);
    }
    }

    // 只关心最大值,需要遍历
    int res = dp[0][1];
    for (int i = 1; i < len; i++) {
    res = Math.max(res, dp[i][1]);
    }
    return res;
    }
    }
    复杂度分析:

    时间复杂度:O(N)O(N),这里 NN 是数组的长度,遍历 2 次数组;
    空间复杂度:O(N)O(N),状态数组的长度为 2N2N。
    问题做到这个地方,其实就可以了。下面介绍一些非必要但进阶的知识。

    第 5 步:考虑表格复用
    动态规划问题,基于「自底向上」、「空间换时间」的思想,通常是「填表格」,本题也不例外;
    由于通常只关心最后一个状态值,或者在状态转移的时候,当前值只参考了上一行的值,因此在填表的过程中,表格可以复用,常用的技巧有:
    1、滚动数组(当前行只参考了上一行的时候,可以只用 2 行表格完成全部的计算);
    2、滚动变量(斐波拉契数列问题)。
    掌握非常重要的「表格复用」技巧,来自「0-1 背包问题」(弄清楚为什么要倒序填表)和「完全背包问题」(弄清楚为什么可以正向填表);
    「表格复用」的合理性,只由「状态转移方程」决定,即当前状态值只参考了哪些部分的值。
    参考代码 2:

    Java
    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)O(N),这里 NN 是数组的长度,最值也在一次遍历的过程中计算了出来;
    空间复杂度:O(1)O(1),只使用了常数变量。
    这里说一点题外话:除了基础的「0-1」背包问题和「完全背包」问题,需要掌握「表格复用」的技巧以外。在绝大多数情况下,在「力扣」上做的「动态规划」问题都可以不考虑「表格复用」。

    做题通常可以不先考虑优化空间(个人观点,仅供参考),理由如下:

    空间通常来说是用户不敏感的,并且在绝大多数情况下,空间成本低,我们写程序通常需要优先考虑时间复杂度最优;
    时间复杂度和空间复杂度通常来说不可能同时最优,所以我们经常看到的是优化解法思路都是「空间换时间」,这一点几乎贯穿了基础算法领域的绝大多数的算法设计思想;
    限制空间的思路,通常来说比较难,一般是在优化的过程中才考虑优化空间,在一些限制答题时间的场景下(例如面试),先写出一版正确的代码是更重要的,并且不优化空间的代码一般来说,可读性和可解释性更强。
    以上个人建议,仅供参考。

    总结
    动态规划问题通常用于计算多阶段决策问题的最优解。

    多阶段,是指解决一个问题有多个步骤;
    最优解,是指「最优子结构」。
    动态规划有三个概念很重要:

    重复子问题:因为重复计算,所以需要「空间换时间」,记录子问题的最优解;
    最优子结构:规模较大的问题的最优解,由各个子问题的最优解得到;
    无后效性(上面已经解释)。
    动态规划有两个特别关键的步骤:

    设计状态:
    有些题目问啥,就设计成什么;
    如果不行,只要有利于状态转移,很多时候,就可以设计成状态;
    根据过往经验;
    还有一部分问题是需要在思考的过程中调整的,例如本题。
    推导状态转移方程:通常是由问题本身决定的。
    动态规划问题思考的两个方向:

    自顶向下:即「递归 + 记忆化」,入门的时候优先考虑这样做;
    自底向上:即「递归」,从一个最小的问题开始,逐步得到最终规模问题的解。后面问题见得多了,优先考虑这样做,绝大部分动态规划问题可以「自底向上」通过递推得到。

    https://www.jianshu.com/p/82a14ad6176e

  • 相关阅读:
    3星|《全球电商进化史》:全球电商亲历记
    2星|陈春花《共生》:逻辑差语文差缺证据。不敢相信知名商学院教授的书居然这么差
    3星|《第五次开始》:考古学家写的人类简史与未来简史
    4星|《财经》2018年第21期:互联网处方能解决药品质量和价格问题
    2.5星|托夫勒《权力的转移》;30年旧书,现在看理论有点牵强肤浅,预测有的准有的不准
    2018左其盛好书榜(截至9月15日)
    沟通交流技巧相关的11本书点评
    没睡好觉的上级更容易辱骂下属:3.5星|《哈佛商业评论》第9期
    3星|《利润模式》:20年旧书,30种模式
    在 C# 中通过 P/Invoke 调用Win32 DLL
  • 原文地址:https://www.cnblogs.com/readlearn/p/12913041.html
Copyright © 2011-2022 走看看