1.题目链接:
2.题目描述:
给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。
一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。请你返回乘积为正数的最长子数组长度。
示例 1:
输入:nums = [1,-2,-3,4]
输出:4
解释:
数组本身乘积就是正数,值为 24 。
示例 2:
输入:nums = [0,1,-2,-3,-4]
输出:3
解释:
最长乘积为正数的子数组为 [1,-2,-3] ,乘积为 6 。
注意,我们不能把 0 也包括到子数组中,因为这样乘积为 0 ,不是正数。 示例 3:
输入:nums = [-1,-2,-3,0,1]
输出:2
解释:
乘积为正数的最长子数组是 [-1,-2] 或者 [-2,-3] 。
3. 解法(动态规划):
算法思路:
继续效仿「最大子数组和」中的状态表示,尝试解决这个问题。
状态表示:dp[i] 表示「所有以 i 结尾的子数组,乘积为正数的最长子数组的长度」。思考状态转移:对于 i 位置上的 nums[i] ,我们可以分三种情况讨论:
i. | 如果 nums[i] = 0 ,那么所有以 i 为结尾的子数组的乘积都不可能是正数,此时 |
dp[i] = 0 ;
ii. 如果 nums[i] > 0 ,那么直接找到 dp[i - 1] 的值(这里请再读一遍 dp[i - 1] 代表的意义,并且考虑如果 dp[i - 1] 的结值是 0 的话,影不影响结果),然后加 一即可,此时 dp[i] = dp[i - 1] + 1 ;
iii. 如果 nums[i] < 0 ,这时候你该蛋疼了,因为在现有的条件下,你根本没办法得到此时 的最长长度。因为乘法是存在「负负得正」的,单单靠一个 dp[i - 1] ,我们无法推导 出 dp[i] 的值。
但是,如果我们知道「以 i - 1 为结尾的所有子数组,乘积为负数的最长子数组的长度」 neg[i - 1] ,那么此时的 dp[i] 是不是就等于 neg[i - 1] + 1 呢?
通过上面的分析,我们可以得出,需要两个 dp 表,才能推导出最终的结果。不仅需要一个「乘积
为正数的最长子数组」,还需要一个「乘积为负数的最长子数组」。
1. 状态表示:
f[i] 表示:以 i 结尾的所有子数组中,乘积为「正数」的最长子数组的长度;
g[i] 表示:以 i 结尾的所有子数组中,乘积为「负数」的最长子数组的长度。
2. 状态转移方程:
遍历每一个位置的时候,我们要同步更新两个 dp 数组的值。
对于 f[i] ,也就是以 i 为结尾的乘积为「正数」的最长子数组,根据 nums[i] 的值,可以
分为三种情况:
i. | nums[i] = 0 时,所有以 i 为结尾的子数组的乘积都不可能是正数,此时 f[i] = |
0 ;
ii. | nums[i] > 0 时,那么直接找到 f[i - 1] 的值(这里请再读一遍 f[i - 1] 代表 |
的意义,并且考虑如果 f[i - 1] 的结值是 0 的话,影不影响结果),然后加一即可,此时 f[i] = f[i - 1] + 1 ;
iii. | nums[i] < 0 时,此时我们要看 g[i - 1] 的值(这里请再读一遍 g[i - 1] 代 |
表的意义。因为负负得正,如果我们知道以 i - 1 为结尾的乘积为负数的最长子数组的长度,加上 1 即可),根据 g[i - 1] 的值,又要分两种情况:
1. | g[i - 1] = 0 ,说明以 i - 1 为结尾的乘积为负数的最长子数组是不存在的,又 |
因为 nums[i] < 0 ,所以以 i 结尾的乘积为正数的最长子数组也是不存在的,此时 f[i] = 0 ;
2. | g[i - 1] != 0 ,说明以 i - 1 为结尾的乘积为负数的最长子数组是存在的,又 |
因为 nums[i] < 0 ,所以以 i 结尾的乘积为正数的最长子数组就等于 g[i - 1] + 1 ;
综上所述,nums[i] < 0 时,f[i] = g[i - 1] == 0 ? 0 : g[i - 1] +
|
对于 g[i] ,也就是以 i 为结尾的乘积为「负数」的最长子数组,根据 nums[i] 的值,可以
分为三种情况:
i. | nums[i] = 0 时,所有以 i 为结尾的子数组的乘积都不可能是负数,此时 g[i] = |
0 ;
ii. | nums[i] < 0 时,那么直接找到 f[i - 1] 的值(这里请再读一遍 f[i - 1] 代表 |
的意义,并且考虑如果 f[i - 1] 的结值是 0 的话,影不影响结果),然后加一即可(因为正数 * 负数 = 负数),此时 g[i] = f[i - 1] + 1 ;
iii. | nums[i] > 0 时,此时我们要看 g[i - 1] 的值(这里请再读一遍 g[i - 1] 代 |
表的意义。因为正数 * 负数 = 负数),根据 g[i - 1] 的值,又要分两种情况:
1. | g[i - 1] = 0 ,说明以 i - 1 为结尾的乘积为负数的最长子数组是不存在的,又 |
因为 nums[i] > 0 ,所以以 i 结尾的乘积为负数的最长子数组也是不存在的,此时 f[i] = 0 ;
2. | g[i - 1] != 0 ,说明以 i - 1 为结尾的乘积为负数的最长子数组是存在的,又 |
因为 nums[i] > 0 ,所以以 i 结尾的乘积为正数的最长子数组就等于 g[i - 1] + 1 ;
综上所述,nums[i] > 0 时,g[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1 ;
这里的推导比较绕,因为不断的出现「正数和负数」的分情况讨论,我们只需根据下面的规则,严
格找到此状态下需要的 dp 数组即可:
i. | 正数 * 正数 = 正数 |
ii. 负数 * 负数 = 正数
iii. 负数 * 正数 = 正数 * 负数 = 负数
3. 初始化:
可以在最前面加上一个「辅助结点」,帮助我们初始化。使用这种技巧要注意两个点:
i. 辅助结点里面的值要「保证后续填表是正确的」;
ii. 「下标的映射关系」。
在本题中,最前面加上一个格子,并且让 f[0] = g[0] = 0 即可。
4. 填表顺序:
根据「状态转移方程」易得,填表顺序为「从左往右,两个表一起填」。
5. 返回值:
根据「状态表示」,我们要返回 f 表中的最大值。
Java算法代码:
class Solution {
public int getMaxLen(int[] nums) {
// 1.创建dp表
// 2.初始化
// 3.填表
// 4.返回值
int n = nums.length;
int[] f = new int[n+1];
int[] g = new int[n+1];
int ret = Integer.MIN_VALUE;
for(int i = 1; i <= n; i++){
if(nums[i-1] > 0){
f[i] = f[i - 1] + 1;
g[i] = g[i - 1] == 0 ? 0: g[i - 1]+1;
}else if(nums[i - 1] < 0){
f[i] = g[i - 1] ==0 ? 0 : g[i - 1] + 1;
g[i] = f[i - 1] + 1;
}
ret = Math.max(ret,f[i]);
}
return ret;
}
}
执行结果:
动态规划: