一个单调队列模版,刷掉 5 道 LeetCode 硬题
文章目录
核心思想一句话:候选人排队,弱的直接出局。
队列里存下标、永远保持单调:新人来了,把队尾不如他的全踢掉;下标过期了,从队首踢掉。队首永远是当前窗口的最优解。
通用模版(递减队列,求窗口最大)
from collections import deque
dq = deque() # 存下标,队列单调
for i, x in enumerate(nums):
while dq and dq[0] < i - k + 1: # 1. 过期:踢队首
dq.popleft()
while dq and nums[dq[-1]] <= x: # 2. 单调:踢队尾
dq.pop()
dq.append(i)
best = nums[dq[0]] # 3. 队首即答案
三步走:过期踢队首,单调踢队尾,队首即答案。
下面五道题全是这个模版的换皮 👇
1️⃣ LC 239 滑动窗口最大值(Hard):模版本体
窗口大小 k,每走一步输出窗口最大值。把模版里的 best 收集起来就是答案。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
dq = deque()
res = []
for i, x in enumerate(nums):
while dq and dq[0] <= i - k:
dq.popleft()
while dq and nums[dq[-1]] <= x:
dq.pop()
dq.append(i)
if i >= k - 1:
res.append(nums[dq[0]])
return res
2️⃣ LC 1696 跳跃游戏 VI(Medium):dp 套模版
dp[i] = nums[i] + 窗口 [i-k, i-1] 内最大的 dp
"窗口最大值"四个字一出现,模版直接套。
class Solution:
def maxResult(self, nums: List[int], k: int) -> int:
n = len(nums)
dp = [0] * n
dp[0] = nums[0]
dq = deque([0])
for i in range(1, n):
while dq and dq[0] < i - k:
dq.popleft()
dp[i] = nums[i] + dp[dq[0]]
while dq and dp[dq[-1]] <= dp[i]:
dq.pop()
dq.append(i)
return dp[-1]
3️⃣ LC 1425 带限制的子序列和(Hard):1696 的双胞胎
和 1696 同构,区别只有一处:子序列可以从任意位置重新开始,所以前面是负的就不要:
dp[i] = nums[i] + max(0, 队首 dp)
class Solution:
def constrainedSubsetSum(self, nums: List[int], k: int) -> int:
n = len(nums)
dp = [0] * n
dq = deque()
ans = float('-inf')
for i, x in enumerate(nums):
while dq and dq[0] < i - k:
dq.popleft()
dp[i] = x + max(0, dp[dq[0]] if dq else 0)
ans = max(ans, dp[i])
while dq and dp[dq[-1]] <= dp[i]:
dq.pop()
dq.append(i)
return ans
4️⃣ LC 862 和至少为 K 的最短子数组(Hard):前缀和 + 递增队列
nums 有负数,滑窗双指针失效。
转成前缀和:找 j < i 使 pref[i] - pref[j] >= k 且 i - j 最短。
维护 pref 递增的队列:队首一旦满足条件就出队结算。这个 j 在当前 i 已经拿到它能贡献的最短长度,以后只会更长,直接扔掉。
class Solution:
def shortestSubarray(self, nums: List[int], k: int) -> int:
n = len(nums)
pref = [0] * (n + 1)
for i, x in enumerate(nums):
pref[i + 1] = pref[i] + x
dq = deque() # pref 递增
ans = float('inf')
for i, p in enumerate(pref):
while dq and p - pref[dq[0]] >= k:
ans = min(ans, i - dq.popleft())
while dq and pref[dq[-1]] >= p:
dq.pop()
dq.append(i)
return ans if ans != float('inf') else -1
5️⃣ LC 1438 绝对差不超过限制的最长连续子数组(Medium):双队列,这就是那 0.5 道
窗口内 max - min <= limit。
两个队列同时维护:一个递减管最大值,一个递增管最小值。超限就缩左边界,直到合法为止。
class Solution:
def longestSubarray(self, nums: List[int], limit: int) -> int:
maxdq = deque() # 递减,队首最大
mindq = deque() # 递增,队首最小
left = 0
ans = 0
for right, x in enumerate(nums):
while maxdq and nums[maxdq[-1]] <= x:
maxdq.pop()
maxdq.append(right)
while mindq and nums[mindq[-1]] >= x:
mindq.pop()
mindq.append(right)
while nums[maxdq[0]] - nums[mindq[0]] > limit:
left += 1
if maxdq[0] < left:
maxdq.popleft()
if mindq[0] < left:
mindq.popleft()
ans = max(ans, right - left + 1)
return ans
一句话记住
过期踢队首,单调踢队尾,队首即答案。
认出"窗口最值"四个字,模版直接套,O(n) 收工。
题目链接
全文完


