常见子数组问题 通用解法

常见子数组问题 通用解法,第1张


1. 和积 最值问题

i位置的答案,一定由第i-1位置的答案发展而来,不用考虑i-2i-3...

因此用普通dp,可以做到O(n)。

53. 最大子数组和
  • 普通DP。因为它向前扩展必连上dp[i-1],考虑dp[i-2]的话就不是连续子数组了。
  • 有一个更优解法判断 sum 是否大于0的,其实就是化简后的普通DP(贪心算法)。

53. 最大子数组和

public class Solution {
    public int maxSubArray(int[] nums) {
        int len = nums.length;
        // dp[i] 表示:以 nums[i] 结尾的连续子数组的最大和
        int[] dp = new int[len];
        dp[0] = nums[0];
        int res=  dp[0];
        for (int i = 1; i < len; i++) {
            if (dp[i - 1] > 0) {
                dp[i] = dp[i - 1] + nums[i];
            } else {
                dp[i] = nums[i];
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}
 152. 乘积最大子数组
  • 可普通DP,同上。只不过是状态机DP。

152. 乘积最大子数组

主要是考虑负数的情况,因此每个位置应当记录两种状态,dp[i][1]记录当前位置的最大值,dp[i][0]记录当前位置的最小值。

方法一:DP二维数组,存储当前最大值和最小值

时间O(n)  空间O(n)

public class Solution {
    public int maxProduct(int[] nums) {
        int len = nums.length;
        if (len == 1) {
            return nums[0];
        }
        // 状态定义:以索引 i 结尾
        int[][] dp = new int[len][2];
        dp[0][0] = nums[0];
        dp[0][1] = nums[0];
        int res = 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]);
            }
            res = Math.max(res, dp[i][1]);
        }
        return res;
    }
}

方法二、DP空间优化版本

nums[i] < 0的时候交换上一轮 imax 和 imin 

时间O(n)  空间O(1)

class Solution {
    public int maxProduct(int[] nums) {
        int imax = nums[0], imin = nums[0], ans = nums[0];
        for(int i = 1; i < nums.length; i++){
            if(nums[i] < 0){  //nums[i] < 0 时,需交换最值
              int tmp = imax;
              imax = imin;
              imin = tmp;
            }
            imax = Math.max(imax*nums[i], nums[i]);
            imin = Math.min(imin*nums[i], nums[i]);
            ans = Math.max(ans, imax);
        }
        return ans;
    }
}

2. 和积 定值问题 2.1 同向滑窗

双指针,以右边界为循环重心,每次+1;不满足条件时左边界收缩。
要求:右指针右移 和 左指针右移 效果相反。这样收缩左边界才可以抵消扩张右边界带来的作用。

剑指 Offer II 008. 和大于等于 target 的最短子数组

实用建议:推荐 建立窗口 和 滑动窗口 写在一个循环中。不推荐滑窗先写个循环建立窗口,再写个循环向右滑动。建立窗口循环末尾容易漏掉这种情况:窗口盛满序列时,还需要移动窗口左边缘以达到最优解。

2.2 前缀和 + 哈希

前缀和数组,把子数组和问题转化为两数之差(类似 两数之和)问题。用哈希就欧了。

560. 和为 K 的子数组

  • 这个题很多人直接滑窗然后错了。如果数据中没有负数,才能用滑窗。因为全正数的话,右指针右移 和 左指针右移 效果相反:前者使sum增加,后者使sum减小。

0 和 1 个数相同的子数组

为什么不能使用滑窗:

左指针右移 和 右指针右移,并不能保证效果相反。右指针右移,可能导致增加一个0;此时收缩窗口,右移左指针,却不能保证减少一个0或增加一个1。
题解大多数上来就说把0换成-1即可,没有人说怎么想到这个思路。这个思路有没有一个通用的方法能让我们想出来呢?其实也是有的。

2.3 一些思考

DP问题有两种很典型的问题:子数组和子序列。

求子数组时,一般是O(n),因为我们求dp[i]时,只用考虑dp[i-1]。再往前就没法连续了。
求子序列时,一般是O(n^2),我们需要考虑所有dp[j] (j

为什么上面提到了 “同向滑窗” 和 “前缀和 + 哈希”,有办法把O(n^2)降低到O(n)?(前提:题目还是求子数组。。如果是子序列,爱莫能助)

前缀和:

用O(1)综合了前N个数的特征(有状态压缩的思想)。
虽然只是求和,我们不能像真正状态压缩一样保留前n个数的全部数据,相当于只保留了“和”这一部分特征。
但当问题与我们所保留的特征相符时,即可完美从O(N)转化为O(1)。那么,什么类型的问题能这么做呢?那就看上文即可明白,什么时候用同向滑窗,什么时候用前缀和 + 哈希。 这也是本文的目的:追溯思路的起因。怎么做重要,怎么想到这么做更重要。
同向滑窗:

假设右边界为遍历的重心,每次扩大右边界时,检查条件,如果不符,左边界收缩。
右边界不同时,左边界是连续收缩的。这就是滑动窗口从O(N)转化为O(1)的核心。
同理,一些题目中用到反向滑窗(如快速排序,反向双指针比同向更容易理解,且不容易错)。能用反向滑窗的前提条件是:左指针右移 和 右指针左移 效果相反。并且反向滑窗能把O(N)降低为O(1)的核心也是:假设左边界看作循环的重心,那么右边界收缩是连续的,并不用每次都从nums.length-1出发向左一一检查。

学习资料来源于leetcode作者Yilin6,仅用于个人学习,侵权删除

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1295799.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-10
下一篇 2022-06-10

发表评论

登录后才能评论

评论列表(0条)

保存