【76. 最小覆盖子串】

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
  • st 由英文字母组成

进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?

思路

核心思路:滑动窗口

关键:

  • right++扩大窗口找可行解 ——> left++缩小窗口找更优解 ——> 判断该更优解是否可能是最优解并对应更新变量 ——> left++继续往后找更多可行解
  • 变量的更新时机:
    • 关于left和right的更新:

      • left更新:缩小窗口找更优解时,以及,找到更优解后继续开始找下一个可行解时
      • right更新:扩大窗口时
    • 关于need数组和needCnt的更新:

      • 窗口进一个字符就need[字符]–,如果该字符是窗口需要的字符就再needCnt–;
      • 窗口出一个字符就need[字符]++,如果该字符是窗口需要的字符就再needCnt++
    • 关于start和minlen的更新:

      • 找到一个更优解时,若窗口大小<minlen,就说明该更优解可能成为最优解,因此更新minlen和start

问题:

  1. 怎样判断窗口已经包含了t中所有字符?
  2. 缩小窗口找更优解怎么实现?

解答:

  1. 如果我们用两个vector来判断:

    • 两个vector分别存放窗口中字符频次和t中字符频次。
    • 那么每次判断窗口是否包含t中字符就需要遍历vector判断对应t中字符的出现频次与窗口中对应字符的出现频次是否相等。

    如果只用一个vector来判断:

    • 这个vector存放········的差值,即窗口中每个t中字符还需要出现多少次。
    • 那么每次仍需要遍历这个vector判断对应t中字符的出现频次是否为0。

    如果只用一个变量needCnt来判断:

    • 这个变量表示窗口中还需要出现的t中字符的总个数

    • 那么每次只需要根据needCnt是否为0,就能直接判断窗口是否包含了t中的所有字符。

      ❓ ❓❓ 为什么能直接根据needCnt判断窗口是否已经包含了t中的所有字符?

      • 只有在窗口进来需要的字符(need[字符]>0)时才会让needCnt–,如果need[字符]<=0就不会让needCnt–,因此不会误判。每次让needCnt–的字符都一定是窗口还需要的t中字符。
  2. 如果need[s[left]] < 0就一直缩小窗口,直到遇到必须包含的字母。因为:

    窗口每进一个字符就会need[字符]–,need[s[left]]<0就说明s[left]是窗口本不需要包含的字符(包括本就不属于t、属于t但所需个数多余了的字符),就可以直接跳过。

步骤:

  1. right不断右移直到窗口包含所有t中的字母,找到一个可行解
  2. left++不断右移缩小窗口直到遇到必须包含的字母,找到一个最优解
  3. 找到一个更优解之后,判断该更优解是否可能是最优解。若窗口大小<minlen,就说明该更优解可能成为最优解,更新与结果相关的变量
  4. left右移找下一个可行解。重复执行上述步骤直至s末尾。

代码

无注释版

string minWindow(string s, string t) {
    vector<int> need(128);
    for (auto& c : t) need[c]++; 

    int left = 0, right = 0;
    int start = 0, minlen = INT_MAX; 
    int needCnt = t.size(); 

    while (right < s.size()) {
        if (need[s[right++]]-- > 0) {  
            if (--needCnt == 0) {  
                while (need[s[left]] < 0) need[s[left++]]++; 
                if (right - left < minlen) { 
                    minlen = right - left;
                    start = left;
                }
                need[s[left++]]++;
                needCnt++;
            }
        }
    }
    return minlen == INT_MAX ? "" : s.substr(start, minlen);
}

带注释版

string minWindow(string s, string t) {
    vector<int> need(128);
    for (auto& c : t) need[c]++;//记录t中字符频次

    int left = 0, right = 0;
    int start = 0, minlen = INT_MAX;//表示满足条件的最小子串的首位和长度,便于最后substr截取子串
    int needCnt = t.size();//窗口需要包含的t中字符数

    while (right < s.size()) {
        //right++扩大窗口
        //如果s[right]不是窗口需要的t中字符,则直接做完if(need[s[right++]]-- > 0)判断就走下一循环继续right++扩大窗口了
        if (need[s[right++]]-- > 0) { //说明窗口需要s[right]
            if (--needCnt == 0) { //说明窗口包含了所有t中字符,找到了一个可行解
                //接下来找更优解
                while (need[s[left]] < 0) need[s[left++]]++;//left++缩小窗口直到遇到必须包含的字母
                if (right - left < minlen) { //若窗口大小<minlen,就说明该更优解可能成为最优解,更新与结果相关的变量
                    minlen = right - left;
                    start = left;
                }
                //left++使当前 更优解窗口 不再满足条件,继续找下一个可行解。
                //同时因为left++前s[left]一定是窗口需要包含的字母,因此needCnt也要记得++
                need[s[left++]]++;
                needCnt++;
            }
        }
    }
    return minlen == INT_MAX ? "" : s.substr(start, minlen);
}

复杂度分析

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

  • 19
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用 Kotlin 实现的解法: ```kotlin fun minWindow(s: String, t: String): String { val map = mutableMapOf<Char, Int>() // 存储 t 中每个字符出现的次数 var left = 0 // 左指针 var right = 0 // 右指针 var count = 0 // 统计当前已经匹配的字符数 var minLen = Int.MAX_VALUE // 最小覆盖的长度 var minStr = "" // 最小覆盖 // 初始化 map for (c in t) { map[c] = map.getOrDefault(c, 0) + 1 } // 遍历 s while (right < s.length) { val c1 = s[right] if (map.containsKey(c1)) { map[c1] = map[c1]!! - 1 if (map[c1]!! >= 0) { // 当前字符匹配成功 count++ } } right++ // 当前窗口已包含 t 中所有字符,尝试收缩窗口 while (count == t.length) { if (right - left < minLen) { // 更新最小覆盖 minLen = right - left minStr = s.substring(left, right) } val c2 = s[left] if (map.containsKey(c2)) { map[c2] = map[c2]!! + 1 if (map[c2]!! > 0) { // 当前字符匹配失败 count-- } } left++ } } return minStr } ``` 解法思路: - 使用一个 map 存储 t 中每个字符出现的次数; - 使用双指针 left 和 right 分别表示窗口的左右边界,初始化均为 0; - 遍历 s,当遍历到的字符属于 t 中的字符时,将其出现次数减一,并统计当前已经匹配的字符数; - 当当前窗口已包含 t 中所有字符时,尝试收缩窗口: - 如果当前窗口长度小于最小覆盖的长度,更新最小覆盖; - 将 left 指向的字符出现次数加一,并判断当前字符是否匹配成功; - 如果当前字符匹配失败,重新开始扩展窗口,继续寻找下一个最小覆盖。 - 最后返回最小覆盖

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值