一.题目要求
给你一个字符串 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 <=
1
0
5
10^5
105
s 和 t 由英文字母组成
进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?
四.解题思路
想到用滑动窗口但是没敲出来,直接看GPT解释吧。
- 初始化:创建两个哈希表,一个用于t的字符计数,另一个用于窗口中字符的计数。同时,设置两个指针left和right表示窗口的左右边界,以及变量来记录窗口的最小长度和起始位置。
- 扩展右边界:移动right指针扩大窗口,直到窗口包含了t中的所有字符。
- 收缩左边界:当窗口包含了所有t的字符后,尝试通过移动left指针来缩小窗口大小,同时更新最小子串的长度和起始位置。如果缩小窗口后窗口不再包含t的所有字符,则停止缩小窗口,并再次扩大右边界。
- 更新结果:在整个过程中,记录并更新满足条件的最小窗口的长度和起始位置。最后根据记录的起始位置和长度得到最小覆盖子串。
五.代码实现
bool mcheck(const unordered_map<char, int>& maps, const unordered_map<char, int>& mapp)
{
for (auto& pair : mapp)
{
auto it = maps.find(pair.first);
if (!maps.count(pair.first) || it->second < pair.second)
{
return false;
}
}
return true;
}
class Solution {
public:
string minWindow(string s, string t) {
if (t.length() > s.length()) return "";
int windowLen = INT_MAX;
int windowIndex = -1;
unordered_map<char, int> tmap,smap;
for (char c :t)
{
tmap[c]++;
}
for (string::iterator it = s.begin(); it < s.begin() + t.length(); it++)
{
smap[*it]++;
}
if (t.length() == s.length())
{
if (mcheck(smap, tmap)) return s;
else return "";
}
for (string::iterator itl = s.begin(), itr = s.begin() + t.length(); itr <= s.end();)
{
int len = itr - itl;
int index = itl - s.begin();
if (mcheck(smap, tmap))
{
if (len < windowLen)
{
windowLen = len;
windowIndex = index;
}
smap[*itl]--;
itl++;
}
else
{
if (itr == s.end()) break;
smap[*itr]++;
itr++;
}
}
if (windowIndex < 0) return "";
string ans(s, windowIndex, windowLen);
return ans;
}
};
六.题目总结
滑动窗口
滑动窗口是一种常用的算法技巧,特别适用于处理数组或字符串相关的问题。它可以用来解决一系列的问题,通常涉及寻找满足特定条件的连续数据块(如子数组或子字符串)。以下是滑动窗口通常可以解决的一些类型的问题,以及其背后的思想:
1. 最大/最小值问题
滑动窗口可以用来寻找数组或字符串中满足特定大小的窗口中的最大值或最小值。例如,给定一个整数数组和一个窗口大小,找出所有窗口中的最大值。
思想:维护一个双端队列,使队列中的元素递增(寻找最小值)或递减(寻找最大值),队头始终保持当前窗口的最大或最小值。
2. 子数组/子字符串问题
求解给定数组或字符串中满足特定条件的最长或最短子数组/子字符串的长度。例如,找出数组中和为特定值的最短子数组或包含所有指定字符的最短子字符串。
思想:扩展窗口直到满足条件,然后收缩窗口以寻找更小的符合条件的窗口,同时更新所需的结果。
3. 满足条件的子数组/子字符串数量
计算满足特定条件的子数组或子字符串的数量。例如,计算数组中所有和为特定值的子数组数量。
思想:移动窗口的右边界以包括更多元素,移动窗口的左边界以排除一些元素,过程中计算满足条件的子数组或子字符串的数量。
4. 字符串排列问题
找出字符串的一个排列是否是另一个字符串的子串。例如,给定两个字符串s1和s2,判断s1的排列之一是否为s2的子串。
思想:通过滑动窗口在s2中维护一个长度与s1相同的窗口,并比较窗口内字符的频率是否与s1中的字符频率相匹配。
滑动窗口的核心思想
滑动窗口算法的核心思想是维护一个窗口,并根据问题的需求,不断调整窗口的大小和位置。窗口可以是连续的数组/字符串的一部分,通过调整窗口的起始和结束位置来包括或排除元素,从而高效地逐步逼近问题的解决方案。
扩张:通常从窗口的右侧扩张,增加新的元素到窗口中,以满足特定的条件或寻求可能的解决方案。
收缩:从窗口的左侧收缩,排除不再需要的元素,通常是为了优化某些条件(如减小窗口大小,增加窗口内元素的总和)。
更新:在每一步窗口变化后,更新所需要维护的数据(如窗口内元素的总和、最大值/最小值、特定字符的计数等),并根据这些数据来更新最终的答案。
滑动窗口算法特别适用于数组和字符串问题,因为它可以将时间复杂度优化到线性时间,同时还可以通过简化问题的解决过程来减少不必要的计算,从而提高效率。