76.最小覆盖字串

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);
    }
};

还需注意的点:

  1. res_len的设置:初始化时把 res_len 设成一个比 s 长度更大的值(或 INT_MAX),这样就能区分“根本没找到合法子串”和“最小子串刚好是整个 s”这两种情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值