题目描述
思路解析
一句话答案:LeetCode 978 最长湍流子数组,找升降符号交替的最长段。双状态动态规划一遍扫:up 记这步在升、down 记这步在降的最长长度,升接旧 down、降接旧 up 交替续,相等归 1,答案取全程最大,时间 O(n)、空间 O(1)。
最长湍流子数组,这道题到底在找什么
给一个整数数组 arr,找最长的「湍流」子数组,返回长度。湍流就是相邻两数一升一降交替:大、小、大、小,或小、大、小、大,一旦相等或连续同向,交替就断。子数组要连续、不能跳挑。比如 arr=[4,8,12,16] 一路上升,最多取相邻两个得 2;arr=[100] 单数一段得 1。
为什么不能把每一段子数组都截出来验一遍
连续子数组有 O(n²) 段(大 O 记号,描述规模变大时操作数怎么涨),每段还要 O(n) 验符号是否交替,合起来 O(n³),稍长就跑不动。而且大量重叠:短段 4、2、10、7 验过,长段接上 8 又从头查一遍。既然长段就是短段接一个数,就不必每段重验,只盯「以当前数结尾、还在生长的湍流段有多长」往后接即可。
一个升态一个降态,为什么非得分开记两个长度
这里用动态规划(以当前数结尾的湍流段长算一次存 up/down,下一步直接接)。只记一个「当前最长湍流段」不够:下一步能不能接,取决于当前段结尾那步是升还是降。
所以拆成两个状态(记扫到当前为止两种局面各自的最长长度):up 是以当前数结尾、最后一步上升的最长段长,down 是最后一步下降的。两个都从 1 起,单数就是长度 1 的段。
升接旧 down、降接旧 up,两个状态怎么互相接力
扫到新数先和前一个比。这一步上升(前一个小于当前),要交替,前一段结尾得是下降,新 up 接旧 down:up = 旧 down + 1,同向的 down 归 1。这步下降反过来,新 down = 旧 up + 1,up 归 1。两数相等既不升也不降,湍流断掉,up、down 一起归 1、重数。每处理完一个数,用 up、down 刷新全局最长答案。
落到代码上,升态记成 f、降态记成 g,都从 1 起;每步先用 ff、gg 暂存新值再一起赋回,免得先改的盖掉要读的旧值。
把题面例子的 up、down 亲手走一遍
拿 arr=[9,4,2,10,7,8,8,1,9] 走一遍:起手 up=down=ans=1。扫 4 比 9 小是降,down = 旧 up + 1 = 2,ans=2;扫 2 仍降,down = 2;扫 10 转升,up = 旧 down + 1 = 3,ans=3;扫 7 又降,down = 旧 up + 1 = 4,ans=4;扫 8 转升,up = 旧 down + 1 = 5,ans=5;再扫 8 相等,硬断,up、down 全归 1,ans 留 5;扫 1 降,down = 2;扫 9 升,up = 旧 down + 1 = 3。全程最大 5,就是 4、2、10、7、8,符号大、小、大、小交替。
相等那步忘把 up、down 一起归 1,湍流为什么会假装没断
扫一遍、每数只比一次,时间 O(n);只留 up、down、ans 三个量轮换,空间 O(1),不用开二维表。
相等是最容易漏的硬断:符号一断,up、down 必须同时归 1、只清当前段,忘了归 1 会让后面错把断点前的长度接着算。ans 别跟着清,它记全程见过的最长,两个 8 打断后仍停在 5,每断就重置会把攒下的最长丢光。边界:单数返回 1,单调或全相等最多 2 或 1。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这套「升就 up 接旧 down、降就 down 接旧 up、相等全归 1,每步刷新答案」,下面每一帧都在套它。
开局:第 0 个元素 9 自己就是一段长度 1 的湍流段(绿色)。up 和 down 都初始化成 1,从第 1 个起才有「前一个」可比。
扫到第 1 个 4,和前一个 9 比。绿色是到上一个为止还在生长的湍流段(长度 1),此刻 up=1、down=1。看着是下降,下一帧看 down 怎么接。
4 比 9 小,这一步是下降。反过来,新的 down 接在「上一个结尾是上升」的段后面:down = 旧 up + 1 = 2;up 归 1。刷新答案 ans=2。
扫到第 2 个 2,和前一个 4 比。绿色是到上一个为止还在生长的湍流段(长度 2),此刻 up=1、down=2。看着是下降,下一帧看 down 怎么接。
2 比 4 小,这一步是下降。反过来,新的 down 接在「上一个结尾是上升」的段后面:down = 旧 up + 1 = 2;up 归 1。还没超过 ans=2,答案不动。
扫到第 3 个 10,和前一个 2 比。绿色是到上一个为止还在生长的湍流段(长度 2),此刻 up=1、down=2。看着是上升,下一帧看 up 怎么接。
10 比 2 大,这一步是上升。湍流要交替,所以新的 up 接在「上一个结尾是下降」的段后面:up = 旧 down + 1 = 3;同向接不上的 down 归 1。这个 3 比旧答案大,刷新 ans=3。
扫到第 4 个 7,和前一个 10 比。绿色是到上一个为止还在生长的湍流段(长度 3),此刻 up=3、down=1。看着是下降,下一帧看 down 怎么接。
7 比 10 小,这一步是下降。反过来,新的 down 接在「上一个结尾是上升」的段后面:down = 旧 up + 1 = 4;up 归 1。刷新答案 ans=4。
扫到第 5 个 8,和前一个 7 比。绿色是到上一个为止还在生长的湍流段(长度 4),此刻 up=1、down=4。看着是上升,下一帧看 up 怎么接。
8 比 7 大,这一步是上升。湍流要交替,所以新的 up 接在「上一个结尾是下降」的段后面:up = 旧 down + 1 = 5;同向接不上的 down 归 1。这个 5 比旧答案大,刷新 ans=5。
扫到第 6 个 8,和前一个 8 比。绿色是到上一个为止还在生长的湍流段(长度 5),此刻 up=5、down=1。两个数一样大,怕是要断,下一帧见分晓。
8 和 8 一样大,既不算上升也不算下降,湍流彻底断在这里。up 和 down 同时归 1,从第 6 个重新数。注意 ans 仍保留着 5,断开只清空当前段,绝不抹掉历史最长。
扫到第 7 个 1,和前一个 8 比。绿色是到上一个为止还在生长的湍流段(长度 1),此刻 up=1、down=1。看着是下降,下一帧看 down 怎么接。
1 比 8 小,这一步是下降。反过来,新的 down 接在「上一个结尾是上升」的段后面:down = 旧 up + 1 = 2;up 归 1。还没超过 ans=5,答案不动。
全部扫完。一路上 up 和 down 此消彼长,最长的一段湍流是高亮的这 5 个:4、2、10、7、8,比较符大、小、大、小完整交替。后面 8、8 相等把湍流打断,所以没能更长。答案 5。
回放这段赢家,看比较符怎么一步步翻转。先单看第 1 个 4,长度 1。
4 到 2 是下降,比较符是「大」,和前一对正式开头,交替成立,长度到 2。
2 到 10 是上升,比较符是「小」,和前一对正好相反,交替成立,长度到 3。
10 到 7 是下降,比较符是「大」,和前一对正好相反,交替成立,长度到 4。
7 到 8 是上升,比较符是「小」,和前一对正好相反,交替成立,长度到 5。比较符一路 大、小、大、小 交替,五个元素,答案就是 5。
边界先想清:单元素为 1、单调或全相等都退化成最多 2 或 1。
两个高频追问,重点是「为什么要双状态」。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class Solution: def maxTurbulenceSize(self, arr: List[int]) -> int: ans = f = g = 1 for a, b in pairwise(arr): ff = g + 1 if a < b else 1 gg = f + 1 if a > b else 1 f, g = ff, gg ans = max(ans, f, g) return ans复杂度
- 时间:O(n),从头到尾扫一遍,每个元素只看一次
- 空间:O(1),只用 up、down、ans 三个变量,状态滚动复用
易错点
面试追问把动画讲成自己的话
追问这题和「最长连续递增子数组」(LC674)有什么区别?
追问为什么非要两个状态,一个变量行不行?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
最低票价
LeetCode 983 · 中等 · 沿着 动态规划套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题