最小覆盖子串(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] 时计数)。
- left 和 right:窗口指针,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)

7万+

被折叠的 条评论
为什么被折叠?



