算法:滑动窗口
滑动窗口
一、介绍
算法中的滑动窗口,它类似于网络数据传输中用于流量控制的滑动窗口协议以及深度学习的卷积操作中的滑窗。实际上这两种的滑动窗口在某一个时刻就是固定窗口大小的滑动窗口,随着一些因素改变窗口大小也会随着改变。
滑动窗口是一种解决问题的思路和方法,通常用来解决一些连续问题。一般情况下,可以通过题目中关键词判定是否是滑动窗口类型的题目,如:“(连续)子串”、“(连续)子数组”等等。
二、类型
滑动窗口的类型主要分为两大类:固定窗口大小和可变窗口大小
1.固定窗口
对于固定窗口,根据窗口大小,确定好左右边界,在固定区间内进行判断操作。
算法过程:
- l 初始化为 0
- 初始化 r,使得 r - l + 1 等于窗口大小
- 同时移动 l 和 r
- 判断窗口内的连续元素是否满足题目限定的条件
4.1 如果满足,再判断是否需要更新最优解,如果需要则更新最优解
4.2 如果不满足,则继续。
但在实际应用中,固定窗口大小k是已知的,因此可以不用设定r,只用设定左边界l即可,从而进行简化。
算法过程:
- l 初始化为 0,要进行操作的序列长度为 n,窗口大小为k
- 移动 l,从 0 移动到 n-k
- 判断窗口内的连续元素(l->l+k-1)是否满足题目限定的条件
3.1 如果满足,再判断是否需要更新最优解,如果需要则更新最优解
3.2 如果不满足,则继续。
伪代码:
初始化 ans
for 左边界 in 移动区间
判定条件计算
if 满足条件
更新答案
返回 ans
2.可变窗口
对于可变窗口,没有固定的窗口大小,但有需要满足的限定条件,满足条件的窗口可以有很多且窗口大小不定,但通常要求满足需求的最佳窗口。一般的题目中都是求解最大/小的满足条件的窗口。
因此需要设定左右边界,右边界移动来判定窗口中元素是否符合条件,当有满足的情况产生后,判断是否为最优窗口从而决定是否移动左边界。简言之:右边界指针不停向右移动,左边界指针仅仅在窗口满足条件之后才会移动,起到窗口收缩的效果。
算法过程:
- l 和 r 都初始化为 0
- r 指针移动一步
- 判断窗口内的连续元素是否满足题目限定的条件
3.1 如果满足,再判断是否需要更新最优解,如果需要则更新最优解。并尝试通过移动 l 指针缩小窗口大小。循环执行 3.1
3.2 如果不满足,则继续。
伪代码:
左边界指针 = 0
初始化 ans
for 右边界指针 in 可移动区间
更新窗口内信息
while 窗口内不符合题意
窗口信息变化
左边界指针移动
更新答案
返回 ans
三、实例
固定窗口
1. 题目链接:子数组最大平均数 I
[题目分析]:由于规定了子数组的长度为 k,最大平均数也可用子数组最大和代替。为了找到子数组的最大元素和,需要对数组中的每个长度为 k 的子数组分别计算元素和。对于长度为 n 的数组,一共有 n-k+1 个长度为 k 的子数组(k<=n),暴力求解则计算每一个子数组的和,但通过固定大小的滑动窗口可以很便捷的解决此问题。
[算法]:左边界从0移动到n-k,每次移动过程中,左边界的元素退出窗口,并在窗口右方加入新的元素,从而更新结果。
点击查看代码
class Solution:
def findMaxAverage(self, nums: List[int], k: int) -> float:
n=len(nums)
res=s=sum(nums[0:k])
for i in range(1,n-k+1):
#当前窗口为 [i...i+k-1]
#i-1位置的元素退出窗口,i+k-1位置的元素进入窗口
s=s-nums[i-1]+nums[i+k-1]
#更新结果
res=max(res,s)
return res/k
[复杂度分析]:
- 时间复杂度:T(n)=O(n),n 为数组nums的长度
- 空间复杂度:S(n)=O(1)
2. 题目链接:学生分数的最小差值
[题目分析]:暴力求解思路简单,即求出 n 个分数中任意 k 个分数中的最大最小值做差,最终选择最小的。很明显时间按复杂度很高,我们可以选择排序+滑动窗口的方法解决问题。问题一:为什么要排序?因为题中所求是 k 个元素中最大最小值差值的最小值,那么这 k 个元素必然是连续的,因为连续的值中才会出现最小的差值。问题二:滑动窗口如何使用?将数据排序后,即对每连续的 k 个元素进行判断,然后扫描所有大小为 k 的窗口,直接找到答案。
[算法]:先排序,左边界从0移动到n-k,每次移动过程中,左边界的元素退出窗口,并在窗口右方加入新的元素,然后更新结果。
点击查看代码
class Solution:
def minimumDifference(self, nums: List[int], k: int) -> int:
nums.sort()
res,n=100000,len(nums)
for i in range(n-k+1):
res=min(res,nums[i+k-1]-nums[i])
return res
[复杂度分析]:
- 时间复杂度:T(n)=O(nlogn),n 为数组nums的长度
排序的复杂度 O(nlogn),遍历寻找答案的时间复杂度 O(n),整体为 O(nlogn)。 - 空间复杂度:S(n)=O(logn),即排序所需的栈空间
3. 题目链接:存在重复元素 II
方法1:排序
[算法]:将元素与其位置构造成二元组元素,再将元组序列按照元组的第一个元素排完序后,相等的元素位于相邻位置,判断相等元素的位置是否满足条件。
点击查看代码
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
#构造(元素,位置)的二元组序列
lis=[(num,idx) for idx,num in enumerate(nums)]
#以元素大小进行排序
lis2=sorted(lis,key=lambda x:x[0])
n=len(lis2)
for i in range(n):
#判断相等的相邻元素是否满足位置需求
if i<n-1 and lis2[i][0]==lis2[i+1][0]:
if lis2[i+1][1]-lis2[i][1]<=k:
return True
return False
[复杂度分析]:
- 时间复杂度:T(n)=O(nlogn),n 为数组nums的长度
排序的复杂度 O(nlogn),遍历寻找答案的时间复杂度 O(n),整体为 O(nlogn)。 - 空间复杂度:S(n)=O(logn),即排序所需的栈空间
方法二:哈希
[算法]:从前到后遍历数组,记录距离当前元素最近的相等元素的位置,判断是否满足位置需求
点击查看代码
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
dic={}
for idx,num in enumerate(nums):
if num in dic and idx-dic[num]<=k:
return True
#记录最新的元素位置,在此之前的相等元素位置没有意义
dic[num]=idx
return False
[复杂度分析]:
- 时间复杂度:T(n)=O(n),n 为数组nums的长度。需要遍历数组一次,对于每个元素,哈希表的操作时间都是O(1)。
- 空间复杂度:S(n)=O(n),需要使用哈希表记录每个元素的最大下标,哈希表中的元素个数不会超过 n。
方法三:滑动窗口
[算法]:维护一个大小为 k 的滑动窗口,扫描每一个窗口。从前向后遍历每一个元素,当窗口中元素个数小于k时,判断当前元素是否在窗口中,不存在则加入窗口中;当窗口中元素个数大于k时,弹出窗口最左侧的元素(当前元素位置i,弹出i-k-1位置的元素)后再进行相同的判断。
点击查看代码
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
#使用集合记录窗口中存在的元素
s=set()
for idx,num in enumerate(nums):
#窗口中元素大于窗口大小k时,弹出窗口最左侧元素
if idx>k:
s.remove(nums[idx-k-1])
#判断当前元素是否已在窗口中
if num in s:
return True
#不存在则加入窗口
s.add(num)
return False
[复杂度分析]:
- 时间复杂度:T(n)=O(n),n 为数组nums的长度。需要遍历数组一次。
- 空间复杂度:S(n)=O(k),需要使用集合记录当前窗口中存在的元素,窗口中元素个数不会超过窗口大小 k。
4. 题目链接:可获得的最大点数
[题目分析]:根据体重拿牌的规则可总结出可拿牌的方式:① 卡牌前 k 张;② 卡牌后 k 张;③ 从牌尾第 n-k 张开始到牌尾最后一张作为起始,依次取牌共取 k 张,不够的从牌头依次补齐。更形象理解,将已有牌复制一份,加于牌尾,分别在[0...k]、[n-k+1...n+k]的范围内连续取 k 张牌。
[算法]:在实际算法实现中,不用进行复制加长,可直接使用 mod 实现取牌头补齐的操作,类似于循环链表。其余过程都和固定窗口大小的基本过程一致。
点击查看代码
class Solution:
def maxScore(self, cardPoints: List[int], k: int) -> int:
n=len(cardPoints)
res=total=sum(cardPoints[n-k:])
for i in range(n-k,n):
#计算判定条件即点数和
#滑动窗口每向右移动一格,增加从右侧进入窗口的元素值,并减少从左侧离开窗口的元素值
total=total-cardPoints[i%n]+cardPoints[(i+k)%n]
#更新结果
res=max(res,total)
return res
[复杂度分析]:
- 时间复杂度:T(n)=O(n),n 为数组cardPoints的长度。
- 空间复杂度:S(n)=O(1)。
5. 题目链接:重复的DNA序列
[题目分析]:所求子字符串长度固定,只用判定子串是否出现过。因此使用哈希+滑动窗口即可解决问题。
[算法]:窗口大小为10,扫描所有窗口(此处窗口即为长度为10的子串)。从前向后遍历,判断已当前位置元素为起点,长度为10的子串是否出现过。在实现过程中,整体框架仍为固定窗口大小算法框架。为避免重复问题,在子串出现次数递增的前提下,只有当出现次数为1时更新结果。
点击查看代码
class Solution:
def findRepeatedDnaSequences(self, s: str) -> List[str]:
n=len(s)
res=[]
dic=defaultdict(int)
for i in range(n-9):
st=s[i:i+10]
#更新结果
if dic[st]==1:
res.append(st)
dic[st]+=1
return res
[复杂度分析]:
- 时间复杂度:T(n)=O(n*k),n 为字符串 s 的长度,k 为子串的长度10。
- 空间复杂度:S(n)=O(n),长度固定的子串数量不会超过 n 个。
[优化]:
很明显看出,上面的滑动窗口实则就是字符串 s 的一个子串,我们是对每个长为 10 的子串都单独计算的,滑动窗口的意义体现不大。滑动窗口通常在滑动过程中,保留中间部分的信息,只对窗口最左侧以及新进入窗口的元素处理,从而更新判断条件。
因此,可进行如下优化,使用二进制数表示序列元素即DNA序列,每个元素使用两位二进制数表示。如 00——A,01——C,10——G,11——T。这样,一个长为 10 的字符串就可以用 20 个比特表示,而一个 int 整数有 32 个比特位,足够容纳该字符串,因此我们可以将 s 的每个长为 10 的子串(即一个滑动窗口)用一个 int 整数表示(只用低 20 位),从而构成哈希以便条件判断。
而之后问题的关键就在于,滑动窗口向右移动一位,此时会有一个新的字符进入窗口,以及窗口最左边的字符离开窗口,这些操作可使用位运算进行操作,从而简化时间复杂度。对应的位运算,按计算顺序表示如下,假设当前滑动窗口对应的整数为x:
- 滑动窗口右移一位:
x = x << 2
。因为每一个元素用两位二进制数表示,因此窗口表示的数要左移两位。 - 一个新字符 c 进入窗口:
x = x | rep[c]
。rep[c]为字符 c 代表的二进制数,等号右边的 x 为右移后的整数。 - 窗口最左边字符离开窗口:
x = x & ((1 << 2 * L) - 1)
。因为窗口只用 int 整数的低 20 位表示,等号右边的 x 已经是移动后加入新字符的整数了,有效位占 20 位,只需将其余位都置为 0 即可,L = 10。
点击查看代码
class Solution:
def findRepeatedDnaSequences(self, s: str) -> List[str]:
L = 10
rep = {'A': 0, 'C': 1, 'G': 2, 'T': 3}
n = len(s)
if n <= L:
return []
ans = []
x = 0
#求出整个序列的整数表示
for ch in s[:L - 1]:
x = (x << 2) | rep[ch]
dic = defaultdict(int)
for i in range(n - L + 1):
#窗口移动
x = ((x << 2) | rep[s[i + L - 1]]) & ((1 << (L * 2)) - 1)
#更新哈希(判断条件)
dic[x] += 1
#条件判断
if dic[x] == 2:
ans.append(s[i : i + L])
return ans
[复杂度分析]:
- 时间复杂度:T(n)=O(n),n 为字符串 s 的长度。
- 空间复杂度:S(n)=O(n),哈希长度不会超过 n 个。
6. 题目链接:找到字符串中所有字母异位词
[题目分析]:异位词的长度与字符串 p 长度相同,且为原字符串的连续子串。因此,可以维护一个大小与 p 长度相等的滑动窗口,在滑动过程中维护窗口中字母的数量。
[算法]:使用固定窗口的算法框架,用哈希表维护窗口中字母数量。滑动过程中的判定条件即窗口中每种字母的数量与字符串 p 中每种字母的数量是否相等,相等则为异位词,不等则继续滑动窗口寻找。在实现过程中,使用两个哈希表,一个哈希表存放字符串 p 的数量,另一个哈希表存放实时窗口中字母的数量。
点击查看代码
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
#dic为字符串p的字母数量哈希,dic1为窗口实时字母数量哈希
dic=collections.Counter(p)
dic1=dict.fromkeys(dic,0)
res=[]
#若字符串p的长度大于s,则s中一定不包含异位词
#因为异位词的长度和p相等
n,m=len(s),len(p)
if n<m:
return []
#初始化窗口字母数量的哈希表
for i in range(m):
if dic.get(s[i]):
dic1[s[i]]+=1
for i in range(n-m+1):
#判断窗口中每种字母的数量与字符串 p 中每种字母的数量是否相等
if dic1==dic:
res.append(i)
#删除窗口最左边的字母,更新窗口字母哈希
if dic1.get(s[i])!=None:
dic1[s[i]]-=1
#添加窗口右端的字母,更新窗口字母哈希
if i+m<n and dic1.get(s[i+m])!=None:
dic1[s[i+m]]+=1
return res
[复杂度分析]:
- 时间复杂度:T(n)=O(m+(n-m)*c),其中 n 为字符串 s 的长度,m 为字符串 p 的长度,c 为所有可能的字符数。
统计字符串p字母数量与初始化窗口字母数量的哈希均为O(m),窗口滑动遍历为O(n-m),遍历过程中判断哈希相等为O(c)。 - 空间复杂度:S(n)=O(c)。
[优化]:
[算法]:上面方法在扫描每个窗口过程中,判断窗口中每种字母的数量与字符串 p 中每种字母的数量是否相等依靠判断两个哈希中对应元素个数是否相等,这使得时间复杂度倍数增加。因此,可使用一个变量表示对应字母数量差值总和,在滑动过程中维护它。而判断条件即当该差值为0时,表示二者字母数量相等。具体维护差值 dif 过程:
- 初始化:
- 统计两个字符串中字母数量,遍历长度为字符串 p 长度 m,字符串 s 中的字母次数加一,字符串 p 中的字母次数减一。哈希中最终值为正数、零以及负数三种。正数表示该字母数量多余,负数表示该字母数量缺少,零表示该字母数量与 p 中数量相等。
- 统计完后,只要字母哈希值不为零则 dif 加一。
- 滑动过程:
- 窗口最左边字母退出窗口,其哈希值需要减一,但是在减之前,若该字母哈希值为 1,移动后就变为 0,即该字母由数量多余变成数量满足需求,则 dif 减一;若该字母哈希值为 0,移动后就变为 -1,即该字母由数量满足需求变成数量缺少,则 dif 加一;其余数值情况对 dif 没有影响。
- 窗口右端字母进入窗口,其哈希值需要加一,在加之前,若该字母哈希值为 0,移动后就变为 1,即该字母由数量满足需求变成数量多余,则 dif 加一;若该字母哈希值为 -1,移动后就变为 0,即该字母由数量缺少变成数量满足需求,则 dif 减一;其余数值情况对 dif 没有影响。
点击查看代码
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
dic=collections.Counter(p)
dic=dict.fromkeys(dic,0)
n,m=len(s),len(p)
res=[]
if n<m:
return res
#统计字母数量并初始化差值
for i in range(m):
if dic.get(s[i])!=None:
dic[s[i]]+=1
if dic.get(p[i])!=None:
dic[p[i]]-=1
dif=[x!=0 for x in dic.values()].count(True)
for i in range(n-m+1):
#判断窗口中每种字母的数量与字符串 p 中每种字母的数量是否相等
if dif==0:
res.append(i)
#窗口最左边字母退出窗口,更新哈希与差值
if dic.get(s[i])!=None:
if dic[s[i]]==1:
dif-=1
elif dic[s[i]]==0:
dif+=1
dic[s[i]]-=1
#新字母进入窗口,更新哈希与差值
if i+m<n and dic.get(s[i+m])!=None:
if dic[s[i+m]]==-1:
dif-=1
elif dic[s[i+m]]==0:
dif+=1
dic[s[i+m]]+=1
return res
[复杂度分析]:
- 时间复杂度:T(n)=O(m+n+c),其中 n 为字符串 s 的长度,m 为字符串 p 的长度,c 为所有可能的字符数。
统计字符串 p 字母数量为 O(m),统计字母数量构造哈希为 O(m),初始化差值为 O(c),窗口滑动遍历为O(n-m),遍历过程中判断条件为O(1)。 - 空间复杂度:S(n)=O(c)。
可变窗口
1. 题目链接:最长和谐子序列
[题目分析]:和谐子序列本质:由若干个元素构成,但一共只有两种元素,且这两种元素大小相差一
方法一:排序+滑动窗口
[算法]:因为最终结果只需求序列的长度,可以改变元素顺序。因此可以先对原序列排序,排序后相邻元素相差小,从而维护一个大小不定的窗口,左边界不变,右边界移动,但要保证窗口中的元素只有两种,其差值为1,只要当不满足此条件,则右边界不变,移动左边界直到重新满足条件。对于满足条件的子序列更新其长度。
点击查看代码
class Solution:
def findLHS(self, nums: List[int]) -> int:
nums.sort()
l,n=0,len(nums)
res=0
for r in range(n):
#当不满足“和谐”条件后移动左边界指针
while nums[r]>nums[l]+1:
l+=1
#更新结果
res=max(res,r-l+1) if nums[r]>nums[l] else res
return res
[复杂度分析]:
- 时间复杂度:T(n)=O(nlogn),n 为nums数组的长度。
排序的复杂度 O(nlogn),遍历寻找答案的时间复杂度 O(n),整体为 O(nlogn)。 - 空间复杂度:S(n)=O(logn),即排序所需的栈空间。
方法二:哈希
[算法]:遍历每一个元素,求得每一个元素与比其大 1 的元素的个数和,最终最大值为所求。通过哈希的方法,也可求得最终的和谐子序列且不改变元素顺序。
点击查看代码
class Solution:
def findLHS(self, nums: List[int]) -> int:
c=collections.Counter(nums)
res=0
for num,cnt in c.items():
if c.get(num+1):
res=max(res,sum([cnt,c[num+1]]))
return res
[复杂度分析]:
- 时间复杂度:T(n)=O(n),n 为数组nums的长度。
遍历寻找答案的时间复杂度 O(n),哈希查找时间复杂度 O(1),整体为 O(n)。 - 空间复杂度:S(n)=O(n)。哈希长度不会超过 n。
2. 题目链接:无重复字符的最长子串
[题目分析]:使用哈希+滑动窗口的方法,窗口大小不定,判定条件为窗口中的元素是否存在重复,因此使用可变窗口的算法框架设计算法。
[算法]:左边界初始化为0,右边界移动,移动过程中,先更新判定条件,即修改哈希表内容。修改后判断条件是否满足需求,不满足则移动左边界,同时修改判定条件直至满足;若满足则更新结果。
点击查看代码
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
n=len(s)
dic=collections.defaultdict(int)
#初始化左边界指针为0
l=res=0
for r in range(n):
#更新判定条件
dic[s[r]]+=1
#不满足条件,移动左边界指针
while dic[s[r]]>1:
dic[s[l]]-=1
l+=1
#更新结果
res=max(res,r-l+1)
return res
[复杂度分析]:
- 时间复杂度:T(n)=O(n)。
- 空间复杂度:S(n)=O(n)。
3. 题目链接:替换后的最长重复字符
[题目分析]:题目最终所求最长重复字符即为原字符的连续子串的长度,因此可以采用滑动窗口的方法。窗口大小不定,但窗口中一直存放满足要求的子串(操作k次变成重复字符串)。
[算法]:根据可变窗口的算法框架,需确定用什么表示需要判定的条件以及如何在不满足需求(判定条件不成立)时修改条件信息。首先,使用哈希表存储每个字符已经出现的次数,每当窗口移动新字符键入窗口,则更新哈希表(次数加 1)。其次,判定条件设定为一个需求量,表示当前窗口所有的字符长度减去当前字符中出现次数最多的字符个数的差值,该差值与可允许操作替换的次数 k 比较,当大于 k 时,说明窗口内字符不满足条件,则需要缩小窗口,更新哈希(将退出窗口的字符次数减 1),从而移动左边界指针,再修改判定条件。最后,更新结果。
点击查看代码
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
l,n=0,len(s)
dic=defaultdict(int)
res=0
for r in range(n):
#修改判定条件need
dic[s[r]]+=1
need=r-l+1-max(dic.values())
#判定条件不满足需求
while need>k:
#移动左边界指针,更新哈希
if l<n and dic[s[l]]:
dic[s[l]]-=1
l+=1
#修改判定条件
need=r-l+1-max(dic.values())
#更新结果
res=max(res,r-l+1)
return res
[复杂度分析]:
- 时间复杂度:T(n)=O(nc),n 为字符串 s 的长度,c 为哈希的最长长度,即所有大写字母个数26。
遍历寻找答案的时间复杂度 O(n),求当前窗口元素出现次数最大值的时间复杂度是 O(c),整体为 O(nc)。 - 空间复杂度:S(n)=O(c)。
[双指针解法]:
[双指针]:滑动窗口是一种特殊的双指针算法,滑动窗口在滑动过程中,窗口中的元素保持着一定的特性,如上面实例中,窗口内元素的最大值与最小值始终相差一或窗口中的所有元素都只出现过一次等等。而双指针意义更加广泛一些,用两个变量在线性结构上遍历,根据元素的特性解决问题。
[问题分析]:通过滑动窗口,我们了解到,窗口的条件是以字符串中的每一个位置作为右端点,然后找到其最远的左端点的位置,满足该区间(窗口)内除了出现次数最多的那一类字符之外,剩余的字符(即非最长重复字符)数量不超过 k 个。在滑动窗口中,当不满足窗口需求时,要一直移动左端点使得满足条件。而在双指针算法中,当新进入窗口(区间)元素后仍满足需求,则继续移动右端点(即窗口变大);当新进入窗口(区间)元素后不满足需求,左右端点同时移动(即窗口不变),使得区间一直保持满足条件的最大值。
[算法]:每次区间右移,更新右移位置的字符出现的次数,然后用它更新重复字符出现次数的历史最大值,最后使用该最大值计算出区间内非最长重复字符的数量,以此判断左指针是否需要右移即可。算法中为什么 r -l
代表最终的最大区间长度?在上面问题分析中已经说明,在区间移动过程中,区间 r-l 一直保持这满足条件的最大值,即便 [l...r] 中的元素不满足条件,但是其长度已经是对历史最大长度的记录,与其中间的元素特性没有关系。
点击查看代码
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
n=len(s)
dic=defaultdict(int)
l=r=maxn=0
while r<n:
dic[s[r]]+=1
maxn=max(maxn,dic[s[r]])
while r-l+1-maxn>k:
if l<n and dic[s[l]]:
dic[s[l]]-=1
l+=1
r+=1
return r-l
[复杂度分析]:
- 时间复杂度:T(n)=O(n),n 为字符串 s 的长度。
- 空间复杂度:S(n)=O(c),c 为哈希的最长长度,即所有大写字母个数26。
4. 题目链接:考试的最大困扰度
[题目分析]:该题原理和实例3完全相同,方法也可使用 滑动窗口 和 双指针。
滑动窗口
class Solution:
def maxConsecutiveAnswers(self, answerKey: str, k: int) -> int:
l,n=0,len(answerKey)
dic={'T':0,'F':0}
res=0
for r in range(n):
dic[answerKey[r]]+=1
dif=min(dic.values())
while dif>k:
dic[answerKey[l]]-=1
l+=1
dif=min(dic.values())
res=max(res,r-l+1)
return res
双指针
class Solution:
def maxConsecutiveAnswers(self, answerKey: str, k: int) -> int:
n=len(answerKey)
l=r=maxn=0
dic={'T':0,'F':0}
while r<n:
dic[answerKey[r]]+=1
maxn=max(maxn,dic[answerKey[r]])
while r-l+1-maxn>k:
dic[answerKey[l]]-=1
l+=1
r+=1
return r-l