76. 最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
- 对于
t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 - 如果
s中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入: s = “ADOBECODEBANC”, t = “ABC” 输出:“BANC” 解释: 最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、‘B’ 和 ‘C’。
示例 2:
输入: s = “a”, t = “a” 输出:“a” 解释: 整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = “a”, t = “aa” 输出: "" 解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.lengthn == t.length1 <= m, n <= 105s和t由英文字母组成
进阶: 你能设计一个在 o(m+n) 时间内解决此问题的算法吗?
class Solution {public: string minWindow(string S, string T) { vector<int> chars(128, 0); // 用来统计字符串T中每个字符的出现次数 vector<bool> flag(128, false); // 标记字符串T中哪些字符是有效的,即出现过的字符
// 统计字符串T中字符的情况 for (int i = 0; i < T.size(); ++i) { flag[T[i]] = true; // 将T中出现过的字符置为true ++chars[T[i]]; // 记录T中每个字符的出现次数 }
int cnt = 0, l = 0, min_l = 0, min_size = S.size() + 1; // cnt用来统计当前窗口中已经包含了T中的字符数 // l和r分别是滑动窗口的左右边界,min_l和min_size用来记录最小窗口的起始位置和长度 // min_size初始值设置为S的长度+1,确保能找到比S更短的子串
for (int r = 0; r < S.size(); ++r) { if (flag[S[r]]) { // 如果S中的当前字符在T中出现过 if (--chars[S[r]] >= 0) { // 更新chars中该字符的计数,并检查是否仍然大于等于0 ++cnt; // 如果仍然大于等于0,则表示当前窗口中包含了一个T中的字符 }
// 当cnt等于T的长度时,当前窗口已包含T中所有字符 while (cnt == T.size()) { // 尝试收缩左边界l,找到最短的子串 if (r - l + 1 < min_size) { min_l = l; // 更新最小子串的起始位置 min_size = r - l + 1; // 更新最小子串的长度 }
if (flag[S[l]] && ++chars[S[l]] > 0) { // 如果收缩左边界后导致当前窗口不再包含所有T中字符 --cnt; // 减少cnt } ++l; // 收缩左边界 } } }
// 返回最小子串,如果min_size仍然是初始值,则返回空字符串 return min_size > S.size() ? "" : S.substr(min_l, min_size); }};class Solution {public: unordered_map <char, int> ori, cnt;
bool check() { for (const auto &p: ori) { if (cnt[p.first] < p.second) { return false; } } return true; }
string minWindow(string s, string t) { for (const auto &c: t) { ++ori[c]; }
int l = 0, r = -1; int len = INT_MAX, ansL = -1, ansR = -1;
while (r < int(s.size())) { if (ori.find(s[++r]) != ori.end()) { ++cnt[s[r]]; } while (check() && l <= r) { if (r - l + 1 < len) { len = r - l + 1; ansL = l; } if (ori.find(s[l]) != ori.end()) { --cnt[s[l]]; } ++l; } }
return ansL == -1 ? string() : s.substr(ansL, len); }};本题使用滑动窗口求解,即两个指针 l 和 r 都是从最左端向最右端移动,且 l 的位置一定在r 的左边或重合。注意本题虽然在 for 循环里出现了一个 while 循环,但是因为 while 循环负责移动 l 指针,且 l 只会从左到右移动一次,因此总时间复杂度仍然是 O(n)。本题使用了长度为 128的数组来映射字符,也可以用哈希表替代;其中 chars 表示目前每个字符缺少的数量,flag 表示每个字符是否在 T 中存在。
方法二:滑动窗口 ![[Pasted image 20240703204712.png]] 右边界移动左边界不移动,左边界移动右边界不移动。先移动右边界
本问题要求我们返回字符串 s 中包含字符串 t 的全部字符的最小窗口。我们称包含 t 的全部字母的窗口为「可行」窗口。
我们可以用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的 r 指针,和一个用于「收缩」窗口的 l 指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 s 上滑动窗口,通过移动 r 指针不断扩张窗口。当窗口包含 t 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。
![[76_fig1.gif]]
如何判断当前的窗口包含所有 t 所需的字符呢?我们可以用一个哈希表表示 t 中所有的字符以及它们的个数,用一个哈希表动态维护窗口中所有的字符以及它们的个数,如果这个动态表中包含 t 的哈希表中的所有字符,并且对应的个数都不小于 t 的哈希表中各个字符的个数,那么当前的窗口是「可行」的。
注意:这里 t 中可能出现重复的字符,所以我们要记录字符的个数。
![[Pasted image 20240703213854.png]]
考虑如何优化? 如果 s=XX⋯XABCXXXX,t=ABC,那么显然 [XX⋯XABC] 是第一个得到的「可行」区间,得到这个可行区间后,我们按照「收缩」窗口的原则更新左边界,得到最小区间。我们其实做了一些无用的操作,就是更新右边界的时候「延伸」进了很多无用的 X,更新左边界的时候「收缩」扔掉了这些无用的 X,做了这么多无用的操作,只是为了得到短短的 ABC。没错,其实在 s 中,有的字符我们是不关心的,我们只关心 t 中出现的字符,我们可不可以先预处理 s,扔掉那些 t 中没有出现的字符,然后再做滑动窗口呢?也许你会说,这样可能出现 XXABXXC 的情况,在统计长度的时候可以扔掉前两个 X,但是不扔掉中间的 X,怎样解决这个问题呢?优化后的时空复杂度又是多少?这里代码给出没有优化的版本,以上的三个问题留给读者思考,欢迎大家在评论区给出答案哟。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






