30 秒识别Q1 · 1004 Max Consecutive Ones III:数组只含 0 和 1,最多可以把 k 个 0 翻成 1,求最长的全 1 连续子数组。你的第一反应是哪个模式?
看答案
滑动窗口。不合法 = 「窗口内 0 的个数 > k」,状态就一个计数器。「最多 K 次操作」是滑窗最标准的暗号。
先问自己三句话,答完模板就填完了:窗口里要维护什么状态(计数 / 和 / 去重集合)?什么时候算不合法(while 的条件)?答案在哪一刻统计(收缩之后,此刻窗口一定合法)?
复杂度是 O(n) 而不是 O(n²):左指针只会向右走,两个指针各自最多走 n 步。如果你写出的收缩循环里左指针可能回退,那说明它不是滑动窗口。
| 题面出现… | 想到 |
|---|---|
| 「最长 / 最短连续子串 / 子数组」+ 一个约束条件 | 可变窗口(模板 A) |
| 「至多 K 个不同字符 / 至多 K 次替换 / 至多 K 个 0」 | 可变窗口,K 就是合法性阈值 |
| 「长度恰好为 k 的子数组的最大和 / 平均值」 | 定长窗口(模板 B) |
| 「s1 的排列是不是 s2 的子串」「找出所有 anagram 起点」 | 定长窗口 + 计数相等(模板 B) |
| 「包含 t 中所有字符的最小子串」 | 可变窗口 + 缺口计数(76 题) |
| 你的暴力解是「枚举所有子数组再逐个检查」 | 滑窗优化信号 |
都带「连续子数组」四个字,但分流很清楚:求最值 / 长度且约束单调 → 滑动窗口(本章);求「和恰好等于 K 的个数」→ 前缀和 + 哈希(第 13 章),因为窗口和不单调、收缩条件无从写起;窗口大小固定且要窗口内的最大 / 最小值 → 单调队列(第 04 章)。另外,数组里有负数时「和 ≥ target 的最短子数组」也不能用滑窗——单调性没了。还有一个同名陷阱:本章的「最短」永远修饰连续子数组;「最短路径 / 最少步数」说的是图上的 BFS(第 07 章的层序、第 10 章的网格),两者毫无关系。
■ 【骨架】 不变,闭着眼默写 ■ 【插槽】 随题变化,写题前先问自己填什么
from collections import Counter
def longest_no_repeat(s):
cnt = Counter() # 【插槽】窗口状态:计数 / 和 / 去重集合
left = best = 0 # 【骨架】
for right, ch in enumerate(s): # 【骨架】右边界无脑扩
cnt[ch] += 1 # 【插槽】加入 s[right] 后怎么更新状态
while cnt[ch] > 1: # 【插槽】"不合法"的判定 —— 整道题的灵魂
cnt[s[left]] -= 1 # 【插槽】移出 s[left] 后怎么更新状态
left += 1 # 【骨架】收缩到重新合法为止
best = max(best, right - left + 1) # 【插槽】答案在"已经合法"之后统计
return best
def max_fixed_window(nums, k):
total = sum(nums[:k]) # 【骨架】先把第一个窗口填满
best = total # 【插槽】要最大 / 最小 / 全部记录下来
for i in range(k, len(nums)): # 【骨架】
total += nums[i] - nums[i - k] # 【骨架】进一个,出一个,状态 O(1) 更新
best = max(best, total) # 【插槽】
return best
while 收缩,只有「进一个、出一个」。注意 k > len(nums) 时首个窗口就不成立,调用前先挡一下。best = max(best, price - min_so_far) 就够。last[ch] 记录上次位置,left = max(left, last[ch] + 1) 一步跳到位——max 不能省,见翻车点③。窗口长度 - 出现最多的字符数 > k。most 只增不减看起来是 bug,实际不影响最终答案(窗口不会缩小,答案也就不会被高估),这一点值得在面试里主动讲清楚。Counter 相等判断。习惯上要把减到 0 的键 del 掉:Python 3.10 起 Counter 的 == 已经会忽略零计数(所以这道题不删也能过),但 len() / in 仍然把它算进去,换成普通 dict 更是直接判不等。模板测试里三条断言把这个区别钉死了。missing 计数「还差几个字符」,为 0 才收缩。说清框架即可,不必默写。while 写成 if——一次收缩不一定能恢复合法(例如窗口里同一个字符出现了三次)。除非你能证明每步最多违规一次,否则一律用 while。max——3 题的进阶写法里 left = max(left, last[ch] + 1);漏掉 max,遇到 "abba" 时左指针会倒退,答案偏大。Counter 减到 0 的键不会自动消失。== 从 Python 3.10 起会忽略零计数,但 len(cnt)、in、遍历都还算它——用「窗口内不同字符数」当合法性判据时必须 del。换成普通 dict 时连 == 都会判不等。30 秒识别Q1 · 1004 Max Consecutive Ones III:数组只含 0 和 1,最多可以把 k 个 0 翻成 1,求最长的全 1 连续子数组。你的第一反应是哪个模式?
滑动窗口。不合法 = 「窗口内 0 的个数 > k」,状态就一个计数器。「最多 K 次操作」是滑窗最标准的暗号。
30 秒识别Q2 · 438 Find All Anagrams in a String:找出字符串中所有「另一个串的字母异位词」的起始下标。你的第一反应是哪个模式?
定长窗口(模板 B)+ 计数相等,和 567 是同一道题换了个问法:567 问存不存在,它问全部位置。窗口长度固定为模式串长度。
30 秒识别Q3 · 239 Sliding Window Maximum:窗口大小固定为 k,返回每个窗口里的最大值。你的第一反应是哪个模式?
单调队列,不是本章——窗口是定长的,但要的是窗口内的最值,而滑窗模板只会维护「和 / 计数」这类能 O(1) 增删的状态,最大值删掉后无法 O(1) 恢复。见 §2 分流条件与第 04 章模板 B。
每章自测都会混入恰好一道非本章题,防止「读哪章选哪章」的条件反射;题面也一律取自本章未收录的题,避免凭记忆而非凭信号作答。