算法题10:最小覆盖子串

题目描述:

给你一个字符串 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 的子串中,
因此没有符合条件的子字符串,返回空字符串。

 思路:

开始时想到滑动窗口,但是实现方式不对

class Solution:
    

    def minWindow(self, s: str, t: str) -> str:
        res = ""
        s_n = len(s)
        t_n = len(t)
        if s == t:
            return s
        for i in range(s_n):
            right = i+t_n
            while right < s_n + 1:
                s1 = s[i:right]
                if self.checkIn(s1, t):
                    res = s[i:right]
                    break
                right += 1
        return res

    def checkIn(self, s, t):
        flag = True
        for i in t:
            if i not in s:
                flag = False
                return flag
        return flag
    

结果没通过,看灵茶山艾府的解题,相对官方解题更简洁易懂一些:

涵盖:

这里用到一个概念“涵盖”,示例中s的字串BANC中没字母出现次数都大于等于t=ABC中每个字母的出现次数,这就叫涵盖。

 

滑动窗口怎么滑

枚举s子串的右端点right(子串最后一个字母的下标),如果子串涵盖t,就不断移动左端点left知道不涵盖位置。在移动过程中更新最短子串的左右断点。

具体步骤:

1、初始化ansLeft = -1, ansRight = m, 用来记录最短子串的左右端点,其中m是s的长度。看起来就是左右端点都初始化为最右侧一个字母的位置。

2、用一个哈希表(或数组)cntT统计t中每个字母的出现次数。

3、初始化left = 0, 以及一个空哈希表(或数组)cntS,用来统计s子串中每个字母的出现次数,用来判断是否涵盖。

4、遍历s,设当前枚举的子串右端点为right,把s[right]的出现次数加一。窗口右端点向右移动。

5、遍历cntS中每个字母及其出现次数,如果出现次数都大于等于cntT中的字母出现次数,满足涵盖条件则判断:

  a.如果right - left < ansRight - ansLeft,说明找到了更短的子串,更新ansLeft = left, ansRight = right。

  b.把s[left]的出现次数减一。窗口左侧端点向右移动后对应的次数减一。

  c.左端点右移,即left加一。窗口左侧端点向右移动。

  d.重复上述三步,知道cntS有字母的出现次数小于cntT中该字母出现的次数位置。即涵盖条件不满足。

 

6、最后,如果ansLeft < 0,说明没有找到符合要求的子串,返回空字符串,否则返回下标ansLeft到下标ansRight之间的子串。

 python

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        ans_left, ans_right = -1, len(s)
        cnt_s = Counter() # s子串字母的出现次数
        cnt_t = Counter(t) # t中字母的出现次数

        left = 0
        for right, c in enumerate(s): # 移动子串右端点
            cnt_s[c] += 1 # 右端点字母移入子串
            while cnt_s >= cnt_t: # 涵盖,这里体现出python的简单 整个Counter()对象可以对比
                if right - left < ans_right - ans_left: # 找到更短子串
                    ans_left, ans_right = left, right # 记录此时的左右端点
                cnt_s[s[left]] -= 1 # 左端点字母移出子串
                left += 1 # 左端点右移
        return "" if ans_left < 0 else s[ans_left :  ans_right + 1]

结果:

 

java:

 

class Solution {
    public String minWindow(String S, String t) {
        char[] s = S.toCharArray();
        int m = s.length;
        int ansLeft = -1;
        int ansRight = m;
        int [] cntS = new int[128]; // s子串字母的出现次数
        int [] cntT = new int[128]; // t中字母的出现次数
        for (char c : t.toCharArray()){
            cntT[c] ++;
        }
        int left = 0;
        for (int right = 0; right < m; right ++) { // 移动子串右端点
            cntS[s[right]]++; // 右端点字母移入窗口(子串)
            while (isCovered(cntS, cntT)) { // 涵盖
                if (right - left < ansRight - ansLeft) { //找到更短的子串
                    ansLeft = left; // 记录次数的左右端点
                    ansRight = right; 
                }
               
                cntS[s[left]] --; // 左端点字母移出子串(窗口)
                left++;
            }
           
        }
        return ansLeft < 0 ? "" : S.substring(ansLeft, ansRight + 1);
    }
    private boolean isCovered(int[] cntS, int[] cntT) {
        for (int i = 'A'; i <= 'Z'; i++) {
            if (cntS[i] < cntT[i]) {
                return false;
            }
        }
        for (int i = 'a'; i <= 'z'; i++) {
            if (cntS[i] < cntT[i]) {
                return false;
            }
        }
        return true;
    }
    
    
}

结果:

 

posted @ 2025-05-20 08:51  夏晓旭  阅读(53)  评论(0)    收藏  举报