leetcode(13)滑动窗口系列题目
209. 长度最小的子数组
向右移动右边界,若>=target则收缩左边界
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
res = float('inf')
sum_ = 0
left = 0
for right in range(len(nums)):
sum_ += nums[right]
while sum_ >= target:
sum_ -= nums[left]
res = min(res, right - left + 1)
left += 1
# print(res)
return res if res != float('inf') else 0
904. 水果成篮
其实就是选只有两个元素的最长连续子序列,比如1,2,3,2,2最长就是2,3,2,2(只包括2或者3,而且是最长的)
注意:与209. 长度最小的子数组的区别是使用字典记录不同数字的个数,如果字典长度超过2就收缩左边界
class Solution:
def totalFruit(self, fruits: List[int]) -> int:
left, right, res = 0, 0, 0
dic = Counter()
while right < len(fruits):
dic[fruits[right]] += 1
while len(dic) > 2:
dic[fruits[left]] -= 1
if dic[fruits[left]] == 0:
del dic[fruits[left]]
left += 1
right += 1
res = max(res, right - left)
return res
76. 最小覆盖子串
注意:分别用cnt和need记录每个字符的个数和字符串的总长度
当t中的所有字符都在当前的子串中出现过时收缩左边界,即need == 0
class Solution:
def minWindow(self, s: str, t: str) -> str:
if len(s) < len(t):
return ''
cnt, need = Counter(t), len(t)
left, minL = 0, len(s) + 1
start, end = 0, -1
for right in range(len(s)):
# if s[right] in cnt:
if cnt[s[right]] > 0:
need -= 1
cnt[s[right]] -= 1
while need == 0:
if right - left + 1 < minL:
minL = right - left + 1
start, end = left, right
# if s[left] in cnt:
if cnt[s[left]] == 0:
need += 1
cnt[s[left]] += 1
left += 1
return s[start: end + 1]
3. 无重复字符的最长子串
借助哈希set去重
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if not s:
return 0
n = len(s)
left = 0
maxL, curL = 0, 0
lookup = set()
for i in range(n):
curL += 1
while s[i] in lookup:
curL -= 1
lookup.remove(s[left])
left += 1
maxL = max(maxL, curL)
lookup.add(s[i])
return maxL
239. 滑动窗口最大值
注意:为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」。
单调队列真是一种让人感到五味杂陈的数据结构,它的维护过程更是如此.....就拿此题来说,队头最大,往队尾方向单调......有机会站在队头的老大永远心狠手辣,当它从队尾杀进去的时候,如果它发现这里面没一个够自己打的,它会毫无人性地屠城,把原先队里的人头全部丢出去,转身建立起自己的政权,野心勃勃地准备开创一个新的王朝.....这时候,它的人格竟发生了一百八十度大反转,它变成了一位胸怀宽广的慈父!它热情地请那些新来的“小个子”们入住自己的王国......然而,这些小个子似乎天性都是一样的——嫉妒心强,倘若见到比自己还小的居然更早入住王国,它们会心狠手辣地找一个夜晚把它们通通干掉,好让自己享受更大的“蛋糕”;当然,遇到比自己强大的,它们也没辙,乖乖夹起尾巴做人。像这样的暗杀事件每天都在上演,虽然王国里日益笼罩上白色恐怖,但是好在没有后来者强大到足以干翻国王,江山还算能稳住。直到有一天,闯进来了一位真正厉害的角色,就像当年打江山的国王一样,手段狠辣,野心膨胀,于是又是大屠城......历史总是轮回的。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
que = collections.deque() # 队列中存储的是数值对应的数组下标位置
for i in range(k): # 一轮遍历,先把窗口塞满
while que and nums[i] >= nums[que[-1]]:
que.pop() # 队尾元素较小则删除
que.append(i)
res = [nums[que[0]]] # 将第一个元素,即最大元素返回
for i in range(k,n):
while que and nums[i] >= nums[que[-1]]:
que.pop()
que.append(i)
while que[0] <= i - k: # 判断队首的下标位置是否在当前窗口内
que.popleft()
res.append(nums[que[0]])
return res
6098. 统计得分小于 K 的子数组数目
注意:与209. 长度最小的子数组的区别是s * (right - left + 1) >= k时收缩左边界
只要某个子数组满足题目要求,在该子数组内的更短的子数组同样也满足题目要求。
class Solution:
def countSubarrays(self, nums: List[int], k: int) -> int:
res = left = s = 0
for right, v in enumerate(nums):
s += v
while s * (right - left + 1) >= k:
s -= nums[left]
left += 1
res += right - left + 1
return res
6169. 最长优雅子数组
遍历数组中的元素 num,
当前 cur 与 n 按位与的结果不为 0,则从cur里面去掉最左边那个元素cur ^= nums[left],收缩左边界left += 1。(因为之前已经通过异或^加进来了,去除就再异或一次即可)
遍历元素过程中,记录下最大窗口长度即可。
class Solution:
def longestNiceSubarray(self, nums: List[int]) -> int:
left = cur = res = 0
for right, n in enumerate(nums):
while cur & n != 0:
cur ^= nums[left]
left += 1
cur ^= n
res = max(res, right - left + 1)
return res
2447. 最大公因数等于 K 的子数组数目
第一步:
第二步:
第三步:
第四步:
第五步:
第六步:
利用函数 gcd() 求两个数的最大公因数
若cur < k
则表示以i开头的子数组已经不满足要求
class Solution:
def subarrayGCD(self, nums: List[int], k: int) -> int:
res = 0
for i in range(len(nums)):
cur = nums[i]
for j in range(i, len(nums)):
cur = gcd(cur, nums[j])
if cur == k:
res += 1
elif cur < k:
break
return res
2470. 最小公倍数为 K 的子数组数目
利用函数 lcm() 求两个数的最小公倍数
class Solution:
def subarrayLCM(self, nums: List[int], k: int) -> int:
res = 0
n = len(nums)
for i in range(n):
if k % nums[i] == 0: # 如果不能整除k直接遍历下一个
l = nums[i]
for j in range(i, n):
l = lcm(l, nums[j])
if l == k:
res += 1
return res
862. 和至少为 K 的最短子数组
前缀和+双端队列(单调队列)
遍历前缀和数组 s,对于遍历到的下标 i,如果 s[i] - s[q.front]≥k,说明当前遇到了一个可行解,更新答案。此时,将队首元素出队,直到队列为空或者 s[i] - s[q.front]<k 为止。
如果此时队列不为空,为了维持队列的严格单调递增,还需要判断队尾元素是否需要出队,如果 s[q.back] ≥s[i],则需要循环将队尾元素出队,直到队列为空或者 s[q.back] <s[i] 为止。然后将下标 i 入队。
遍历结束,如果没有找到可行解,那么返回 -1。否则,返回答案。
class Solution:
def shortestSubarray(self, nums: List[int], k: int) -> int:
s = list(accumulate(nums, initial = 0))
print(s)
q = deque()
res = inf
for i, v in enumerate(s):
print(q)
while q and v - s[q[0]] >= k:
res = min(res, i - q.popleft())
while q and s[q[-1]] >= v:
q.pop()
q.append(i)
return res if res != inf else -1
1438. 绝对差不超过限制的最长连续子数组
Python 中 sortedcontainers 实现了有序的容器
使用 left 和 right 两个指针,分别指向滑动窗口的左右边界;定义 SortedList 保存滑动窗口的所有元素;
- right 主动右移:right 指针每次移动一步,把 A[right] 放入滑动窗口;
- left 被动右移:判断此时窗口内最大值和最小值的差,如果大于 limit,则 left 指针被迫右移,直至窗口内最大值和最小值的差小于等于 limit 为止;left 每次右移之前,需要把 A[left] 从 SortedList 中减去一次。
滑动窗口长度的最大值就是所求。
class Solution:
def longestSubarray(self, nums: List[int], limit: int) -> int:
from sortedcontainers import SortedList
s = SortedList()
res = 0
left = right = 0
while right < len(nums):
s.add(nums[right])
while s[-1] - s[0] > limit:
s.remove(nums[left])
left += 1
res = max(res, right - left + 1)
right += 1
return res
480. 滑动窗口中位数
用SortedList维持顺序
class Solution:
def medianSlidingWindow(self, nums: List[int], k: int) -> List[float]:
from sortedcontainers import SortedList
s = SortedList()
left, right = 0, k-1
for i in range(k - 1):
s.add(nums[i])
res = []
while right < len(nums):
s.add(nums[right])
# print(s)
if k%2:
res.append(s[k//2])
else:
res.append((s[k//2] + s[k//2 - 1])/2)
s.remove(nums[left])
right += 1
left += 1
return res
795. 区间子数组个数
从i0到i1有i1 - i0个满足要求的子数组
class Solution:
def numSubarrayBoundedMax(self, nums: List[int], left: int, right: int) -> int:
res, i0, i1 = 0, -1, -1
for i, x in enumerate(nums):
if x > right: i0 = i
if x >= left: i1 = i
res += i1 - i0
return res
2444. 统计定界子数组的数目
与795. 区间子数组个数 的区别就是多了一个最小值的约束
当遍历到 nums[i] 时,如果 minK 和 maxK 之前出现过,则左端点 ≤min(minI,maxI) 的子数组都是合法的,合法子数组的个数为 min(minI,maxI)+1
上一个在 [minK,maxK] 范围之外的 nums[i] 的下标,记作 i0。此时合法子数组的个数为 min(minI,maxI)−i0
如果 min(minI,maxI)−i0<0,则表示在 i0右侧 minK 和 maxK 没有同时出现,此时合法子数组的个数为 0
class Solution:
def countSubarrays(self, nums: List[int], minK: int, maxK: int) -> int:
res, min_i, max_i, i0 = 0, -1, -1, -1
for i, n in enumerate(nums):
if n == minK: min_i = i
if n == maxK: max_i = i
if n > maxK or n < minK: i0 = i
res += max(0, min(min_i, max_i) - i0)
return res
1687. 从仓库到码头运输箱子
动态规划 + 单调队列
f[i] 表示把前 i 个箱子从仓库运送到相应码头的最少行程数
class Solution:
def boxDelivering(self, boxes: List[List[int]], portsCount: int, maxBoxes: int, maxWeight: int) -> int:
n = len(boxes)
pre_w = list(accumulate((box[1] for box in boxes), initial = 0))
dis = [int(a != b) for a, b in pairwise(box[0] for box in boxes)]
pre_d = list(accumulate(dis, initial = 0))
dp = [0] * (n + 1)
q = deque([0])
for i in range(1, n + 1):
# 不能继续往后运,需要重新回到仓库,或回到上一个满足要求的码头
while q and (i - q[0] > maxBoxes or pre_w[i] - pre_w[q[0]] > maxWeight):
q.popleft()
if q: # 上一个满足要求的码头+从该码头到第i个的行程
dp[i] = dp[q[0]] + pre_d[i - 1] - pre_d[q[0]] + 2
if i < n: # 直接送到第i个码头是最少行程
while q and dp[q[-1]] - pre_d[q[-1]] >= dp[i] - pre_d[i]:
q.pop()
q.append(i)
return dp[-1]
参考资料:
闪电五连鞭带你秒杀12道中档题
滑动窗口3. 无重复字符的最长子串
滑动窗口76. 最小覆盖子串
『 一招吃遍七道 』滑动窗口的应用