面试经典150题[033]:最小覆盖子串(LeetCode 76)

最小覆盖子串(LeetCode 76)

题目链接:最小覆盖子串(LeetCode 76)
难度:困难

1. 题目描述

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

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

要求:

  • m == s.length
  • n == t.length
  • 1 <= m, n <= 10^5
  • s 和 t 由英文字母组成

示例:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。

2. 问题分析

2.1 规律

  • 问题要求在 s 中找到最短的连续子串,该子串必须包含 t 中所有字符及其出现次数(允许额外字符)。
  • 如果 t 有重复字符,子串必须至少有相同数量的该字符。
  • 由于保证唯一答案(最短且最早出现),我们只需找到长度最小的窗口。
  • 核心问题:如何高效地在 s 上滑动窗口,动态维护覆盖 t 的最小范围?

2.2 贪心思路

我们使用滑动窗口算法(贪心优化):

  • need:用 Counter 或字典记录 t 中每个字符的所需计数。
  • window:记录当前窗口 [left, right] 中字符的计数。
  • valid:记录当前窗口中已满足 need 中字符数量的种类数(当 window[c] >= need[c] 时计数)。
  • leftright:窗口指针,right 扩展窗口,left 收缩窗口。

遍历过程:

  • 移动 right 扩展窗口,添加 s[right] 到 window。
  • 如果 s[right] 在 need 中,更新 window[c] += 1;若 window[c] == need[c],valid += 1。
  • 当 valid == len(need) 时,窗口有效,此时尝试收缩 left:
    • 更新最小长度和起始位置(如果当前窗口更短)。
    • 移除 s[left] 从 window,更新 window[d] -= 1;若 window[d] < need[d],valid -= 1。
    • left += 1,继续收缩直到窗口无效。
  • 继续扩展 right,直到遍历结束。
  • 如果找到有效窗口,返回 s[start:start+length];否则返回 “”。

这种贪心确保每次有效时都尽量缩小窗口,找到全局最小。

3. 代码实现

Python

from collections import Counter

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if not t:
            return ""
        
        need = Counter(t)
        window = Counter()
        left = 0
        valid = 0
        start, length = 0, float('inf')
        
        for right in range(len(s)):
            c = s[right]
            if c in need:
                window[c] += 1
                if window[c] == need[c]:
                    valid += 1
            
            while valid == len(need) and left <= right:
                if right - left + 1 < length:
                    start = left
                    length = right - left + 1
                
                d = s[left]
                if d in need:
                    if window[d] == need[d]:
                        valid -= 1
                    window[d] -= 1
                left += 1
        
        return s[start:start + length] if length != float('inf') else ""

C++

#include <string>
#include <unordered_map>
#include <climits>

class Solution {
public:
    std::string minWindow(std::string s, std::string t) {
        if (t.empty()) return "";
        
        std::unordered_map<char, int> need, window;
        for (char c : t) need[c]++;
        
        int left = 0, valid = 0;
        int start = 0, length = INT_MAX;
        
        for (int right = 0; right < s.size(); ++right) {
            char c = s[right];
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c]) {
                    valid++;
                }
            }
            
            while (valid == need.size() && left <= right) {
                if (right - left + 1 < length) {
                    start = left;
                    length = right - left + 1;
                }
                
                char d = s[left];
                if (need.count(d)) {
                    if (window[d] == need[d]) {
                        valid--;
                    }
                    window[d]--;
                }
                left++;
            }
        }
        
        return length == INT_MAX ? "" : s.substr(start, length);
    }
};

4. 复杂度分析

  • 时间复杂度:O(m + n),right 指针遍历 s 一次 O(m),left 指针最多遍历 m 次(总移动 O(m)),Counter 初始化 O(n)。
  • 空间复杂度:O(|\Sigma|),其中 \Sigma 是字符集大小(英文字母最多 52),用于 need 和 window。

5. 总结

  • 滑动窗口 + 最小覆盖 → 经典模板,适用于类似子串/子数组问题。
  • 核心维护 need、window 和 valid,很通用。
    • 类似 BFS 但优化为 O(m + n)。
    • 可扩展到其他计数覆盖问题,如排列包含等。

复习

面试经典150题[003]:删除有序数组中的重复项(LeetCode 26)
面试经典150题[018]:整数转罗马数字(LeetCode 12)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洪哥等风来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值