mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1231 字
3 分钟
76. 最小覆盖子串
2024-07-03

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.length
  • n == t.length
  • 1 <= m, n <= 105
  • s 和 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,怎样解决这个问题呢?优化后的时空复杂度又是多少?这里代码给出没有优化的版本,以上的三个问题留给读者思考,欢迎大家在评论区给出答案哟。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

76. 最小覆盖子串
https://xjrhoshi.github.io/posts/2024-07-03-76-最小覆盖子串/
作者
星间润
发布于
2024-07-03
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录