目录
[1. 乘积为正数的最长子数组长度](#1. 乘积为正数的最长子数组长度)
[1.1 题目解析](#1.1 题目解析)
[1.2 解法](#1.2 解法)
[1.3 代码实现](#1.3 代码实现)
1. 乘积为正数的最长子数组长度
https://leetcode.cn/problems/maximum-length-of-subarray-with-positive-product/description/
给你一个整数数组 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] 。
提示:
1 <= nums.length <= 10^5-10^9 <= nums[i] <= 10^9
1.1 题目解析
题目本质
这是一道动态规划问题,核心是求「乘积为正数的最长连续子数组长度」。本质上是在处理符号变化:正数、负数、零对乘积符号的影响。
常规解法
最直观的想法是枚举所有子数组,计算每个子数组的乘积,判断是否为正,记录最长长度。代码如下:
java
for (int i = 0; i < n; i++) {
long product = 1;
for (int j = i; j < n; j++) {
product *= nums[j];
if (product > 0) {
maxLen = Math.max(maxLen, j - i + 1);
}
}
}
问题分析
这种暴力枚举的时间复杂度是 O(n²),对于 n ≤ 10⁵ 的数据规模会超时。更致命的是,我们并不关心乘积的具体值,只关心乘积的符号(正/负/零),每次都去计算乘积是一种浪费。
思路转折
关键观察:乘积的符号只取决于负数的个数
- 负数个数为偶数 → 乘积为正
- 负数个数为奇数 → 乘积为负
- 遇到 0 → 乘积为 0,必须重新开始
要想高效 → 必须避免重复计算 → 动态规划登场。我们可以用两个状态:
- f[i]:以 nums[i] 结尾的乘积为正的最长子数组长度
- g[i]:以 nums[i] 结尾的乘积为负的最长子数组长度
要想高效 → 利用前面的计算结果 → 动态规划:维护两个状态来记录"以当前位置结尾的正数链"和"负数链"的长度,O(1) 完成状态转移,总复杂度降为 O(n)。
1.2 解法
算法思想
定义状态:
- f[i] = 以第 i 个元素结尾的乘积为正的最长子数组长度
- g[i] = 以第 i 个元素结尾的乘积为负的最长子数组长度
状态转移方程:
java
if(nums[i-1] > 0){
if(g[i-1] == 0){
g[i] = 0;
f[i] = Math.max(1, f[i-1]+1);
}else{
f[i] = Math.max(1, f[i-1]+1);
g[i] = Math.max(0, g[i-1]+1);
}
}else if(nums[i-1] < 0){
if(g[i-1] == 0){
f[i] = 0;
g[i] = Math.max(1, f[i-1]+1);
}else{
f[i] = Math.max(0, g[i-1]+1);
g[i] = Math.max(1, f[i-1]+1);
}
}else{
f[i] = g[i] = 0;
}
**i)初始化:**创建两个 DP 数组 f 和 g,长度为 m+1(m 为数组长度),初始值 f[0] = 0, g[0] = 0,表示空数组状态
**ii)遍历数组:**从 i = 1 到 m,使用 nums[i-1] 访问当前元素,根据元素符号进行状态转移
iii)分类讨论:
-
遇到正数:
-
f[i] 存正数连续最长值,如果前面有个乘积是正数的子数组(f[i-1]),那乘上当前这个正数,乘积还是正数,长度+1
-
g[i] 存负数连续最长值,如果前面有个乘积是负数的子数组(g[i-1]),那乘上当前这个正数,乘积还是负数,长度+1
-
如果前一个数是0,g[i] <0 && g[i-1] == 0,那么相乘之后 g[i] 一定为0,所以此时
g[i] = 0
-
-
简单说就是:正数不改变符号,只让长度增加
-
-
遇到负数:
-
f[i] 存正数连续最长值,如果前面有个乘积是负数的子数组(g[i-1]),那乘上当前这个负数,负负得正,乘积变正数了
-
如果前一个数是0,f[i] <0 && g[i-1] == 0,那么相乘之后 g[i] 一定为0,所以此时
f[i] = 0
-
-
g[i] 存负数连续最长值,如果前面有个乘积是正数的子数组(f[i-1]),那加上当前这个负数,正负得负,乘积变负数了
-
简单说就是:负数会反转符号(正变负,负变正)
-
-
遇到零:
- 乘积直接变成 0,之前的计算全作废,从下一个数重新开始
**iv)更新答案:**每次状态转移后,用 f[i] 更新全局最大值 ret
易错点
- **初始化错误:**f[0] 和 g[0] 不能初始化为负无穷,应该是 0(表示空数组状态)
- **Math.max 滥用:**不能写 Math.max(1, f[i-1]+1),这会导致即使 f[i-1] = 0 也强制返回 1,破坏了 DP 逻辑。正确做法是先判断 f[i-1] 是否为 0
- **负数链的判断:**当前元素为负数时,只有前面存在正数链(f[i-1] > 0)才能形成新的负数链,否则只能自己单独成链(长度为 1)
- **零的处理:**遇到 0 必须将 f[i] 和 g[i] 都置为 0,因为乘积为 0 不是正数,必须从下一个元素重新开始
1.3 代码实现
java
class Solution {
public int getMaxLen(int[] nums) {
int m = nums.length;
int[] f = new int[m + 1]; // f[i]: 以 nums[i-1] 结尾的乘积为正的最长长度
int[] g = new int[m + 1]; // g[i]: 以 nums[i-1] 结尾的乘积为负的最长长度
// 初始化:空数组时两条链长度都是 0
f[0] = 0;
g[0] = 0;
int ret = 0;
for (int i = 1; i <= m; i++) {
if (nums[i - 1] > 0) {
// 当前元素为正数
// f[i] 存储正数链:正数自己可以延长正数链
f[i] = f[i - 1] + 1;
// g[i] 存储负数链:正数可以延长负数链,但前提是之前存在负数链
if (g[i - 1] > 0) {
g[i] = g[i - 1] + 1;
} else {
// 之前不存在负数链,当前正数无法单独形成负数链
g[i] = 0;
}
} else if (nums[i - 1] < 0) {
// 当前元素为负数
// f[i] 存储正数链:负数可以把之前的负数链变成正数链(负负得正)
if (g[i - 1] > 0) {
f[i] = g[i - 1] + 1;
} else {
// 之前不存在负数链,当前负数无法形成正数链
f[i] = 0;
}
// g[i] 存储负数链:负数可以把之前的正数链变成负数链,或自己单独成链
g[i] = f[i - 1] + 1;
} else {
// 当前元素为 0,乘积为 0 不是正数,两条链都断掉
f[i] = 0;
g[i] = 0;
}
// 更新全局最大值
ret = Math.max(ret, f[i]);
}
return ret;
}
}
复杂度分析
- **时间复杂度:O(n),**只需遍历数组一次,每次状态转移 O(1)
- **空间复杂度:O(n),**需要两个长度为 n+1 的 DP 数组