最小覆盖子串 滑动窗口

76. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1

输入:s = "ADOBECODEBANC", t = "ABC"

输出:"BANC"

示例 2

输入:s = "a", t = "a"

输出:"a"

示例 3:

输入: s = "a", t = "aa"

输出: ""

解释: t 中两个字符 'a' 均应包含在 s 的子串中,

因此没有符合条件的子字符串,返回空字符串。

思路:

  滑动窗口!滑动窗口!滑动窗口!找“子串”问题,我们十有八九就用滑动窗口。滑动窗口是一种非常常用的处理思路,包括其代码框架(看似很多很复杂,实际上万变不离其宗!用多了就是一个样)。能用滑动窗口解决的经典题目大概只有有五六个,但是他们又很高频!所以基本上都做过一遍后,一见到这个题,就知道怎么做了:开滑!

  等待,什么是滑动窗口?就是一个宽度可以改变、位置可以改变且初始化在最左边的区间

  滑动窗口的滑动方式是固定的。就像“毛毛虫爬行“,先是右边界右移,窗口长度拉长;再是左边界右移,窗口长度缩小。重复这个过程直到窗口抵达最右边。

  为什么要以这样的方式去滑动呢?结合这道题我们就理解了。我们首先定义一个长度为0的滑动窗口,在字符串的最左边,显然现在它包含0个字符,现在我们扩大右边界(右边界右移),扩大到多少呢——使得窗口包含题目要求的子串,一旦满足这个要求,我们就记录下这个窗口的初步长度;然后保持右边界不动,开始缩小左边界(左边界右移),缩小到多少呢——使得窗口恰好不满足包含题目要求的子串,而在此之前,每一次缩小,我们都记录和更新这个最小区间。缩小结束,我们再次扩大右边界……重复这个过程直到窗口抵达最右边。

  可以看到,滑动窗口的思路就是以一个很聪明的(复杂度非常低的)方式去完成对字符串的遍历,不断地通过扩大右边界来初步满足结果,再通过缩小左边界来逼近最优解

  滑动窗口的整体逻辑如上,但一些实现细节还需要注意。对于这个题,题目中要求的t字符并不要求顺序,所以我们用字典的形式存起来即可。比如t="ABCA"则我们定义一个需求字典need={"A":2,"B":1,"C":1}。对于滑动窗口当前包含的字符,我们也定义一个字典has,存储方式类似。

代码:

class Solution(object):

    def minWindow(self, s, t):

        def tianjia(d,c):#向字典中添加元素的函数

            if c in d:

                d[c]+=1

            else:

                d[c]=1

        left=0#初始化左边界

        right = 0#初始化右边界

        has={}#当前窗口中有的字母

        need = {}#存放题目的需求,即需要包含哪些字符

        lenth=len(s)

        if lenth<len(t):#如果s的长度小于t,不存在覆盖子串,按题意返回"".

            return ''

        valid=0 #valid表示目前已经满足的字母数

        res_len = float('inf') #初始化结果长度为正无穷,方便一步步缩小

        res_left =0#存储结果区间的左边

        res_right=0#存储结果区间的右边

        for i in t:#先把t字符串添加进need字典

            tianjia(need,i)

        while(right<lenth):#在窗口抵达最右边之前

            c = s[right]#窗口每扩大一点,就包含了一个新的元素

            right+=1#扩大窗口(右移右边界)

            if c in need:#如果这个新的元素在”need清单”里,证明这个元素值得被记录         
                #我们就把它放在has里

                tianjia(has,c)

                if has[c]==need[c]:#如果此时has里相应元素的个数正好等于need

                    valid +=1#证明这个元素满足条件了,valid+1
            #当valid值等于need清单的长度,证明所有字符都满足条件了

            while(valid==len(need)):
                #此时要准备开始缩小窗口来逼近最优解了

        #首先我们要做的就是记录和更新(如果区间长度是历史最小值的话)这个区间的位置

                if right-left<res_len:

                    res_len = right-left #res_lne暂存结果区间的长度

                    res_left = left #res_left暂存结果区间的左边

                    res_right = right #res_right暂存结果区间的右边



                #注意!!!!开始缩小窗口了

                cc = s[left]#把左边界当前这个元素拿出,这个元素随着窗口收缩要除去

                left+=1#缩小窗口(右移左边界)

                if cc in need:#如果要除去的这个元素在”need清单”里

                    has[cc]-=1#在has字典里把相应值-1

         #减完之后若恰好差1满足需求,说明减掉的这个字符起到了让窗口不再满足条件的作用
                    if has[cc]+1==need[cc]:

                        valid-=1#valid减少,即满足条件的元素减少了

        return s[res_left:res_right]

小结:

  题目中我定义了一个向字典d中添加元素c的函数tianjia(d,c),若原来没有则置1;若已有则+1,这样提前定义后,后续就不需要重复再写了,我感觉这样还是很方便的。

  这段代码看似很长,细节很多,但实际上很好理解,且可复用性极高。整个滑动窗口,就是两层while循环:  外层while(right<length)扩大窗口,内层while(valid满足条件)缩小窗口。而扩大和缩小窗口,就是分别右移右边界和右移左边界而已。

posted @ 2021-12-16 19:27  JunanP  阅读(15)  评论(0)    收藏  举报  来源