[算法]滑动窗口

一、what?

一般用于获取数组中“满足某条件C”的最长/短/等于某长度的区间。滑动窗口算法维护两个指针l,r,利用lr寻找满足条件C的区间[l,r)。lr移动方向相同,形成了一个「窗口」在直线上「滑动」的效果。

二、when?

1. when:满足什么条件可以使用滑动窗口?

当可以根据题目条件C,将当前行动分为 r右移 or l右移时,可以采用滑动窗口来解。

if 满足条件C(or不满足条件C):
    应进行的操作 = l右移
else:
    应进行的操作 = r右移

2. why:为什么满足这个条件就可以使用滑动窗口?

  • 不妨将C与C的反面分别称为 满足l右移条件 与 满足r右移条件,显然这两个条件互补。由于二者互补,满足l右移条件时必然不满足r右移条件,不能r右移必须l右移;反之亦然。也就是说,能l右移时必然不能r右移;能r右移时必然不能l右移。这是可使用滑窗的关键。(下面称为“右移互补性”,自己瞎起的名字hhh)

3. what:什么问题满足这种条件?

  • 一般题目要求形如“题目条件C”+“长度要求:最长/短/定长”。

  • 这种“右移互补性”是由"长度要求:最长/短/定长"和“题目要求C”共同作用导致的。
    这两个要求必须满足 “一个希望l越右越好 r越左越好;一个希望l越左越好 r越右越好”。(这一性质也决定了“当求最短区间时,l右移的条件=题目条件C;当求最长区间时,l右移的条件=题目条件C的反”,不理解没关系,后面有讲)
    可以发现这两个要求的希望是矛盾的,由于要同时满足这两个条件,导致我们不能直接把两个指针一个放最左一个放最右。
    正是这种矛盾的性质使得问题满足“右移互补性”。以具体问题为例:

    • e.g. lc3
      • “不包含重复字符”希望l越右越好,r越左越好(极限情况:l右到数组结束,区间啥都不包括,必然是满足要求的);“最长”要求l越左越好,r越右越好(这个很好理解了,l越左r越右,区间越长)。
      • 那么在区间满足“不包含重复字符”时,为了满足“最长”,“r能右l不能右”应进行的操作->右移r;在区间“包含重复字符”时,为了满足“不包含”,“l能右r不能右”应进行的操作->右移l
    • e.g. lc76
      • “包含t所有字符”要求l越左越好,r越右越好(极限情况:包含所有的元素了,必然是最可能满足要求的);“最短”要求l约右越好,r越左越好(极限情况:空区间,最短)
      • 那么在区间满足“包含t所有字符”时,为了满足最短,“l能右r不能右”应进行的操作->右移l;在区间不满足“包含t所有字符”时,为了满足“包含”,“r能右l不能右”应进行的操作->右移r
  • 以上分析的是“最长/短”的要求。对于要求等于某长度的,可以将其转换为“求最短”,具体见后。

三、how?

1. 模板

l, r = 0, 0 #初始化为0,当前区间[l, r)为空,区间长度始终为 r-l
while r < len(s): # 保证r不出界
    ... # 对状态做修改(上一个轮执行了r右移,状态改变了),好让程序在后面检测是否满足条件
    r += 1 # 利用大循环完成r指针右移,这一步其实放在小循环结束后也可以,影响不大,注意长度计算、数组索引取对就行
    while 使得l右移的条件,也是使得r右移条件的反 and l < r: # 注意这里刚经过 r += 1,也就是说刚遍历过元素为 s[r-1],而不是 s[r]. 这里可能会需要l < r
        min_len = min(min_len, r-l) # 最小值
        ... # 对状态做修改(上一轮小循环执行了l右移,状态改变了),好让程序在后面检测是否满足条件
        l += 1 # 利用小循环完成l指针右移
    max_len = max(max_len, r-l) # 最大值

2. truth:

  • 3.中的分析都是基于这几条truth的:
    • 问题满足能l右移时必然不能r右移;能r右移时必然不能l右移(“右移互补性”)(从when中分析来的核心)
    • 始终维护区间[l, r)满足题目要求C。 r指针用于扩张区间,l指针用于收缩区间。注意:这个区间是单调向右移动的。
    • 由于最长/短+条件C的共同作用,导致r指向任意元素时,只会有唯一满足“题目条件C”+“长度要求:最长/短”的区间[l,r)。(下面称作**“局部唯一性”**,很重要!!又是一个自己起的名字hhh)【ps:当然l指向每个元素时必然也只有唯一符合条件的区间[cur_l,r),只不过这里我们不用这一条性质】

3. 分析模板

  • 由于“局部唯一性”,那么我们要做的就是用r遍历所有元素(这样可以找到所有符合要求的区间),对于r指向的每个位置都找到当r处于当前位置时符合要求的区间[l,cur_r)(满足题目要求C和长度要求),比较所有[l,cur_r)就可以获得最长/最短/等于某长度的区间了。
  • 整个程序采用大小两个while循环完成以上目的:
    • 大循环while:当r<len(s)时大循环while不断右移r;——用r遍历所有元素
    • 小循环while:当r右移后区间不满足题目条件C和长度要求时,采用小循环右移l使得其满足题目条件C和长度要求;——,对于r指向的每个位置都找到当r处于当前位置时符合要求的区间[l,cur_r)
  • 根据两个循环的任务,具体写法如下:
    • 利用大循环完成r右移r+=1,利用小循环完成l右移l+=1。(因为r用于扩张,所以必然先执行r右移,采用大循环。)
    • 大循环控制r小于len(s)小循环写使得l右移的条件 and l小于r(理论上这里需要控制l小于r,但是’l小于r’有时必然满足,经常忽略,但写了肯定是没错的)
      • 小循环的控制条件具体是"满足题目要求C"or"不满足题目要求C"并不重要,关键是使得l右移。
      • 当长度要求是最短区间时,l右移的条件=题目条件C;当求最长区间时,l右移的条件=题目条件C的反
        why?假设求最长区间时,l右移条件=C,也就是说C希望l约右越好,而最短也希望l越右越好。这两要求不矛盾,不可能。(这一点不用记,小循环条件直接分析啥时候l右移就行了,别管题目的要求C是啥都无所谓。)
      • 关于小循环写l右移条件的合理性分析:
        1)while结束后:进入大循环while进行r右移,这是科学的。因为此时不满足l右移的条件,则必然满足r右移的条件,应该进行r右移。
        2)进入while前:在大循环看,每次r右移后都会判断是否满足l右移,若满足则进入此循环进行l右移(此时必然不满足r右移条件,应该l右移);若不满足l右移则进入下一轮大循环继续进行r右移(此时不满足l右移条件,应该l右移),这也是科学的。
        以上两点都是由“右移互补性”保证的。也正是因为这一点,才能采用这种大小循环的结构。
  • 再来看长度要求:最长/最短/定长(recall:当求最短区间时,l右移的条件=C;当求最长区间时,l右移的条件=C的反)
    • 关于区间长度
      • 由于始终维护[l,r)为所找区间,所以区间长度始终为r-l
      • 当然也可以维护cur_len变量来记录长度,每次r右移+1,l右移-1,但是没有r-l简洁明白
    • 对比更新获得最大值地方:在大循环的末尾进行
      • why?刚退出小循环(区间不满足小循环条件+小循环条件为C的反),也就是说此时区间[l, r)是符合题目要求C的。而由于整个大循环过程中我们对于l指针能不右移就不右移,所以此处[l,r)就是满足题目条件C+长度要求:最长的区间。所以在大循环每轮的末尾进
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值