[leetcode] - 不定长滑动窗口
不定长滑动窗口总体可以分为三大类:
1.求满足小于k的最长子数组/子串
2.求满足至少k的最短子数组/子串
3.求子数组/子串个数
其中第三类又可以分为三小类:
1>满足小于k的子数组/子串的个数
2>满足至少k的子数组/子串的个数
3>恰好等于k的子数组/子串的个数
一、求满足小于k的最长子数组/子串
枚举右端点,while循环处理不满足条件,直到满足条件退出,退出while循环后更新答案。
for循环枚举右端点while 不满足条件移动左端点ans = max(ans, r-l)
# 3. 无重复字符的最长子串
class Solution:def lengthOfLongestSubstring(self, s: str) -> int:if not s:return 0d=defaultdict(int)l,ans=-1,0for r,c in enumerate(s):d[c]+=1while d[c]>1:l+=1d[s[l]]-=1ans=max(ans,r-l)return ans
对于左端点的处理,可以初始化成0,也可以初始化成-1。如果初始化成0,那更新答案时,就是r-l+1
。作者更喜欢用-1,所以更新时就是r-l
# 904. 水果成篮
class Solution:def totalFruit(self, fruits: List[int]) -> int:d=defaultdict(int)l,ans=-1,0for r,f in enumerate(fruits):d[f]+=1while len(d)>2:l+=1d[fruits[l]]-=1if not d[fruits[l]]:del d[fruits[l]]ans=max(ans,r-l)return ans
此类题目经常会把字典的key的个数(也即,种类数)作为while的判断条件,此时在移动左端点更新字典时,如果value=0,必须将key del
掉。
二、求满足至少k的最短子数组/子串
枚举右端点,while循环处理满足条件,直到不满足条件退出,在while循环内更新答案。
for循环枚举右端点while 满足条件ans = min(ans, r-l)移动左端点
# 209. 长度最小的子数组
class Solution:def minSubArrayLen(self, target: int, nums: List[int]) -> int: s,n=sum(nums),len(nums)if s<target:return 0if s==target:return nl,ans,s=-1,inf,0for r,x in enumerate(nums):s+=xwhile s>=target:ans=min(ans,r-l)l+=1s-=nums[l]return ans
# 76. 最小覆盖子串
class Solution:def minWindow(self, s: str, t: str) -> str:n,m=len(s),len(t)if n<m:return ''cntt=Counter(t) cnts=defaultdict(int) #cnts=Counter() l,ans=-1,''for r,c in enumerate(s):cnts[c]+=1while all(cnts[k]>=v for k,v in cntt.items()):#while cnts>=cntt:ans=s[l+1:r+1] if ans=='' or r-l<len(ans) else ansl+=1cnts[s[l]]-=1return ans
注意,如果想要判断一个字典A是否完全包含另一个字典B(即B中的key,A中都有且A中对应key的value都大于B),可以用cntA>=cntB
来判断。
三、求子数组/子串个数
3.1 满足小于k的子数组/子串的个数
枚举右端点,while循环处理不满足条件,直到满足条件退出,退出while循环后更新答案。
for循环枚举右端点 while 不满足条件移动左端点ans += r-l
# 713. 乘积小于 K 的子数组
class Solution:def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:mn,n=min(nums),len(nums)if mn>=k:return 0prod=1 l,ans=-1,0for r,x in enumerate(nums):prod*=xwhile prod>=k:l+=1prod//=nums[l]# 学习这种统计子数组的方法ans+=r-lreturn ans
右端点每移动一步,如果满足条件,当前右端点都会和前面的子数组依次组成r-l-1
个新的子数组,另外它本身也是一个符合条件的子数组,因此更新答案ans+=r-l
。
# 2762. 不间断子数组
class Solution:def continuousSubarrays(self, nums: List[int]) -> int:cnt=defaultdict(int)l,ans=-1,0for r,x in enumerate(nums):# 空间换时间,# 用哈希表减少对重复元素的遍历# 但是如果列表中有大量不重复的元素,用哈希表还是会超时,这时就需要使用单调队列,见1438题cnt[x]+=1while max(cnt)-min(cnt)>2:l+=1cnt[nums[l]]-=1if cnt[nums[l]]==0:del cnt[nums[l]]ans+=r-lreturn ans
本题思路并不复杂,但是如果用max()和min()函数去遍历之前子数组会超时,这时可以使用字典来减少对重复元素的遍历。
# 1438. 绝对差不超过限制的最长连续子数组
# 单调队列,
# 用单调队列记录列表的第一小,第二小,...,第n小;记录列表的第一大,第二大,...,第n大
class Solution:def longestSubarray(self, nums: List[int], limit: int) -> int:mxq,mnq=deque(),deque() l,ans=-1,0for r,x in enumerate(nums):while mnq and x<=nums[mnq[-1]]:mnq.pop()mnq.append(r)while mxq and x>=nums[mxq[-1]]:mxq.pop()mxq.append(r)while nums[mxq[0]]-nums[mnq[0]]>limit:l+=1if mnq[0]<=l:mnq.popleft()if mxq[0]<=l:mxq.popleft()ans=max(ans,r-l)return ans
有时使用字典依然会超时,这时就必须使用单调队列来存储按序排列的索引值。
# 1918. 第 K 小的子数组和
class Solution:def kthSmallestSubarraySum(self, nums: List[int], k: int) -> int:def check(sum):s=0ll,ans=-1,0for rr,x in enumerate(nums):s+=xwhile s>sum:ll+=1s-=nums[ll]ans+=rr-llreturn ans>=kl,r,ans=min(nums),sum(nums),0while l<=r:mid=(l+r)//2if check(mid):ans=midr=mid-1else:l=mid+1return ans
这题将二分答案和滑动窗口相结合,非常巧妙,值得反复练习和理解。
3.2 满足至少k的子数组/子串的个数
枚举右端点,while循环处理满足条件,直到不满足条件退出,在while循环内更新答案。
for循环枚举右端点while 满足条件ans += n-r移动左端点
# 1358. 包含所有三种字符的子字符串数目
class Solution:def numberOfSubstrings(self, s: str) -> int:n=len(s)cnt=defaultdict(int)l,ans=-1,0for r,c in enumerate(s):cnt[c]+=1while len(cnt)==3:ans+=n-rl+=1cnt[s[l]]-=1if cnt[s[l]]==0:del cnt[s[l]]return ans
这里的n-r
,可以理解为,当前子串是满足条件的最小集合,那后面跟着[0,n-r]个后缀就更满足条件了。因此对于当前子串,共有n-r
个满足条件的个数。
# 3298. 统计重新排列后包含另一个字符串的子字符串数目 II
# 超时
class Solution1:def validSubstringCount(self, word1: str, word2: str) -> int:if len(word1)<len(word2):return 0cnt1,cnt2=Counter(),Counter(word2)n=len(word1)l,ans=-1,0for r,c in enumerate(word1):cnt1[c]+=1while cnt1>=cnt2:ans+=n-rl+=1cnt1[word1[l]]-=1return ansclass Solution:def validSubstringCount(self, word1: str, word2: str) -> int: cnt2=Counter(word2)k,n=len(cnt2),len(word1)l,ans=-1,0for r,c in enumerate(word1):# 先减频数,再减个数# 通过频数和个数来模拟子串是否覆盖word2,参考76题cnt2[c]-=1if cnt2[c]==0:k-=1while k==0:ans+=n-rl+=1if cnt2[word1[l]]==0:k+=1cnt2[word1[l]]+=1return ans
本题跟76题类似,但是如果采用76题的思路会超时,这时就必须采用先减频数再减个数的方法来模拟是否覆盖。
3.3 恰好等于k的子数组/子串的个数
恰好等于k可以转化为 满足>=k - 满足>k
的个数,而满足>k
等价于满足>=k+1
,因此可以进一步转化为满足>=k - 满足>=k+1
的个数。
for循环枚举右端点while 满足>=k移动左端点l1while 满足>=k+1移动左端点l2ans += l1 - l2
# 930. 和相同的二元子数组
class Solution:def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:# 如果数组中有负数,就不能用滑动窗口,滑动窗口必须是单调的,见题560l1=l2=-1s1=s2=0ans=0for r,x in enumerate(nums):s1+=xwhile l1<r and s1>=goal:l1+=1s1-=nums[l1]s2+=xwhile l2<r and s2>=goal+1:l2+=1s2-=nums[l2]ans+=l1-l2return ans
# 560. 和为 K 的子数组
class Solution:def subarraySum(self, nums: List[int], k: int) -> int:# 为什么前缀和需要第一个0, 因为只有在最前面插入一个0,才能用s[2]-s[0]表示以第一个元素开始的子数组[x0,x1]。如果没有插入第一个0,s[0]表示第一个元素,那么s[2]-s[0]就会把第一个元素排除掉。所以必须要在前面插入一个0s=list(accumulate(nums,initial=0)) cnt=defaultdict(int) ans=0 for x in s:ans+=cnt[x-k]cnt[x]+=1return ans
该题数组中有负数,因此不能使用滑动窗口。
整理自leetcode 灵神题单:
https://leetcode.cn/discuss/post/3578981/ti-dan-hua-dong-chuang-kou-ding-chang-bu-rzz7/