76. 最小覆盖子串(Minimum Window Substring)

难度:困难 | 主题:滑动窗口、哈希表、字符串

题目

给你一个字符串 s 和一个字符串 t,返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串。

示例:

 
输入:s = "ADOBECODEBANC", t = "ABC"
 
输出:"BANC"
 
解释:覆盖 "ABC" 的最小子串是 "BANC"
 

思路

先讲个故事:赶集凑齐摊位

你去赶集,要买齐清单(t)上的所有商品。你在集市(s)上从左往右走,每走过一个摊位就把商品加入购物车。

购物车凑齐了?记下当前位置,然后试试从左边扔掉一些摊位——扔掉后还齐全吗?还齐全就继续扔,直到不够了停。这就是当前最优解。

一直走到集市尽头,最优解就是最短购物路线。


引导式推导:欠账计数法

need 统计 t 中每个字符还缺多少,need_cnt 记录还差几种字符。

 
s = "ADOBECODEBANC", t = "ABC"
 
need = {A:1, B:1, C:1}, need_cnt = 3
 
r=0, 'A': need[A]=0, need_cnt=2
 
r=1, 'D': 不在 need 中
 
r=2, 'O': 不在 need 中
 
r=3, 'B': need[B]=0, need_cnt=1
 
r=4, 'E': 不在 need 中
 
r=5, 'C': need[C]=0, need_cnt=0 → 窗口已覆盖!
 
  记录 [0,5],长度 6
 
  收缩: l=0 'A' 出窗, need[A]=1, need_cnt=1 → 不够了,停
 
r=6, 'O': 不在 need 中
 
r=7, 'D': 不在 need 中
 
r=8, 'E': 不在 need 中
 
r=9, 'B': need[B]=-1, need_cnt 不变(负值不减 need_cnt)
 
  收缩: l=1 'D' → l=2 'O' → l=3 'B' 出窗, need[B]=0, need_cnt=1
 
  → 不够了,停
 
r=10, 'A': need[A]=0, need_cnt=0 → 窗口已覆盖!
 
  记录 [4,10],长度 7
 
  收缩: l=4 'C' 出窗, need[C]=1, need_cnt=1 → 不够了,停
 
r=11, 'N': 不在 need 中
 
r=12, 'C': need[C]=0, need_cnt=0 → 窗口已覆盖!
 
  记录 [9,12],长度 4 ← 最优!
 

graph TD

    subgraph 窗口扩缩过程

        A["l=0, 扩到 r=5<br/>'ADOBEC' 覆盖 ✓<br/>长度6"]

        B["收缩: l=0'A'出窗<br/>need_cnt=1, 停"]

        C["l=4, 扩到 r=10<br/>'CODEBANC' 覆盖 ✓<br/>长度7"]

        D["收缩: l=4'C'出窗<br/>need_cnt=1, 停"]

        E["l=9, 扩到 r=12<br/>'BANC' 覆盖 ✓<br/>长度4 ← 最优"]

    end

    A --> B --> C --> D --> E

    style E fill:#9cf,stroke:#333

为什么 need[c] >= 0 时 need_cnt 才减? 因为负值表示某个字符多了,不减少欠账。


代码

 
def minWindow(self, s, t):
 
    from collections import Counter
 
    need = Counter(t)           # t 中各字符需要的次数
 
    window = {}                 # 当前窗口内各字符的次数
 
    left = 0
 
    formed = 0                  # 窗口内已满足 need 要求的字符种类数
 
    ans = float('inf'), None, None  # (长度, 左边界, 右边界)
 
    for right in range(len(s)):
 
        char = s[right]
 
        window[char] = window.get(char, 0) + 1  # 右指针字符加入窗口
 
        # 如果该字符在 need 中,且窗口内该字符次数正好满足 need 要求
 
        if char in need and window[char] == need[char]:
 
            formed += 1
 
        # 当窗口已包含 t 中所有字符时,尝试收缩左边界
 
        while formed == len(need):
 
            if right - left + 1 < ans[0]:
 
                ans = (right - left + 1, left, right)  # 更新最短窗口
 
            char_left = s[left]
 
            window[char_left] -= 1   # 左指针字符移出窗口
 
            if char_left in need and window[char_left] < need[char_left]:
 
                formed -= 1          # 移出后不满足 need 要求
 
            left += 1
 
    if ans[1] is None:
 
        return ""               # 没找到
 
    return s[ans[1]:ans[2] + 1] # 切片右边界要 +1
 

