一、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
- e.g. lc3
-
以上分析的是“最长/短”的要求。对于要求等于某长度的,可以将其转换为“求最短”,具体见后。
三、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)
- 大循环while:当
- 根据两个循环的任务,具体写法如下:
- 利用大循环完成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右移),这也是科学的。
以上两点都是由“右移互补性”保证的。也正是因为这一点,才能采用这种大小循环的结构。
- 利用大循环完成r右移
- 再来看长度要求:最长/最短/定长(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+长度要求:最长的区间。所以在大循环每轮的末尾进
- 关于区间长度