1. 和积 最值问题
53. 最大子数组和第
i
位置的答案,一定由第i-1
位置的答案发展而来,不用考虑i-2
,i-3
...因此用普通dp,可以做到O(n)。
- 普通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即可,没有人说怎么想到这个思路。这个思路有没有一个通用的方法能让我们想出来呢?其实也是有的。
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,仅用于个人学习,侵权删除
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)