76. 最小覆盖子串

困难 含交互动画

学完这道你应该能: 说清它为什么用「变长」,而不是死记步骤; 合上页面,自己默写出核心代码; 用 30 秒把思路讲清楚。

AI 私教 · 就着本题动画讲
卡住了不用硬扛,按 8 步把这题真正走通。
从读题、试解、找法到手写代码,边问边打勾,适合第一次系统学算法的同学。

题目描述

给字符串 s 和 t,在 s 里找出最短的子串,使它包含 t 的所有字母(含重复次数)。找不到返回空串。

s = "ADBC"
t = "BC"
输出 = "BC"

思路解析

把每个起点、每个终点都试一遍当然能做,但 n² 个子串里绝大多数明显不够格,白白检查,太慢。

need 记下 t 里每个字母还差几个。右指针 r 不停吃字符,直到窗口把 t 全包住;这时再移左指针 l 把多余的左边挤掉,窗口能缩就缩、缩到不能再缩,就拿到一个最短候选。右扩只会让窗口更可能覆盖,左缩只在已覆盖时做——所以两根指针都不回头。

准备:先数 t="BC":need = {B:1, C:1},一共要凑齐 2 种字母才算覆盖。窗口从空开始。

r = 0 · 吃进 A:A 不是 t 需要的字母,吃进窗口但 need 不变,仍差 B、C 两种。

r = 1 · 吃进 D:D 同样不需要。窗口 "AD" 还是没覆盖 t——这就是为什么不能急着检查、要让 r 继续扩。

r = 2 · 吃进 B:B 正是要的!need[B] 从 1 减到 0,凑齐种类数 +1。现在差 C 一种。

r = 3 · 吃进 C:C 也吃进来,need 全部归零,凑齐数 = required = 2,第一次覆盖 t!记下候选 "ADBC"(长 4)。覆盖之后,轮到左指针登场——开始收缩。

r = 3 · 左缩① 丢 A:最左是 A,不是 t 要的,丢掉它窗口照样覆盖l 前进到 1,窗口缩成 "DBC"(长 3),刷新最短。

r = 3 · 左缩② 丢 D:D 同样多余,丢掉,l 到 2。窗口缩成 "BC"(长 2)——更短了!继续试着缩。

r = 3 · 左缩③ 想丢 B:再想丢最左的 B——但 B 是 t 必需的!一旦丢掉,need[B] 弹回 1,覆盖被打破。这就是收缩的边界:缩到「再缩一步就不覆盖」就得停。所以 "BC" 是这一轮的最短。

结束:r 已到末尾、左边也缩到极限。整个过程里最短的覆盖子串是 "BC",就是答案。

「找最短的合法窗口」都是这个手感:r 扩到刚好合法,l 缩到再缩就不合法。判覆盖靠 formed == required,别去重排或重数整个窗口。

参考代码(Python)

Python
1def minWindow(self, s, t):
2    from collections import Counter
3    need = Counter(t)             # 每个字母还差几个
4    required = len(need)          # 要凑齐的字母种类数
5    formed = 0                    # 已凑齐的种类数
6    l = 0; best = (float(class="cl-str">"inf"), 0, 0)
7    for r in range(len(s)):       # 右指针右扩
8        c = s[r]; need[c] -= 1
9        if need[c] == 0: formed += 1   # 这种字母刚好凑齐
10        while formed == required:      # 已覆盖 → 左缩求最短
11            if r - l + 1 < best[0]: best = (r-l+1, l, r)
12            d = s[l]; need[d] += 1
13            if need[d] > 0: formed -= 1    # 丢掉必需字符,覆盖被破
14            l += 1
15    return class="cl-str">"" if best[0] == float(class="cl-str">"inf") else s[best[1]:best[2]+1]

复杂度分析

  • 时间复杂度:O(n) —— l 和 r 各自只向右走一遍,每个字符进窗口、出窗口各一次
  • 空间复杂度:O(字符集) —— need 最多存下 t 里的不同字母

套路模板

右指针只管扩;一旦合法就进 while 不停左缩、边缩边记最短,缩到不合法才退出。求「最短」就在 while 里记答案,求「最长」则在 while 外记。

Python
1# 求「最短合法窗口」通用骨架
2l = 0
3for r in range(len(s)):
4    把 s[r] 加入窗口(更新计数)
5    while 窗口已合法:        # 注意是 while 不是 if
6        更新最短答案(r - l + 1)
7        把 s[l] 移出窗口; l += 1

易错点

  • 错误写法:凭「窗口够长」就记答案,或随手重数整个窗口判覆盖正确写法:用 formed == required 判覆盖,恰好凑齐时才进 while 左缩并记最短(覆盖看的是「每种必需字母数量都够」,不是长度;formed 计数能 O(1) 判断,重数窗口会退化成 O(n·字符集))
  • 错误写法:收缩时只挪 l,不把 s[l] 加回 need / 不更新 formed正确写法:l 右移的同时 need[s[l]] += 1,若变正就 formed -= 1(丢掉的若是必需字符,覆盖已被打破,不回滚 formed 会继续错缩、答案偏短)

以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握

下一题 →438. 找到字符串中所有字母异位词 ← 返回题库