BM90 最小覆盖子串¶
较难通过率 27.35%python3核心代码模式样例通过牛客 AC字符串哈希双指针
讲解章节:双指针与滑动窗口
一句话
滑动窗口:右指针扩到「够了」就收左指针,两个指针各走一遍。
解题思路¶
这题考什么¶
两条单调性凑出线性解法。其一:若 S[l..r] 已经覆盖 T,把 r 继续右移只会更长、 不会更优;其二:右端点固定时,可行的左端点构成一段前缀(l 越小越容易覆盖), 所以每个右端点只需要关心最靠右的那个可行左端点。 两条合起来意味着左端点随右端点单调不减,这正是滑动窗口成立的前提, 也是它 O(n) 的来源。见 双指针与滑动窗口。
一张计数表加一个标量:
右指针吃进字符 c 时 need[c] -= 1;只有在减之前 need[c] > 0 (说明这一个确实在补缺,而不是多余的第二份)才让 missing -= 1。 T 里有重复字符时(样例 "AA"),这条规则保证两个 "A" 都要凑齐。
missing == 0 说明窗口已覆盖 T,此时把左端收到不能再收: 左端字符若是「窗口里恰好只剩必需的那一份」就停手,否则吐掉并右移。 收完结算一次答案,再主动吐掉一个必需字符让窗口失效,去找下一个可行窗口。
走一遍 S = "abcAbA"、T = "AA"(need = {"A": 2},missing = 2):
r 字符 窗口 need["A"] missing 动作
0 a [a] 2 2 与 T 无关
1 b [ab] 2 2
2 c [abc] 2 2
3 A [abcA] 1 1 补到一个
4 b [abcAb] 1 1
5 A [abcAbA] 0 0 覆盖成立,开始收左指针
a、b、c 都与 T 无关,逐个吐掉
收到 [AbA] 时再收就要吐掉 "A",停手
答案 = "AbA",长度 3
数据规模与复杂度¶
|S|, |T| <= 1e4,仅含大小写英文字母,时限「其他语言 2 秒」。 每个字符至多进窗一次、出窗一次,时间 O(n + m), 空间 O(字符集) = O(52)——need 表最多装下 T 里出现过的字符种类。 暴力枚举左右端点再逐个数字符是 O(n^2 * m) = 1e4 * 1e4 * 1e4 = 1e12 量级, 完全不可行。
坑在哪¶
- 左指针只能单调右移,绝不能因为换了右端点就退回去重扫。 一旦允许回退,总移动次数就从 O(n) 退化回 O(n^2); 而上面那条单调性保证了不会漏解——被左指针甩掉的左端点, 配任何更靠右的右端点都只会得到更长的窗口。
- 结算完答案必须主动把 S[l] 吐回去、missing += 1、l += 1,人为破坏覆盖。 不这么做,missing 会一直是 0,外层的提前 continue 拦不住任何字符, 内层的收缩循环则在同一个位置反复空转。
- 收缩的停止条件是「该字符属于 T 且 need 已经归零」。 need[lc] == 0 意味着窗口里这个字符恰好只剩必需的那一份, 再吐一个覆盖就破了;need[lc] < 0 说明还有富余,可以放心吐掉并加回一份。
- 只记 (best_l, best_len),最后再切一次片。每次窗口成立都拷一份子串出来, 常数会被抬到无法接受的程度。
- best_len 初值取 n + 1:任何合法窗口长度都不超过 n, 所以末尾用 best_len > n 就能判出「一次都没成立过」,返回空串。 初值取 0 或 n 都会与合法答案混淆。
- 题面保证满足条件的最短子串唯一,所以更新答案时用严格 < 或 <= 都不影响结果。
参考实现¶
[:octicons-arrow-left-16: BM89](BM89.md) [BM91 :octicons-arrow-right-16:](BM91.md)