滑动窗口总结

滑动窗口题目的一般格式:

一个序列中, 满足某些条件的子串的最长/最短/个数

时间复杂度分析: 暴力解法的时间复杂度一般为O(N ^ 3):O(N ^ 2)枚举所有的子串,O(N)判断是否满足条件。
滑动窗口在两个方面都降低时间复杂度:首先并不枚举所有的子串,只枚举可能包含答案的那些。其次通过记录窗口内子串的一些信息,使得判断是否满足条件的时间复杂度下降。

难点: 什么时候更新答案,窗口内保存哪些信息来降低判断的时间。

正确性证明: 分解为子问题,或者反证法证明窗口内一定包含过正确答案。



最长:

这一类题目的特点是:一开始子串就满足条件,然后不断扩大窗口直至不满足条件,再缩小窗口直至满足条件,每次扩大窗口并判断满足条件后都更新答案。

3. 无重复字符的最长子串:给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

解法: 窗口的范围为[left,right),用一个字典记录窗口中的字符及其出现次数,当窗口内的子串满足条件时扩大窗口(right右移)并更新答案不满足条件时缩小窗口(left右移)。

正确性证明: 实际上我们把问题分解为了多个子问题:求以s[i]起点的满足条件的子串中最长的,再将子问题答案合并即可。
但还有一个问题:left每次并不是只向前走一步,如何保证枚举每一个s[i]呢?假设[left,right)满足条件,[left,right+1)不满足条件,此时答案至少是right-leftleft向前走k步直至满足条件的过程中,以s[left]为起点的满足条件的子串,长度都不可能超过right-left,也就是说这些子问题我们虽然没有求,但它们的答案没有意义,本身也就不需要求。

时间复杂度分析: leftright均没有后退:O(N)。判断right-left==len(window):O(1)。故总时间复杂度为O(N)。

代码:

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        ls = len(s)
        window = dict()
        left = right = 0
        ans = 0

        while right<ls:
            c = s[right]
            right += 1
            window[c] = window.get(c,0)+1

            while len(window)<right-left:
                d = s[left]
                left+=1
                window[d] -= 1
                if window[d]==0:
                    window.pop(d)
                
            ans = max(ans,right-left)
        return ans

424. 替换后的最长重复字符



最短:

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

解法: 窗口的范围为[left,right),用一个字典记录窗口中的字符及其出现次数,当窗口内的子串不满足条件时扩大窗口(right右移),满足条件时缩小窗口(left右移)并更新答案

正确性证明: 类似上一题,把问题分解为子问题:求以s[i]终点的满足条件的子串中最短的。
假设[left,right)是第一个满足条件的区间,则所有终点小于right的子问题都不必再考虑。之后依次枚举每一个right。但每次枚举时,left并没有从最左边开始找,是否可能漏掉一些答案呢?其实和上一题类似,如果[left,right)是当前子问题的最优解,则下一个子问题从[left+1,right+1)开始找,即使在left+1左边存在该子问题的解,对于最终的最优解也是没有意义的,故不需要考虑。

时间复杂度分析: leftright均没有后退:O(N)。判断是否满足条件:O(1)。故总时间复杂度为O(N)。

代码:

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        ls,lt = len(s),len(t)
        need = {}  # t中的元素
        for i in t:
            need[i] = need.get(i,0)+1
        window = {}  # 窗口中的元素
        left = right = 0  # 窗口范围
        start = length = float('inf')  # 答案
        valid = 0  # window是否包含t

        while right<ls:
            # 更新窗口内信息
            c = s[right]
            window[c] = window.get(c,0)+1
            if c in need and window[c] == need[c]:
                valid+=1
            right += 1

            # 更新答案
            while valid == len(need):
                if right - left < length:
                    start, length = left, right-left
                c = s[left]
                window[c] -= 1
                if c in need and window[c] < need[c]:
                    valid -= 1
                left += 1
        
        return s[start:start+length] if length<float('inf') else ''

总结:

滑动窗口为什么快?实际上是因为它把原问题分解为了子问题,而其中很大一部分子问题是不需要求解的(最优解一定不出现在这些子问题之中)。故要求问题一定要有二段性质:

求最长时,对于任意子问题s[i],一定存在j>i,使得s[i...k],i<k<j都满足条件,s[i...k]j<=k<n都不满足条件,才可以使当s[i+1...j]不满足条件时,无需求解子问题s[i+1]
求最短时,对于任意子问题s[j],一定存在i<j,使得s[k...j],0<k<i都满足条件,s[k...j]i<=k<j都不满足条件,才可以使当s[i+1...j+1]不满足条件时,无需求解子问题s[j+1]

总而言之,求解最长问题的子问题s[left]的过程中,right向右移时要先满足再不满足;求解最短问题的子问题s[right]的过程中,left右移时也要先满足再不满足。这样才可以用滑动窗口算法。

395. 至少有K个重复字符的最长子串,由于不满足上述条件,就无法直接使用滑动窗口算法。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值