乘积为正数的最长子数组长度 图解题解
这道题到底在问什么
- 输入
- nums=[1,-2,-3,4]
- 输出
- 4 (整段乘积 = 正,负号成对抵消)
最优解:为什么这么做
一句话答案:LeetCode 1567 乘积为正数的最长子数组长度用双状态扫描:pos 记以当前位结尾乘积为正的最长长度、neg 记为负的,遇负数把两者互换加一、遇 0 清零,一趟扫完取 pos 最大值,时间 O(n)、空间 O(1)。
乘积为正的最长子数组,到底在找什么
给一个整数数组 nums,有正有负、还可能有 0,找一段连续子数组(挨着的一截、不能跳挑),让这截乘起来为正,返回最长长度;乘积为 0 不算正。一段的正负只看负数个数,偶数个(含 0 个)为正,奇数个为负。nums=[1,-2,-3,4] 里 -2、-3 两负号成对抵消,整段为正,长度 4。
10⁵ 个数的 nums,把所有子数组都试一遍为什么跑不完
n 个数能划出约 n²/2 段连续子数组,逐段判正负再留最长,光枚举就 O(n²) 个(大 O 记号描述规模变大时操作数怎么涨),每段再乘一遍,叠成 O(n³)。浪费在相邻段符号重算。其实只要「以每个位置结尾乘积为正的最长段」,扫一遍递推(拿前一格推出后一格)就够。
为什么非得同时盯正、负两个长度
从左往右扫,要知道「以当前数结尾、乘积为正的最长段多长」。难点是负数翻转符号:正段碰负数变负,负段反而变正。只记正段不够,遇负数能翻出多长正段,来自之前攒的负段。所以同时维护两个状态(边扫边记、往后递推的量):pos 记以当前位结尾乘积为正的最长长度,neg 记为负的。把 pos、neg 存下再往后推就是动态规划(DP)。
遇正数、负数、0,两个状态各怎么变
每读一个数按符号更新(这步叫转移,由前一格的 pos、neg 推出这一格)。遇 0:乘出来必不为正,pos、neg 清零,下段重起。遇正数:符号不变,pos=旧 pos+1;neg=旧 neg+1,但旧 neg 为 0 时仍是 0(没负段,正数乘不出负)。遇负数:正负互换,新 pos=旧 neg+1(旧 neg 为 0 则为 0,得先有负段可翻正),新 neg=旧 pos+1(接上正段成负段,没正段时自成长度 1 负段)。
负数这步必须同时取旧值——参考代码 pos, neg = (neg+1 if neg else 0), pos+1 一行赋值正为此。
拿 [1,-2,-3,4] 把两个状态逐格填出来
起点 pos=neg=ans=0。读 1(正):pos=1、neg=0、ans=1。读 -2(负):旧 neg 为 0 故新 pos=0,新 neg=旧 pos+1=2,ans 仍 1。读 -3(负):新 pos=旧 neg+1=2+1=3(旧 neg=2 的负段被 -3 翻正),新 neg=旧 pos+1=0+1=1,ans=max(1,3)=3。读 4(正):pos=旧 pos+1=4,neg=旧 neg+1=2,ans=max(3,4)=4。扫完 ans=4,正是整段 [1,-2,-3,4]。
负数分支先改 pos 再算 neg,答案为什么会莫名偏小
答案莫名偏小,多半栽在负数分支串用旧值:先写 pos=neg+1、再写 neg=pos+1,第二行的 pos 已是新值,neg 接错、后面每格跟着错。遇 0 忘清零,跨 0 的两段被拼成一条,长度虚高,可乘积明明是 0。凭空造出的假负段藏得深:遇正数旧 neg 为 0 还硬加 1、遇负数忘把新 pos 置 0,pos 都会接上不存在的负段、越算越大。复杂度:扫一趟、每数常数次判断,时间 O(n);只用 pos、neg、ans 三个变量,空间 O(1)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住「遇 0 清零、遇正 pos+1、遇负 正负交换」,pos 和 neg 一直在记两种乘积的最长结尾长度。
- 4开局三个计数都为 0。从左往右逐个读:盯住 pos(正乘积最长结尾)和 neg(负乘积最长结尾)怎么随符号变化。
- 5读到第 0 个是正数 1。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
- 6更新:1 > 0 → pos = 旧pos+1 = 1;neg = 旧neg为0仍为 0。现在 pos=1,绿色就是当前「乘积为正」的最长结尾段。它超过了旧 ans,刷新 ans=1。
- 7读到第 1 个是负数 -2。负数翻转符号:原来的正段会变负、原来的负段会变正,pos 和 neg 要互换着更新。
- 8更新:pos=0(当前以这个数结尾还凑不出正乘积),neg=2。绿色暂时消失,等后面再出现负数把负段翻回正段。ans 仍是 1。
- 9读到第 2 个是负数 -3。负数翻转符号:原来的正段会变负、原来的负段会变正,pos 和 neg 要互换着更新。
- 10更新:-3 < 0 → pos = 旧neg+1 = 3;neg = 旧pos+1 = 1。现在 pos=3,绿色就是当前「乘积为正」的最长结尾段。它超过了旧 ans,刷新 ans=3。
- 11读到第 3 个是正数 4。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
- 12更新:4 > 0 → pos = 旧pos+1 = 4;neg = 旧neg+1 = 2。现在 pos=4,绿色就是当前「乘积为正」的最长结尾段。它超过了旧 ans,刷新 ans=4。
- 13读到第 4 个是负数 -5。负数翻转符号:原来的正段会变负、原来的负段会变正,pos 和 neg 要互换着更新。
- 14更新:-5 < 0 → pos = 旧neg+1 = 3;neg = 旧pos+1 = 5。现在 pos=3,绿色就是当前「乘积为正」的最长结尾段。还没超过 ans=4,保持。
- 15读到第 5 个是 0。乘积一旦乘上 0 就变 0,绝不可能为正——它会把当前所有连胜清零。
- 16更新:pos 和 neg 都清零(红色标出这个 0 是分界)。前面攒的连胜到此作废,下一段从 0 之后重新开始。ans 仍保留历史最大 4。
- 17读到第 6 个是正数 6。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
- 18更新:6 > 0 → pos = 旧pos+1 = 1;neg = 旧neg为0仍为 0。现在 pos=1,绿色就是当前「乘积为正」的最长结尾段。还没超过 ans=4,保持。
- 19读到第 7 个是正数 7。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
- 20更新:7 > 0 → pos = 旧pos+1 = 2;neg = 旧neg为0仍为 0。现在 pos=2,绿色就是当前「乘积为正」的最长结尾段。还没超过 ans=4,保持。
- 21读到第 8 个是负数 -8。负数翻转符号:原来的正段会变负、原来的负段会变正,pos 和 neg 要互换着更新。
- 22更新:pos=0(当前以这个数结尾还凑不出正乘积),neg=3。绿色暂时消失,等后面再出现负数把负段翻回正段。ans 仍是 4。
- 23读到第 9 个是正数 9。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
- 24更新:9 > 0 → pos = 旧pos+1 = 1;neg = 旧neg+1 = 4。现在 pos=1,绿色就是当前「乘积为正」的最长结尾段。还没超过 ans=4,保持。
- 25扫完全程,绿色这 4 个 1、-2、-3、4 乘积为正(含 2 个负号,成对抵消),长度 4 就是答案。其余灰段要么含 0、要么负号是奇数个。
⚠️ 容易写错的地方
✗ 错:遇负数时先更新 pos 再用旧 pos 算 neg
✓ 对:必须用「同一时刻的旧 pos、旧 neg」同时算新值
先改 pos 会污染算 neg 用的旧 pos,要么交换、要么用临时变量
✗ 错:neg 从 0 直接 +1
✓ 对:neg 只有原来 > 0 才能延长,否则保持 0
没有负段可接时,硬加会凭空造出不存在的负乘积段
✗ 错:把 0 也算进正乘积
✓ 对:遇 0 一律清零
乘积含 0 就是 0,不是正数
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def getMaxLen(self, nums: List[int]) -> int:
pos = neg = ans = 0
for x in nums:
if x == 0:
pos = neg = 0
elif x > 0:
pos += 1
neg = neg + 1 if neg else 0
else:
pos, neg = (neg + 1 if neg else 0), pos + 1
ans = max(ans, pos)
return ansC++
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int getMaxLen(vector<int>& nums) {
int pos = 0, neg = 0, ans = 0;
for (int x : nums) {
if (x == 0) pos = neg = 0;
else if (x > 0) { ++pos; neg = neg ? neg + 1 : 0; }
else { int oldPos = pos; pos = neg ? neg + 1 : 0; neg = oldPos + 1; }
ans = max(ans, pos);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int getMaxLen(int[] nums) {
int pos = 0, neg = 0, ans = 0;
for (int x : nums) {
if (x == 0) { pos = 0; neg = 0; }
else if (x > 0) { pos++; neg = neg == 0 ? 0 : neg + 1; }
else { int oldPos = pos; pos = neg == 0 ? 0 : neg + 1; neg = oldPos + 1; }
ans = Math.max(ans, pos);
}
return ans;
}
}复杂度
时间
O(n)
从头到尾扫一遍
空间
O(1)
只用 pos、neg、ans 三个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 乘积为正数的最长子数组长度 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
只维护一个「最长正乘积长度」不行吗,为什么非带上 neg?+
因为负数会翻转符号:当前的正段遇负数变负,负段遇负数变正。只记正段长度,遇到负数时你不知道能翻出多长的正段——那正好来自之前攒下的负段。neg 就是为将来的负数备着的「翻身本钱」,没有它,负数一来就接不上历史长度,pos 只能从头再数。所以正、负两个状态得成对维护,缺一个就漏解。
遇正数时 neg 为什么要判「旧 neg 大于 0 才加 1,否则保持 0」?+
neg 记的是以当前位结尾、乘积为负的最长段。正数不改变符号,能把已有的负段接长,所以 neg=旧 neg+1。但若之前根本没有负段(旧 neg 为 0),一个正数自己乘不出负乘积,neg 只能还是 0。要是这里不判断、直接 neg+1,就凭空造出一个长度 1 的假负段,后面接到负数时会把 pos 算大,答案偏高。
这题和乘积最大子数组(LeetCode 152)是什么关系?+
骨架都是「负号翻转就成对维护两个状态」。152 维护以当前位结尾的最大、最小乘积,遇负数交换 max 与 min;本题维护乘积为正、为负的最长长度,遇负数交换 pos 与 neg。原因一样:一个负数能把最大翻成最小、把正翻成负,单一状态兜不住,必须两个一起记。会了一个,另一个换的只是「记乘积值」还是「记长度」。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 乘积为正数的最长子数组长度 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。