题目描述
给字符串 s 和 t,在 s 里找出最短的子串,使它包含 t 的所有字母(含重复次数)。找不到返回空串。
思路解析
把每个起点、每个终点都试一遍当然能做,但 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)
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 外记。
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 会继续错缩、答案偏短)
以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握。