全栈开发知识库

33.最小覆盖子串

题目

给你一个字符串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
  • s 和 t 由英文字母组成

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

思路

题目中说如果 s 中存在这样的子串,我们保证它是唯一的答案。

整体策略: 双指针先扩张找到合法窗口,再收缩求最短:

  1. right不断右移扩张窗口,直到窗口覆盖了t的所有字符
  2. 一旦覆盖,left右移收缩窗口,在仍然覆盖的前提下尽可能缩短
  3. 收缩过程中不断记录最短的那个窗口
  4. 缩到不再覆盖时,回到第1步继续右移right,寻找下一个合法窗口

这就是求最短型滑动窗口的标准节奏: 右扩到合法,左缩到极限,全程记最短

核心难点: 如何O(1)判断窗口已覆盖t?

朴素做法是每次都把窗口的词频表和t的词频表整个对比一遍,但是这样每次判断都要O(字符集)的开销,拖慢整体。

关键的技巧: 引入valid计数器,把是否覆盖的判断降到O(1)

我们需要维护三样东西:

  • need : t中每个字符需要的数量
  • window : 当前窗口里每个字符的实际数量
  • valid : 已经数量达标的字符种类数

判断逻辑:

  • 当某字符c入窗后,恰好window[c] == needc,就valid++

  • 当valid == need.size(所有种类都达标),窗口就完整覆盖了t

这样是否覆盖只需要比较两个整数valid == need.size,O(1)完成

class Solution {
    fun minWindow(s: String, t: String): String {
        // 边界:s 比 t 还短,或 t 为空,不可能覆盖
        if (s.length < t.length || t.isEmpty()) {
            return ""
        }

        // need: t 中每个字符需要的数量(需求账本)
        val need = HashMap<Char, Int>()
        for (c in t) {
            need[c] = need.getOrDefault(c, 0) + 1
        }

        var left = 0
        // 当前窗口内每个字符的数量
        val window = HashMap<Char, Int>()
        // 已「数量达标」的字符种类数
        var valid = 0

        // 记录最优解:起点 start + 最短长度 minLen,因为left一直在变,而start是最优解时的left
        var start = 0
        var minLen = Int.MAX_VALUE

        for (right in s.indices) {
            // ===== 扩张:右边字符 c 入窗 =====
            val c = s[right]
            // 只关心 t 里有的字符
            if (need.containsKey(c)) {
                window[c] = window.getOrDefault(c, 0) + 1
                // 该字符「刚好」达标
                if (window[c] == need[c]) {
                    valid++
                }
            }

            // ===== 收缩:窗口已覆盖 t(valid 达标)就尝试缩短 =====
            while (valid == need.size) {
                // 收缩前先记录:此刻窗口是合法的,可能是更优解
                if (right - left + 1 < minLen) {
                    start = left
                    minLen = right - left + 1
                }

                // 移出最左字符 d
                val d = s[left]
                left++
                if (need.containsKey(d)) {
                    if (window[d] == need[d]) {
                        // 移出后将「跌破」达标线
                        valid--
                    }
                    window[d] = window[d]!! - 1
                }
            }
        }

        return if (minLen == Int.MAX_VALUE) "" else s.substring(start, start + minLen)
    }
}


  • 邮箱 :charon.chui@gmail.com
  • Good Luck!