题目网址:https://leetcode-cn.com/problems/minimum-window-substring/
题目分析
这道题目,明显之处在于,我们需要在字符串 s 中框出一个窗口,来判断这个窗口中的子串是否覆盖了 t,如下图所示。就是要判断黄色窗口内的子串,有没有覆盖字符串 t。

由此一来,我们需要解决两个很重要的问题。一是如何判断黄色窗口子串有没有覆盖 t,二是如何得到滑动窗口。
如何判断是否覆盖
假设我们有两个字符串,分别为 s1 = "OBECOEB"(上图中的黄色区域)和 t = "ABC"。如何判断 s1 有没有覆盖 t 呢?
我们可以分别统计字符串 s1 和 t 中各字符的个数,得到下图。我们可以看到,t 中有 1 个 A,而 s1 中没有,因此 s1 不能覆盖 t。

再比如,有字符串 s2 = "BANC",t 包含的字符均在 s2 中出现,且 t 中各字符的数量,都小于等于 s2 中相应字符的数量。因此,s2 覆盖了 t。
并且,我们只需要关注那些出现在 t 中的字符,可以忽视掉那些不出现在 t 中的字符。比如,s1 中的 E 和 O,它没有出现在 t 中,我们可以忽略它。同时为了节省空间,我们实际上保存好 t 和 s1 各字符个数的差值就好。
我们只关心出现在 t 中的字符,把 t 中这些字符的个数减去 s1 中相应字符的个数,结果如下图。字符 A 的结果是正数,说明还有一个 A 没有被覆盖,因此字符串 s1 未能覆盖 t。

但是,窗口是在不断变化的。如果每次变化窗口,都需要重新计算窗口中各字符的数量,且都要与 t 中相应字符的数量做差,这样做是很低效的。但事实上并非如此。
假设我们已经有了一个滑动窗口,窗口左、右下标分别为 l 和 r。我们可以观察到,无论是向右移动 l 还是 r,都是只有一个字符的改变。例如,把 l 向右移动一位,字符 O 减少一个。实际上,我们只要得到一个窗口的各字符数量与 t 的差值,就可以快速地得到下一个窗口与 t 各字符数量的差值。
再进一步。
现在我们可以得到 t 与各个窗口中字符的差值。这是不是意味着我们每得到一个窗口,都需要来看看差值中每个数都小于等于 0?
其实,我们可以使用空间换时间的方法,再用一个变量 cnt 来表示覆盖的数量。还是字符串 s1 = "OBECOEB" 和 t = "ABC",s1 实际上只覆盖 t 的 B 和 C 两个字符,此时 cnt 等于 2,小于 t 的长度。很明显,s1 未能覆盖 t。
而窗口 s2 = "BANC" 的 cnt 为 3,恰好等于 t 的长度,因此 s2 覆盖了 t。
到这里,我们已经解决了第一个问题,如何判断子串有没有覆盖。接下来解决第二个问题,如何得到滑动窗口。
滑动窗口
同样,我们用下标 l 和 r 来表示区间 [l, r] 中的子串。当然,我们可以枚举,把所有可能的子串都枚举出来,但这没有必要。
我们让 l 和 r 都从 0 出发,并且可以得到字符差值,如下图所示。

此时 cnt 为 1,只满足了一个 A,不满足覆盖的条件。当不满足覆盖条件时,我们要尽可能地扩大窗口,也即让下标 r 向右移动。
当下标 r 向右移动一位,指向字符 D,D 并没有出现在字符串 t 中,它是我们不关心的字符,我们可以直接忽略,继续向下移动,遇到字符 O 时同样忽略。
当 r 向右移动指向 B 时,如下图。此时 cnt 为 2,也不满足覆盖条件,继续向右移动 r,直到指向 C。这时 cnt 等于 t 的长度,满足覆盖条件。

当 r 指向 C 时,如下图所示。此时 cnt 等于 t 的长度,我们找到了一个答案。我们的目的是找到更短地字符串,因此找到答案之后,我们要缩小窗口,即把 l 向右移动。

当 l 向右移动一位,指向字符 D,由于失去了字符 A,因此 cnt 减为 2。实际上,D 不在字符串 t 中,我们可以忽略,同理忽略 O,l 一直向右移动指向 B,如下图所示。

此时 cnt 2,不满足覆盖条件,我们继续把 r 向后移动,如下图。

这时的 r 指向 B,虽然 B 在字符串 t 中,但窗口中 B 的个数超过了 t 中 B 的个数,而 A 又没有覆盖,因此 cnt 不用修改,还是 2。r 继续向后移动。

这个时候 cnt 等于 3,满足覆盖条件了,因此 l 向右移动。这时与上一次满足条件时的情况略有不同,这次 cnt 无需减 1。因为在差值中,B 为 -1,表示窗口中的子串相比于 t 多了一个 B。虽然,l 向右移动少了一个 B,但依旧满足覆盖条件。因此,l 只需向右移动,且 cnt 无需减 1。在这两步中,体现了差值的作用。
当 l 向右移动到下图,此时 cnt 依旧等于 3,满足条件,l 继续向右移动。

当 l 向下移动到这个情况时,cnt 等于 2,不满足条件,r 向后移动。

此时又满足了覆盖条件,l 继续向后移动,直到字符串末尾,程序结束。

综上所述,我们可以借用字符差值和 cnt 来判断覆盖情况。当不满足覆盖条件时,r 向右移动。当满足覆盖条件时,l 向右移动。现在进行代码实现。
代码实现
这里使用 C++ 实现,可以利用代码中的注释进行辅助理解,在实现细节上可能与上述过程略有出入。
class Solution {
public:
string minWindow(string s, string t) {
unordered_set<char> st; // 字符 t 中的字符
unordered_map<char, int> hash; // 字符 t 中各字符的数量
for (const char & c : t)
{
hash[c]++;
st.insert(c);
}
// 记录答案
string ans = "";
int min_len = s.length() + 1;
// 左边界 l,覆盖个数 cnt
int l = 0, cnt = 0;
// 不满足覆盖条件时,r 向后移动
for (int r = 0; r < s.length(); r++)
{
// 如果字符在 t 中,则执行;不存在则跳过
if (st.find(s[r]) != st.end())
{
// 修改差值和 cnt
hash[s[r]]--;
if (hash[s[r]] >= 0)
cnt++; // 属于覆盖条件,cnt++;当差值小于 0 的时候,说明这个字母是多出来的,不用 cnt++
// 如果满足答案
while (cnt == t.length())
{
// 如果此时答案更佳,则修改答案
if (r - l + 1 < min_len)
{
min_len = r - l + 1;
ans = s.substr(l, min_len);
}
// l 要准备向后移动一位,修改差值 和 cnt
hash[s[l]]++;
if (st.find(s[l]) != st.end() && hash[s[l]] > 0)
cnt--; // 当差值大于 0 的时候,说明这个字符无法覆盖了,cnt--
l++; // 满足条件,l 向后移动一位
}
}
}
return ans;
}
};









