LeetCode 76. 最小覆盖子串 | C++ 滑动窗口高阶模板题解
📌 题目描述
题目级别:困难 (Hard)
给定两个字符串 s 和 t,长度分别是 m 和 n。返回 s 中的 最短窗口 子串,使得该子串包含 t 中的每一个字符(包括重复字符)。如果没有这样的子串,返回空字符串 ""。
测试用例保证答案唯一。
- 示例:
输入:s = "ADOBECODEBANC",t = "ABC"
输出:"BANC"
解释:最小覆盖子串"BANC"包含来自字符串t的'A'、'B'和'C'。
💡 解题思路:滑动窗口通用模板
寻找满足某种条件的"最短子串",是滑动窗口 的绝对主场。这道题的核心难点在于:如何快速判断当前窗口内是否已经凑齐了 t 中的所有字符?
我们引入两个哈希表 need 和 windows,以及一个极其巧妙的变量 valid:
need:记录字符串t中每个字符需要的数量。windows:记录当前滑动窗口中,这些目标字符出现的数量。valid:记录当前窗口中,已经满足数量要求的字符种类数 。当valid == need.size()时,说明窗口已经完全覆盖了t!
核心运作机制:两步走战略
-
步骤一:向右扩张(寻找可行解)
右指针
r不断向右移动,把新字符加入windows。如果加入的字符恰好是need中需要的,并且数量刚好达标,我们就让valid++。
目标:一直向右扩张,直到valid == need.size(),此时我们找到了一个"可行解"(虽然可能很长)。 -
步骤二:向左收缩(优化最优解)
一旦窗口满足了条件(
valid == need.size()),我们就暂存当前的子串长度和起始位置,并尝试逼近极限 。左指针
l开始向右移动,把字符从窗口中"吐出来"。如果吐出的字符恰好是关键字符,且导致窗口内该字符的数量低于了need的要求,那valid--,窗口不再合法。
目标:剥离边缘的无用字符,找到满足条件的"最短"长度。
窗口就在这样"扩张 -> 满足条件 -> 收缩 -> 不满足条件 -> 再次扩张"的博弈中,不断刷新最短记录,直到遍历完整个字符串。
💻 C++ 代码实现
cpp
class Solution {
public:
string minWindow(string s, string t) {
// need 记录目标字符串 t 中每个字符的需求量
// windows 记录当前窗口中对应字符的拥有量
unordered_map<char, int> windows, need;
for (char c : t) need[c]++;
int l = 0, r = 0;
int valid = 0; // 记录窗口中满足 need 条件的字符种类数
// 记录最小覆盖子串的起始索引及长度
int st = 0, len = INT_MAX;
while (r < s.size()) {
// c 是将移入窗口的字符,同时右指针向右移动
char c = s[r++];
// 进行窗口内数据的一系列更新
if (need.count(c)) {
windows[c]++;
// 当该字符的数量刚好达到目标要求时,满足条件的种类数 +1
if (windows[c] == need[c]) {
valid++;
}
}
// 判断左侧窗口是否要收缩 (当所有的字符种类都达标时)
while (valid == need.size()) {
// 在这里更新最小覆盖子串的记录
if (r - l < len) {
st = l;
len = r - l;
}
// d 是将移出窗口的字符,同时左指针向右移动
char d = s[l++];
// 进行窗口内数据的一系列更新
if (need.count(d)) {
// 如果移出的字符是关键字符,且移出后数量不达标了,种类数 -1
if (windows[d] == need[d]) {
valid--;
}
windows[d]--;
}
}
}
// 返回最小覆盖子串,如果没有找到则返回空字符串
return len == INT_MAX ? "" : s.substr(st, len);
}
};