题目描述
思路解析
一句话答案: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)。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住「遇 0 清零、遇正 pos+1、遇负 正负交换」,pos 和 neg 一直在记两种乘积的最长结尾长度。
开局三个计数都为 0。从左往右逐个读:盯住 pos(正乘积最长结尾)和 neg(负乘积最长结尾)怎么随符号变化。
读到第 0 个是正数 1。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
更新:1 > 0 → pos = 旧pos+1 = 1;neg = 旧neg为0仍为 0。现在 pos=1,绿色就是当前「乘积为正」的最长结尾段。它超过了旧 ans,刷新 ans=1。
读到第 1 个是负数 -2。负数翻转符号:原来的正段会变负、原来的负段会变正,pos 和 neg 要互换着更新。
更新:pos=0(当前以这个数结尾还凑不出正乘积),neg=2。绿色暂时消失,等后面再出现负数把负段翻回正段。ans 仍是 1。
读到第 2 个是负数 -3。负数翻转符号:原来的正段会变负、原来的负段会变正,pos 和 neg 要互换着更新。
更新:-3 < 0 → pos = 旧neg+1 = 3;neg = 旧pos+1 = 1。现在 pos=3,绿色就是当前「乘积为正」的最长结尾段。它超过了旧 ans,刷新 ans=3。
读到第 3 个是正数 4。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
更新:4 > 0 → pos = 旧pos+1 = 4;neg = 旧neg+1 = 2。现在 pos=4,绿色就是当前「乘积为正」的最长结尾段。它超过了旧 ans,刷新 ans=4。
读到第 4 个是负数 -5。负数翻转符号:原来的正段会变负、原来的负段会变正,pos 和 neg 要互换着更新。
更新:-5 < 0 → pos = 旧neg+1 = 3;neg = 旧pos+1 = 5。现在 pos=3,绿色就是当前「乘积为正」的最长结尾段。还没超过 ans=4,保持。
读到第 5 个是 0。乘积一旦乘上 0 就变 0,绝不可能为正——它会把当前所有连胜清零。
更新:pos 和 neg 都清零(红色标出这个 0 是分界)。前面攒的连胜到此作废,下一段从 0 之后重新开始。ans 仍保留历史最大 4。
读到第 6 个是正数 6。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
更新:6 > 0 → pos = 旧pos+1 = 1;neg = 旧neg为0仍为 0。现在 pos=1,绿色就是当前「乘积为正」的最长结尾段。还没超过 ans=4,保持。
读到第 7 个是正数 7。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
更新:7 > 0 → pos = 旧pos+1 = 2;neg = 旧neg为0仍为 0。现在 pos=2,绿色就是当前「乘积为正」的最长结尾段。还没超过 ans=4,保持。
读到第 8 个是负数 -8。负数翻转符号:原来的正段会变负、原来的负段会变正,pos 和 neg 要互换着更新。
更新:pos=0(当前以这个数结尾还凑不出正乘积),neg=3。绿色暂时消失,等后面再出现负数把负段翻回正段。ans 仍是 4。
读到第 9 个是正数 9。正数不改变乘积符号:正的更正、负的更负,pos 能直接接着长。
更新:9 > 0 → pos = 旧pos+1 = 1;neg = 旧neg+1 = 4。现在 pos=1,绿色就是当前「乘积为正」的最长结尾段。还没超过 ans=4,保持。
扫完全程,绿色这 4 个 1、-2、-3、4 乘积为正(含 2 个负号,成对抵消),长度 4 就是答案。其余灰段要么含 0、要么负号是奇数个。
边界先想清:单负为 0、双负为正、0 处必清零。
两个追问,核心是认出「负号翻转 → 同时维护正负两态」这个母题。
参考代码
from typing import Listclass 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 ans复杂度
- 时间:O(n),从头到尾扫一遍
- 空间:O(1),只用 pos、neg、ans 三个变量
易错点
面试追问把动画讲成自己的话
追问不用 DP,能否用「记录负号位置」的思路?
追问这题和「乘积最大子数组 LC152」有何异同?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
统计只差一个字符的子串数目
LeetCode 1638 · 中等 · 沿着 动态规划套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题