介紹#
滑動視窗處理的是「連續子陣列 / 子字串」的問題。暴力枚舉所有區間是 O(n^2),滑動視窗讓左右邊界都只往右走,每個元素只進出視窗各一次,降到 O(n)。
關鍵是視窗移動時,只更新進來和出去的那一個元素,不要重算整個視窗。
| 形式 | 視窗怎麼動 | 例子 |
|---|---|---|
| 固定長度 | 右邊進一個、左邊出一個 | 長度 k 的最大和、每個視窗的統計 |
| 可變長度,求最長 | 右邊一直擴,不合法時左邊縮到合法 | 最多 k 個 0 的最長子陣列 |
| 可變長度,求最短 | 右邊一直擴,合法時左邊盡量縮 | 和至少 k 的最短子陣列 |
| 數子陣列個數 | 以 right 結尾的合法子陣列有 right - left + 1 個 | 「剛好 K」用「最多 K」相減 |
| 視窗內的最大 / 最小值 | 單調佇列 | 滑動視窗最大值 |
能用可變長度視窗的前提是單調性:視窗變大時條件只會越來越難(或越來越容易)滿足。如果陣列有負數、條件不單調,通常要改用前綴和 + 雜湊表。
模板一:固定長度視窗#
window = 0
for right, x in enumerate(nums):
window += x # 進來
if right >= k:
window -= nums[right - k] # 出去
if right >= k - 1: # 視窗滿了才更新答案
res = max(res, window)
視窗裡要維護的東西不一定是總和:
- 雜湊表 / 計數陣列:視窗內有幾種數(LC 2841、2461),第
x小的負數(LC 2653,值域小,用計數陣列)。 - 「刪掉一段」轉成「留下一段」:從兩端拿
k張牌 = 中間留下長度n - k的連續一段,求最小和(LC 1423);刪掉長度k的子字串 = 剩下的位移 = 總位移減視窗位移(LC 3694)。 - 環狀陣列:把陣列接兩次,或用
i % n(LC 2134、1888)。 - 滾動雜湊:視窗移動時,乘上 / 除以
power,只更新頭尾(LC 2156)。
模板二:可變長度,求最長#
left = 0
for right, x in enumerate(nums):
加入(x)
while 視窗不合法():
移除(nums[left])
left += 1
res = max(res, right - left + 1) # 縮完之後一定合法
- 最多 1 個 0:刪掉一個元素後全是 1 的最長長度,就是「最多一個 0 的最長視窗」再減 1(LC 1493)。
模板三:可變長度,求最短#
left = 0
for right, x in enumerate(nums):
加入(x)
while 視窗合法():
res = min(res, right - left + 1) # 合法時更新,再試著縮
移除(nums[left])
left += 1
- 不同值的和至少
k(LC 3795):同一個值在視窗裡出現多次只算一次。記錄每個值最後一次出現的位置,左邊界移出時,只有它是最後一次出現才扣掉。
模板四:數子陣列個數#
以 right 結尾、左端在 [left, right] 的子陣列都合法,共 right - left + 1 個:
def at_most(k):
left = count = 0
for right, x in enumerate(nums):
加入(x)
while 超過(k):
移除(nums[left])
left += 1
count += right - left + 1
return count
return at_most(k) - at_most(k - 1) # 剛好 k
「剛好 K」不單調,但「最多 K」單調,所以用兩次「最多」相減(LC 1248)。如果條件是「花費 <= k」本身就單調,直接數就好(LC 3835)。
「至少 K」的寫法:視窗合法時就一直縮左邊,縮到剛好不合法為止。這時左端放在 [0, left) 的任何位置都合法,所以 count += left:
left = count = 0
for right, x in enumerate(nums):
加入(x)
while 視窗合法():
移除(nums[left])
left += 1
count += left # 左端在 [0, left) 都合法
(LC 2962、1358、3325、3298、2537)。「剛好 K」也可以寫成「至少 K」減「至少 K + 1」(LC 3306)。
模板五:單調佇列維護視窗最值#
from collections import deque
max_q = deque() # 存 index,值遞減
for right, x in enumerate(nums):
while max_q and nums[max_q[-1]] <= x:
max_q.pop() # 被 x 支配,之後不可能是最大值
max_q.append(right)
left = right - k + 1 # 固定長度 k;可變長度時 left 由條件決定
if max_q[0] < left:
max_q.popleft() # 過期:已經在視窗外
if left >= 0:
res.append(nums[max_q[0]]) # 視窗最大值
兩個條件要分開:尾端丟掉被支配的,頭端丟掉過期的。同時要最大和最小值,就開兩個佇列(LC 3835)。
DP + 單調佇列:dp[i] = nums[i] + max(0, dp[i-k..i-1] 的最大值),前 k 個 dp 的最大值就是一個滑動視窗最大值(LC 1425)。
主要參數說明#
left、right:視窗是[left, right],長度right - left + 1。- 固定長度時,出去的元素是
nums[right - k],視窗滿的條件是right >= k - 1。 - 計數用的雜湊表:值減到 0 時要刪掉,才能用
len()當作「幾種數」。
複雜度#
每個元素進出視窗各一次,時間 O(n)。視窗內用雜湊表時空間是 O(視窗內不同值的數量)。
常見錯誤#
這些都是我練習時實際寫錯過的:
- 出去的元素用錯變數:複製貼上後,判斷左邊元素時用的還是右邊的期望值(LC 1888)。
- 被丟掉的元素還留在計數裡:題目規定要丟掉的到貨,之後離開視窗時不應該再扣一次(LC 3679)。
- 第一個視窗沒算到:更新答案的條件寫成
i - k >= 0,少了i == k - 1那個視窗(LC 3694)。 - 條件寫成總和,題目要的是不同值的和(LC 3795)。
- 沒檢查視窗長度剛好等於目標(LC 2953)。
- 次方沒有取模:
power ** k每次重算大整數,TLE(LC 2156)。 - 單調佇列把「過期」和「被支配」混在同一個條件(LC 3835)。
- 縮視窗時忘了更新計數:
while裡只移動了left,沒有把離開的元素從計數裡扣掉(LC 2537)。 - 離開視窗時的門檻寫死:「至少
k次」的計數要在數量從k變成k - 1時才減,我寫成== 1(LC 3325)。
相關 LeetCode#
LC 1248Count Number of Nice SubarraysLC 1493Longest Subarray of 1’s After Deleting One ElementLC 2841Maximum Sum of Almost Unique SubarrayLC 2461Maximum Sum of Distinct Subarrays With Length KLC 3679Minimum Discards to Balance InventoryLC 1423Maximum Points You Can Obtain from CardsLC 3694Distinct Points Reachable After Substring RemovalLC 2134Minimum Swaps to Group All 1’s Together IILC 3795Minimum Subarray Length With Distinct Sum At Least KLC 1888Minimum Number of Flips to Make the Binary String AlternatingLC 2653Sliding Subarray BeautyLC 2953Count Complete SubstringsLC 2269Find the K-Beauty of a NumberLC 2156Find Substring With Given Hash ValueLC 3835Count Subarrays With Cost Less Than or Equal to KLC 1016Binary String With Substrings Representing 1 To NLC 220Contains Duplicate IIILC 2024Maximize the Confusion of an ExamLC 1208Get Equal Substrings Within BudgetLC 3634Minimum Removals to Balance ArrayLC 3090Maximum Length Substring With Two OccurrencesLC 2958Length of Longest Subarray With at Most K FrequencyLC 2831Find the Longest Equal SubarrayLC 2779Maximum Beauty of an Array After Applying OperationLC 2271Maximum White Tiles Covered by a CarpetLC 1838Frequency of the Most Frequent ElementLC 2730Find the Longest Semi-Repetitive SubstringLC 2516Take K of Each Character From Left and RightLC 2106Maximum Fruits Harvested After at Most K StepsLC 2555Maximize Win From Two SegmentsLC 2009Minimum Number of Operations to Make Array ContinuousLC 2781Length of the Longest Valid SubstringLC 3413Maximum Coins From K Consecutive BagsLC 395Longest Substring with At Least K Repeating CharactersLC 1763Longest Nice SubstringLC 2904Shortest and Lexicographically Smallest Beautiful StringLC 2968Apply Operations to Maximize Frequency ScoreLC 3258Count Substrings That Satisfy K-Constraint ILC 1040Moving Stones Until Consecutive IILC 2302Count Subarrays With Score Less Than KLC 2875Minimum Size Subarray in Infinite ArrayLC 1234Replace the Substring for Balanced StringLC 2962Count Subarrays Where Max Element Appears at Least K TimesLC 1358Number of Substrings Containing All Three CharactersLC 3325Count Substrings With K-Frequency Characters ILC 930Binary Subarrays With SumLC 3298Count Substrings That Can Be Rearranged to Contain a String IILC 2537Count the Number of Good SubarraysLC 3306Count of Substrings Containing Every Vowel and K Consonants IILC 992Subarrays with K Different IntegersLC 2401Longest Nice SubarrayLC 1156Swap For Longest Repeated Character SubstringLC 1984Minimum Difference Between Highest and Lowest of K ScoresLC 3323Minimize Connected Groups by Inserting IntervalLC 1425Constrained Subsequence SumLC 4067Longest Subarray With Restricted Pair SumsLC 424Longest Repeating Character ReplacementLC 219Contains Duplicate IILC 187Repeated DNA Sequences