复杂度

指标解释
时间O(n)左右指针各遍历 s 一次
空间O(Σ

实战考量

频率分析

出现在:滑动窗口困难题,常见,考察对不定长窗口和状态维护的精细控制。字节/美团/腾讯压轴题,约 15% 的高难度常会出不定长窗口题。

延伸思考

Q:如果 t 里有重复字符,怎么保证窗口内也正好有那么多?

A:window[char] == need[char] 时才 formed += 1,多出来的不算。

Q:如果 s 很长但 t 很短,有优化空间吗?

A:先用 t 构建 need,只关注 need 里的字符,其他字符不影响 formed。

Q:如果要求覆盖顺序也一致呢?

A:变成子序列问题,用动态规划。

Q:收缩条件为什么是 formed == len(need)

A:formed 记录的是「满足次数要求的字符种类数」,不是总字符数。当所有种类都满足时窗口才覆盖 t。

Q:切片为什么右边界要 +1?

A:Python 切片是左闭右开区间,s[left:right+1] 才能包含 right 位置的字符。

易错点

  • formed 是种类数不是总数

  • 收缩用 while 不是 if

  • 切片右边界 +1

  • 负值不影响 formed(字符多了不算满足)


生活类比

最小覆盖子串 → 赶集凑齐摊位

你拿着购物清单在集市上走,从左往右扫过摊位。

购物车凑齐了?记下位置,然后从左边开始扔——扔到不够了停,这就是当前最优。

一直走到尽头,最短的那段就是答案。

不定长窗口就像”贪心购物”——凑够了就记,精简了再记,永远找最短路线。


相关题目

题目关系
438找到字符串中所有字母异位词固定窗口版
567字符串的排列固定窗口判断
03无重复字符的最长子串另一类滑动窗口

→ 返回题单:LeetCode学习路线图 > 九、滑动窗口

速记卡(面试闪卡)

Q1:一句话讲清「76. 最小覆盖子串(Minimum Window Substring)」到底是什么?

A:在长串 s 里找能盖住短串 t 全部字符的最短窗口,用左右双指针不定长滑动窗口实现。

Q2:一、题目 —— 怎么理解?

A:给你串 s 和 t,返回 s 里涵盖 t 所有字符的最小子串,找不到就返回空串。像拿购物清单在集市上走,要凑齐所有商品、还走最短路线,这就是最小覆盖窗口 (Minimum Window)

Q3:二、思路:欠账计数法 —— 怎么理解?

A:用 need 记 t 中每种字符还缺多少,formed 记已满足的种类数。右指针一路加商品,凑齐了就记位置,左指针开始扔——扔到不够为止。这叫滑动窗口 (Sliding Window),多出来的字符算负账、不减种类。

Q4:三、代码实现要点 —— 怎么理解?

A:formed == len(need)(种类数而非总数)才收缩左边界;左指针移出后若不满足 need 就让 formed 减一。结果用 s[left:right+1] 切片,右边界要 +1(Python 左闭右开)。负数表示字符多了,不影响 formed。

Q5:四、复杂度与实战考量 —— 怎么理解?

A:时间 O(n):左右指针各扫 s 一遍;空间 O(Σ) 是字符集大小的哈希表。它是字节/美团/腾讯压轴困难题,约 15% 高难度爱考不定长窗口。面试 (interview) 延伸:t 有重复字符、要求顺序一致变子序列。

Q6:核心速记主线有哪些?

  • 题目:s 中涵盖 t 所有字符的最短子串,无则空串

  • 思路:need 欠账计数,右扩左缩找最短覆盖窗口

  • 代码:formed 计种类数,收缩用 while,切片右+1

  • 复杂度:时间 O(n)、空间 O(Σ)

  • 实战:字节/美团/腾讯压轴困难,引出滑动窗口系列

口诀

A:最小窗口滑动找,

欠账计数要记牢;

右扩左缩不断调,

覆盖全部心不焦。

相关链接