前言: 本文将用于记录博主的做题记录

一, 最大子数组和

题目链接 :https://leetcode.cn/problems/maximum-subarray/?envType=study-plan-v2&envId=top-100-liked

image.png

求出数组内最大的子数组和, 由于题目要求子数组必须连续, 所以注意不要把他和子序列问题混为一谈了

对于这类在一段区间的子问题, 我首先想到就使用动态规划来解决.
按照经验来思考, 我们先初始化一个dp表, 假设dp[i] : 以 i位置为结尾的最大的子数组和,
先按照这样的思路来尝试解决问题

数组元素有正有负,这代表我们每次加入这个最终的子数组中的数,都会对和造成影响,如果是正数,则无脑往里进 即可, 但是如果是负数呢?难道就直接舍弃前面已经累积的和吗,万一后面后面是一个更大的数字,完全足以抵消让你目前加入这个负数带来的代价 还有累增.

为了填满dp表, 我们自然要遍历一次数组,当但是对于我们每次遍历到的数组元素nums[i], 我们需要做一个判断, 那就是该不该让nums[i] 加入到子数组中?有两种选择

    1. 加入, 让nums[i] 抱大腿, 去在先前的dp[i-1]: 以nums[i-1] 为结尾的最大子数组和上做累加,那么dp[i] = dp[i-1] + nums[i]
    1. 不加入, 舍弃前面的累加结果,自己作为新的子数组开头单干来, 那么此时dp[i] = nums[i] 即可

对于以上这两种情况, 并不清楚那种情况是最优的, 所以要取max

下面是使用动态规划的代码

代码参考一(动态规划)


``````java
class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        if(n == 1) return nums[0];
        int []dp = new int [n+1];
        //dp[i]:以i位置为结尾的最大的子数组之和
        int ret = Integer.MIN_VALUE;
        for(int i = 1;i <= n;i++){
            dp[i] = Math.max(nums[i-1],dp[i-1] + nums[i-1]);
            ret = Math.max(ret,dp[i]);
        }
        return ret;
    }
}

dp表长度为n+1,为了映射到nums中的元素, 所以要使用i-1来成功映射到nums[]中的元素

不过这里也分享另外一种空间复杂度更低的解法

代码参考二(贪心)

class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        if(n == 1) return nums[0];
        int sum = 0;//初始情况下
        int ret = nums[0];
        for(int i = 0;i < n;i++){
            if(sum > 0){//sum对结果起到增益效果
                sum += nums[i];
            }else{
                sum = nums[i];
            }
            ret = Math.max(sum,ret);
        }
        return ret;
    }
}

总体思想不变,但是核心在于遍历中的两个判断, 这里的sum 表示为前面的子数组累加和,
在遍历数组时,当sum > 0 时,前面的累加结果为正, 则无论如何考虑,我们都会让sum 累加当前元素nums[i] . 这是为什么呢? 如果nums[i] > 0 还好, 万一nums[i] 是一个非常小的负数呢? 这也能说加上num[i] 起到增益吗?注意我在代码中的注释并不是加上nums[i] 会对结果起到增益,而是让nums[i] 加上一个前面的sum正数对结果起到增益,
假设, 前面的计算sum = 3 ,当前nums[i]很小,为 -100,
那就有两种选择

    1. 抱大腿,继承前面的状态, 3 +(-100) = -97
    1. 不继承,自己单干, 作为新的子数组的开头, sum = -100

把两者对比,可以发现sum > 0 时, 还是抱大腿直接累加最优,和nums正负无关
这也是sum <= 0 时. sum = nums[i] 这个代码的含义: nums[i]自己单干!

二, 乘积最大的子数组

题目链接 : https://leetcode.cn/problems/maximum-product-subarray/description/

image.png

又是子数组问题, 我们尝试使用动态规划,先按照经验,初始化一个dp表,
dp[i] : 以i位置为结尾的所有子数组中乘积的最大值,按照这个思路来尝试解题

由于nums[i] 有正有负, 所以上面的方法是不行的, 因为即使前面的状态是最大值,但是乘一个负数或者0就全部作废; 如果前面的状态是一个很小的负数,再乘一个负数就变正了

一个dp表光存储最大值肯定太局限了, 所以我们就引入两个dp表, 一个存子数组乘积最大值,另一个存储子数组乘积最小值

这样一来,就有两种情况了, 对于nums[i] ,先前的一个只存最大值的dp表只需要和它做一次判断即可, 这里则需要和两个dp表中保存的状态来在做一个判断,

代码参考一(动态规划)

class Solution {

    public int maxProduct(int[] nums) {

        int n = nums.length;

        if(n == 1) return nums[0];

        int []dp1 = new int [n+1];//大

        int []dp2 = new int [n+1];//小

        int ret = Integer.MIN_VALUE;

        for(int i = 1;i <= n;i++){

            int x = nums[i-1];

            int y = x * dp1[i-1];//情况1:乘前面保存的状态最大值

            int z = x * dp2[i-1];//情况2:乘前面保存状态的最小值

            dp1[i] = Math.max(x,Math.max(y,z));//MAX维护dp1的最大值

            dp2[i] = Math.min(x,Math.min(y,z));//MIN维护dp2的最小值

            ret = Math.max(dp1[i],ret);//从dp1表中的最大值取舍

        }

        return ret;

    }

}

我们会发现,dp[i] 的更新,只会依赖dp[i-1] 前一个状态的值, 先前位置保存的状态都用不到, 所以我们可以来使用一个滚动变量来做空间优化

代码参考二(空间优化)

class Solution {
    public int maxProduct(int[] nums) {
        int n = nums.length;
        if (n == 1) return nums[0];

        // 用三个变量代替原本的 dp 数组
        int maxVal = nums[0]; // 最大乘积
        int minVal = nums[0]; // 最小乘积
        int ret = nums[0];    // 全局最大值

        for (int i = 1; i < n; i++) {
            int x = nums[i];

            // 三种可能的结果
            int a = x;
            int b = maxVal * x;
            int c = minVal * x;
            maxVal = Math.max(a, Math.max(b, c));
            minVal = Math.min(a, Math.min(b, c));

            // 更新全局最大值
            ret = Math.max(ret, maxVal);
        }

        return ret;
    }
}

以上就是有关子数组问题的解题思路,如有纰漏还请指出~~

更多推荐