#76 可变滑窗 困难

最小覆盖子串

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

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

示例 1

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

💭 模拟答题者思考

1. 暴力:枚举所有子串检查是否覆盖 t。要滑动窗口就必须回答「可行」(cover)→「比长度」。

2. 右指针 r 扩张到覆盖为止,一旦覆盖就尝试收左指针 l 缩到最短。

3. 需要 O(1) 判断是否覆盖,所以引入计数表 need/window 和 valid 计数。

4. 当 valid == distinct_chars_in_t 时,当前窗口 [l,r) 是一个可行解,尝试缩小 l 找更短。

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
need[c]map<char,int>定义:目标串 t 对字符 c 的需求次数
维护:不变量,初始化为 t 的字符频率
更新:不更新
window[c]map<char,int>定义:当前窗口内字符 c 的计数
维护:随窗口滑动实时反映窗口内容
更新:窗口右扩时 window[c]++,左缩时 window[c]--
validint定义:已满足需求的字符种类数
维护:每轮后 valid = 满足 window[c] >= need[c] 的字符 c 的数量
更新:当 window[c] 刚好等于 need[c](从 < 变成 =)时 valid++

⌨️ 落码步骤

1. 统计 need:遍历 t,对每个字符 need[c]++

2. 右指针 r 扩张:window[s[r]]++,若 window[s[r]] == need[s[r]] 则 valid++

3. 当 valid == len(need)(全部满足),循环收缩左指针:更新答案,window[s[l]]--,若 window[s[l]] < need[s[l]] 则 valid--,l++

4. 返回最短子串或空串

💻 代码实现

from collections import Counter

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        need = Counter(t)
        need_types = len(need)
        window = Counter()
        valid = 0
        l = 0
        start, min_len = 0, float('inf')

        for r in range(len(s)):
            c = s[r]
            window[c] += 1
            if window[c] == need[c]:
                valid += 1

            while valid == need_types:
                if r - l + 1 < min_len:
                    start, min_len = l, r - l + 1

                d = s[l]
                if window[d] == need[d]:
                    valid -= 1
                window[d] -= 1
                l += 1

        return s[start:start+min_len] if min_len != float('inf') else ''
class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map need, window;
        for (char c : t) need[c]++;

        int valid = 0;
        int l = 0, start = 0, min_len = INT_MAX;

        for (int r = 0; r < s.size(); r++) {
            char c = s[r];
            window[c]++;
            if (need.count(c) && window[c] == need[c])
                valid++;

            while (valid == need.size()) {
                if (r - l + 1 < min_len) {
                    start = l;
                    min_len = r - l + 1;
                }
                char d = s[l];
                if (need.count(d) && window[d] == need[d])
                    valid--;
                window[d]--;
                l++;
            }
        }
        return min_len == INT_MAX ? "" : s.substr(start, min_len);
    }
};
// 时间 O(m+n),空间 O(|Σ|)

📈 复杂度分析

时间复杂度 O(m+n)
空间复杂度 O(|Σ|)

⚠️ 常见坑

valid 计数逻辑:必须 window[c] 刚好等于 need[c] 时才 valid++,超过不算(否则重复字符会虚增 valid)。

收缩时更新顺序:先判断 window[d]==need[d] 再 window[d]--,和扩张时顺序相同。

返回空串的判断:min_len 仍是 INF 说明从未形成合法窗口。

🔍 必测边界 Case

Case 1:s 和 t 相同
s = "ABC", t = "ABC" → 输出 "ABC"
Case 2:t 比 s 长
s = "A", t = "AB" → 输出 ""
Case 3:t 有重复字符
s = "AAB", t = "AA" → 输出 "AA"