76. 最小覆盖字串
思路
因为是跟着《代码随想录》刷题,所以根据分类确实是用滑动窗口做的。题目本质上是要求一个最短的区间 [l, r],这个区间包含 t 的所有字符。常见思路是右扩展窗口收集字符,再左收缩窗口尽可能缩短。
详细分析
用滑动窗口解决这类问题,一般采用以下思路:
class Solution {
public:
string minWindow(string s, string t) {
int begin = 0; // 记录左侧位置
for(int end = 0;end<s.length();++end){ // 移动右侧位置
while(符合条件){
//挪动左侧位置直到不符合条件
}
}
return 结果;
}
};
那针对这个问题,我们所说的条件就是“s 的子串涵盖 t 所有字符”,转为通俗描述就是“s 的某字串中每种字符的个数大于等于 t中的每种字串的个数”。用代码描述就如下:
int cnt_s[128]={0}; # s子串中每个字符的个数统计
int cnt_t[128]={0}; # t中每个字符的个数统计
注:这里开长度 128 的数组是为了直接用 char 作为下标,避免 s[i]-'A' 这种减法产生负数。
因为我们要在s中寻找,因此模板中的for循环对应着遍历s的end标志。
但我们需要对着结果在s中找,因此需要先对t中每个字符的个数统计,即:
for(int i = 0; i < t.length; ++i){
cnt_t[t[i]]++;
}
下面就会进入for循环部分,首先我们来看什么叫条件,即:
bool f = false; // 标识当前每个字符的个数是否都达到了t的要求
for(int i = 0; i < s.length; ++i){
if(cnt_s[s[i]]<cnt_t[s[i]]){
f = true;
break;
}
}
这段逻辑其实就是判断当前窗口是否覆盖了 t 的所有字符,可以抽象成一个 isValid() 函数。
这样子我们注意到,在每次移动end的时候,需要在cnt_s数组中进行记录,也就是在while前加:
cnt_s[s[end]]++;
进入合规条件后,我们需要移动左侧端点试图寻找更短的字串并记录:
if(cnt_s[s[begin]]-1<cnt_t[s[begin]]){
// 此时表示不符合条件,则需记录一下当前找到的字串,也就是s[begin:end]
length = end - begin + 1;// 记录以end结尾最短合规字串长度
if (length < res_length){
// 如果长度小于先前记录的最小长度则更新,则更新长度和起始位置
// 记录始末位置或者是初始位置长度是一样的,因为后面要用substr函数,因此这边采用初始位置和长度这种记录方式
res_length = length;
res_begin = begin;
}
break;//此时以end为结尾以及找到最短的字符串的,因此要继续挪动end
}
//此时表示仍符合条件,移动左侧端点,并在记录
begin++;
cnt_s[s[begin]]--;
此时的代码基本完成,但我们发现上面的isValid判断非常的繁复,因此就会想有什么方法可以简化,因为观察到里面做的也是对于s字符串的循环,我们就想能不能和模板中的if进行合并。
同时我们发现我们前面用了两个数组进行计数维护,因此想着能否也通过计数的方式来进行维护,最终想到了边遍历s数组,边统计cnt_s[s[end]] == cnt_t[s[end]]的个数,并用cnt变量进行统计。
但我们其实需要先获取t中一共有几种字符,也就是将预处理部分的代码稍微修改一下:
for(int i = 0; i < t.length; ++i){
if(cnt_t[t[i]]==0) cnt++;
cnt_t[t[i]]++;
}
也就是说,我们每找到一个符合cnt_s[s[end]] == cnt_t[s[end]] && cnt_t[s[end]]!=0的,就需cnt--,也就意味着找齐了一个字符,注意此处是==不可以写成>=,不然会重复统计一些统计过的字符,导致无法进入while。代码则是将前面的简单计数部分改为:
if(++cnt_s[s[end]] == cnt_t[s[end]] && cnt_t[s[end]]!=0){
cnt--;
}
这意味着 t 中所有字符的需求都已经满足,可以尝试收缩左边界,即while条件为cnt==0。
完整代码
下面则是优化后的完整代码:
class Solution {
public:
string minWindow(string s, string t) {
int cnt=0;
int cnt_t[128]={0};
for(int i=0;i<t.length();i++){
if(cnt_t[t[i]]==0)cnt++;
cnt_t[t[i]]++;
}
int begin=0;
int cnt_s[128]={0};
int res_len=s.length()+5;
int res_begin=0;
int length=0;
for(int end=0;end<s.length();end++){
if(cnt_t[s[end]]!=0&&++cnt_s[s[end]]==cnt_t[s[end]]) cnt--;
while(cnt==0){
if(cnt_s[s[begin]]-1<cnt_t[s[begin]]){
length=end-begin+1;
if(length<res_len){
res_len=length;
res_begin=begin;
}
break;
}
cnt_s[s[begin]]--;
begin++;
}
}
return res_len==s.length()+5?"":s.substr(res_begin,res_len);
}
};
还需注意的点:
- res_len的设置:初始化时把
res_len设成一个比 s 长度更大的值(或INT_MAX),这样就能区分“根本没找到合法子串”和“最小子串刚好是整个 s”这两种情况。

897

被折叠的 条评论
为什么被折叠?



