LC76 最小覆盖子串¶
讲解章节:双指针与滑动窗口
一句话
可变长滑动窗口:右端负责凑齐,左端负责在合法前提下尽量压缩。
解题思路¶
这题考什么¶
要在 s 里找最短的、把 t 的每个字符(含重复次数)都覆盖住的子串。 枚举左右端点是 O(m^2) 个区间,每个还要验证一次覆盖,m = 1e5 下不可能。
能上滑动窗口的根据是单调性:若窗口 s[l .. r] 已经覆盖了 t, 那么把右端继续右扩得到的窗口也一定覆盖 t(字符只增不减)。 于是对每个右端 r,存在唯一的「最靠右的合法左端」g(r), 而且 g 随 r 单调不减——右端右移只会让约束更松,合法左端不可能变得更靠左。 所以左指针只需单调右移、永不回退,两个指针合起来走 2m 步。 见 双指针与滑动窗口。
算法形态是「右端扩张到合法,左端收缩到临界,记录答案」:
覆盖判定怎么做到 O(1):维护
右端吞进 c 时,只有 need[c] > 0 才说明这个 c 是「真正补上缺口」的, 此时 missing 减一;否则它只是多余的富余量。missing == 0 就是覆盖成立。 用一个标量代替「逐个检查 t 里每种字符够不够」,把每步压成常数。
数据规模与复杂度¶
1 <= m = len(s), n = len(t) <= 1e5,s 与 t 都由英文字母组成 (大小写都算,共 52 种,所以用字典而不是长度 26 的数组)。 时间 O(m + n):建 need 表 O(n),左右指针各自单调走完 s 一遍; 空间 O(k),k 是 t 里的不同字符数,上界 52,实际是常数。 正好达到题面「进阶」要求的 O(m + n)。
坑在哪¶
- 左指针不能回退,这是整份解法的效率根据,也是最容易写错的地方。 常见的错误写法是「右端每前进一步,就从头再找一次最优左端」, 那是 O(m^2)。左端只右移的合法性来自上面那条单调性: r 增大时最靠右的合法左端 g(r) 不会变小,所以走过的位置永远不必回头。
- t 里有重复字符时必须按次数统计,不能退化成集合。 题面明示「包括重复字符」,示例 3 的 s = "a"、t = "aa" 期望空串, 若把 t 当成集合,会误判 "a" 已覆盖。
- need[c] 允许为负,这不是 bug 而是设计:负值记录窗口里多带的富余量。 正因如此,左端吐出字符时要先加回 need[c],再看它是否变成正数—— 只有从 0 变成 1 才意味着真的缺了,missing 才加一。 若只要 need[c] 不为 0 就加 missing,富余字符会被误判成缺口, 窗口刚一收缩就退出,答案偏长。
- 记录答案要在收缩循环内部、吐出字符之前。放到循环外面, 记下的是已经不再覆盖的窗口,长度会短一格且不合法。
- 窗口长度是 right - left + 1(闭区间),最终切片是 s[best_l : best_l + best_len], 切片右端是开区间,写成 s[best_l : best_l + best_len - 1] 会少一个字符。
- 无解时返回空字符串。这里靠 best_len 的初值(比 m 大的哨兵)兜住: 从没进过收缩循环则 best_len 不变,直接返回 ""。若初值取 0 之类的值, 就分不清「无解」与「找到了长度 0 的答案」。
- 严格比对:题面写「测试用例保证答案唯一」,所以最短覆盖子串就是那一个, 不存在多解放宽。
样例复核¶
s = "ADOBECODEBANC",t = "ABC",need = A:1 B:1 C:1,missing = 3。
下标 0 1 2 3 4 5 6 7 8 9 10 11 12
字符 A D O B E C O D E B A N C
r=0 'A' need[A] 由 1 减到 0,missing=2
r=3 'B' missing=1
r=5 'C' missing=0,覆盖成立,进入收缩:
窗口 [0,5] "ADOBEC" 长 6 -> 记为当前最优;
吐 s[0]='A',need[A] 回到 1(真的缺了),missing=1,left=1,退出
r=9 'B' need[B] 此刻是 0,不是正数 -> missing 不变(仍是 1),
need[B] 变成 -1,记下「窗口里多带了一个 B」
r=10 'A' missing=0,收缩:[1,10] 长 10、[2,10] 长 9 都不如 6;
吐 s[3]='B' 时 need[B] 由 -1 加回 0,**不是正数**,
所以 missing 不变、收缩继续——这正是负值富余量的用处;
一路吐到 s[5]='C',need[C] 变成 1,missing=1,left=6,退出
r=12 'C' missing=0,收缩:[6,12] 长 7、[7,12] 长 6 都不更优,
[8,12] 长 5 -> 记下,[9,12] "BANC" 长 4 -> 记下;
再吐 s[9]='B' 就缺 B,missing=1,left=10,退出
最终 best_l=9、best_len=4,切片 s[9:13] = "BANC",与样例一致。 若第 4 条坑写错(把记录挪到收缩循环之后),r=12 这一轮记下的会是 已经不再覆盖的 [10,12] "ANC"。