Leetcode 76:最小覆盖子串(最详细解决方案!!!)

给定一个字符串 S 和一个字符串 T,请在 S 中找出包含 T 所有字母的最小子串。

示例:

输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"

说明:

  • 如果 S 中不存这样的子串,则返回空字符串 ""
  • 如果 S 中存在这样的子串,我们保证它是唯一的答案。

解题思路

对于这个问题我们首先就会相同通过对撞指针的方法来解。这里我们有一个比较难解决的问题就是包含 T 所有字母的最小子串怎么表示?首先想到的做法时对于输入的s子串sW和输入的t通过collection.Counter分别统计字母个数,得到两个字母个数字典sW_counttW_count,然后遍历tW_count种的每个元素,判断它是不是在sW_count中,并且其元素个数不大于sW_count中的。于是我们可以写出这样的代码

def _minWindow(self, sW, tW):
    sW_count, tW_count = Counter(sW), Counter(tW)
    for i, val in tW_count.items():
        if i in sW_count and val <= sW_count[i]:
            continue
        return False
    return True

但是这种做法我们在解决问题的过程中,出现了超时。很容易理解,对于一个很长的字符串来说,这个字符串包含了很多的子串,而我们对每个子串进行统计,这里面实际上有大量的冗余。所以我们可以通过动态记录一个dict来做为我们的子串字典。

接着,我们建立两个指针lr分别指向窗口的左右,初始值赋为0t_len

A D O B E C O D E B A N C
↑   ↑
l   r

然后我们建立一个dict:sW_count,存储[l,r]这个区间内的元素个数。我们同时建立一个dict:tW_count,记录t中的元素个数。当[l,r]区间内的元素个数不小于t中的元素个数时,我们要缩小这个窗口,也就是l--,而区间内的元素小于t中元素的时候,我们又要将这个窗口放宽,也就是r++

from collections import Counter
class Solution:
    def minWindow(self, s, t):
        """
        :type s: str
        :type t: str
        :rtype: str
        """
        if not s or not t:
            return ''

        s_len, t_len = len(s), len(t)
        if t_len > s_len:
            return ''

        l, r, minLength, minL, minR = 0, t_len, s_len+1, 0, 0
        tW_count = Counter(t)
        sW_count = Counter(s[l:r])
        while l < s_len:   
            if self._minWindow(sW_count, tW_count):
                if minLength > r - l:
                    minLength = r - l
                    minL, minR = l, r
                if sW_count[s[l]] == 1:
                    sW_count.pop(s[l])
                else:
                    sW_count[s[l]] -= 1
                l += 1
            else:
                if r == s_len:
                    break
                sW_count[s[r]] = sW_count.get(s[r], 0) + 1
                r +=1 

        return s[minL:minR]

    def _minWindow(self, sW, tW):
        for i, val in tW.items():
            if i in sW and val <= sW[i]:
                continue
            return False
        return True

上面这种做法思路很清晰,但是不够优雅。其实这个问题和Leetcode 3:无重复字符的最长子串(最详细解决方案!!!) 是一类问题,都可以归纳为子串问题,这类问题最优美的解法就是Leetcode 3:无重复字符的最长子串(最详细解决方案!!!) 中的解法2,通过借助hash表来处理。

以题目中的例如为例,我们先建立滑动窗口,滑动窗口的边界是lr,我们的l初始化为0,而r对应当前s中遍历到元素的位置。

A D O B E C O D E B A N C
↑         ↑
l         r

mem:A0 B0 C0 D-1 E-1 O-1
t_len:0

我们首先建立一个可以容纳ASCII的字典mem,然后记录t中元素出现的次数。接着我们遍历s中的元素c,我们将字典mem中的c元素减一,如果s中的元素在t中出现过,那么我们记录此时的t_len = len(t)-1。当t_len==0时,说明此时t中的元素在s中都出现了,我们此时要进入下一步判断。

我们要判断mems[l]是不是小于0,如果是的话,说明t中不应该包含s[l],所以我们需要将它剔除出去(也就是mem[s[l++]]++)。否则的话,我们记录此时的lr。接着我们需要继续找符合条件的位置,我们将l++并且t_len++,将s[l]再加入到mem中去,这样我们就将窗口向右滑动一步

A D O B E C O D E B A N C
  ↑       ↑
  l       r

mem:A1 B0 C0 D-1 E-1 O-1
t_len:1

我们基于此就可以写出下面的代码

from collections import Counter
class Solution:
    def minWindow(self, s, t):
        mem = Counter(t)
        t_len = len(t)
        
        minL, minR = 0, float('inf')
        
        l = 0
        for r, c in enumerate(s):
            if mem[c] > 0:
                t_len -= 1
            mem[c] -= 1
                
            if t_len == 0:
                while mem[s[l]] < 0:
                    mem[s[l]] += 1
                    l += 1
                    
                if r - l < minR - minL:
                    minL, minR = l, r
                
                mem[s[l]] += 1
                t_len += 1
                l += 1
        
        return '' if minR == float('inf') else s[minL:minR+1]

这种写法比上面要简洁,速度要快(我们没有对字典进行插入和删除操作)。

该问题的其他语言版本添加到了我的GitHub Leetcode

如有问题,希望大家指出!!!

你可能感兴趣的:(Problems,leetcode解题指南)