关于数组的连续区间的问题,应条件反射想到滑动窗口和前缀和技巧
利用左右指针,在一次遍历中求解题目
使用滑动窗口(双指针)解决的问题多与「最小」、「最大」有关,这是因为我们可以在满足条件的前提下,尽可能维护最长的窗口
例如:
LeetCode 3. 无重复字符的最长子串;(VIP)
LeetCode 209. 长度最小的子数组;
LeetCode 76. 最小覆盖子串;(209题的升级版)
LeetCode 159. 至多包含两个不同字符的最长子串;(VIP)
LeetCode 424. 替换后的最长重复字符(⭐转化问题:维护窗口,除了数量最多的字符外,其余字符的个数必须小于等于k)
LeetCode 904. 水果成篮
给出一个数组,求满足条件的最长子数组长度,条件是:数组中所含的数字种类<=2种
字典cnt维护当前窗口内的数字及其个数,kind维护当前窗口内的数字种类,kind超过2时,收缩窗口,不断更新窗口的最大长度r-l+1
即可
核心思路是把多个不同区间的问题,统一为相同类型的子问题
[l,r]
区间的查询,左右两边界是不固定的,我们固定一边[l,r]
区间和=[0,r+1)
区间和 - [0,l)
区间和[0,i)
区间的和提前求出)我们可以推广这种思想:把[左右界不固定的问题] 转化为两个 [左边界固定,右边界不定]的问题
前面介绍过,使用滑动窗口(双指针)解决的问题多与「最小」、「最大」有关
然而还有一些问题,不是最大、最小的问题,也可以用滑动窗口求解,但前提是需要把原问题转换成为容易求解的问题
问题要求数组的某个指标在[left,right]
范围内/要求指标恰好为k
(相当于left=right=k
的情况),我们把「某范围内 / 恰好」的复杂问题转化为「最多」的滑动窗口问题
这里需要类比前缀和的思想,(并不是真正的前缀和,而是问题转化的思维)
例如:
[left,right]
之间的子数组数量 = atMost(right)
- atMost(left-1)
atMost(n)
为数组的最大值<=n
的子数组数量atMost(k)
- atMost(k-1)
atMost(kind)
为数字种类<=kind种
的子数组数量要做到不重不漏的统计,有多种方法(例如,可以依次求长度为1、2、…的子数组分别有几个,然而这样的方法不便于代码实现)
[1,2,3]
,r
指针从左往右,依次统计了子数组[1]
(+1)、[1,2]
、[2]
(+2)、[1,2,3]
、[2,3]
、[3]
(+3),LeetCode 713. 乘积小于K的子数组
给一个数组,求所有数字乘积小于 k 的子数组的个数
分析:
滑动窗口,每次维护以r为右边界的、满足条件的[最长]子数组窗口
每次窗口长L,则新增L个符合要求的子数组
如[2,3]
长为2,新增2个乘积小于7的子数组[2,2]
、[3]
LeetCode 467. 环绕字符串中唯一的子字符串
类似的思想,以某字母结尾的子串最长为n,则就找到了n种新的不同子串
LeetCode 1358. 包含所有三种字符的子字符串数目
给出只含有a,b,c的字符串s,统计a,b,c都至少出现一次的子串数量(不同下标的相同字符串算多次)
仍然是固定右边界来统计:
维护r
指针为结尾的、满足要求的[最短]滑动窗口,对于该窗口,向左扩张同样是满足要求的答案,(s='aaabc'
,'abc'
是当前窗口,而'aabc'
、'aaabc'
也满足)
因此,每次满足条件的最短窗口[l,r]
,那么新增的答案数量为l+1
LeetCode 340. 至多包含 K 个不同字符的最长子串
[left,right]
之间的子数组数量LeetCode 795. 区间子数组个数
给出一个数组,求满足要求的子数组数量,要求:子数组的最大值在[left,right]
之间
如nums = [2,1,4,3], left = 2, right = 3,返回3(满足条件的三个子数组:[2], [2, 1], [3])
这里要统计子数组数目,不仅要保证计数不重不漏,还要满足特定条件
直接思路:
我们如果尝试用滑动窗口一边遍历一边统计会发现比较困难,例如窗口为[2,1]
时,最大值在[2,3]之间的子数组数量肯定不是+2,因为[1]
并不是这样的子数组;如果这里+2,统计的是最大值不超过3种的子数组数量
(怎么解决呢?如果在每个窗口的位置,依次列举并判断其包含的子数组是否满足要求,显然大大增加了计算量)
转化问题:
类比前面的前缀和思想,我们把问题转化为:
求数组的最大值<=n
的子数组数量atMost(n)
则最大值在[left,right]
之间的子数组数量 = atMost(right)
- atMost(left-1)
再特殊一点,left=right=n,就是求最大值刚好为n的子数组数量,同样可以通过
atMost(n)
-atMost(n-1)
求出
实现:atMost函数的实现就比较简单了,在前文的子数组计数的方法之上稍加修改即可
class Solution:
def numSubarrayBoundedMax(self, nums: List[int], left: int, right: int) -> int:
# 问题分解为两个子问题,它们都可以由一次遍历求出
# 数组最大值在[left, right] 内的数组数 = 数组最大值小于等于right的数组数 - 数组最大值小于等于left-1的数组数
def atMost(n):
"""求解最大值<=n的子数组数量"""
# 遍历数组中的num,以数字num结尾的,满足要求的最长连续数组长为L,那么就找到了L个符合要求的子数组
combo = 0
ans = 0
for num in nums:
if num <= n:
combo += 1
else:
combo = 0
ans += combo
return ans
return atMost(right) - atMost(left - 1)
LeetCode 992. K 个不同整数的子数组
给一个数组,求其中刚好有k种不同数字的子数组数量
如nums = [1,2,1,2,3], k = 2,返回7
(恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2].)
r
指针枚举所有可能的数组结尾,每次收缩l
指针保证当前窗口是满足要求的最长窗口[1,2]
时,刚好有2种数字的子数组数量肯定不是+2,因为[2]
并不是这样的子数组,如果这里+2,统计的是数字种类不超过2种的子数组数量数字种类<=kind种
的子数组数量为atMost(kind)
atMost(k)
- atMost(k-1)
from collections import defaultdict
class Solution:
def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
"""求最长连续子数组的长度,要求数组中元素种类==k"""
# 分解子问题
# 满足要求的数组个数 = 元素种类小于等于k的数组个数 - 元素种类小于等于k-1的数组个数
def atMost(kinds):
"""元素种类小于等于kinds的数组个数"""
L = len(nums)
# 滑动窗口
cnt = defaultdict(lambda: 0)
l = 0
# 每次向右扩展窗口,维护窗口内的元素种类数,如果元素种类>kinds,收缩窗口
types, ans = 0, 0
for r, num in enumerate(nums):
# 新入窗口的元素
cnt[num] += 1
if cnt[num] == 1:
types += 1
# 如果多于kinds种,收缩
while types > kinds:
del_num = nums[l]
cnt[del_num] -= 1
if cnt[del_num] == 0:
types -= 1
l += 1
# 此处元素种类数<=kinds
# 统计答案,以num为结尾的,符合条件的子数组数量=当前的窗口长度
ans += r - l + 1
return ans
return atMost(k) - atMost(k - 1)