↓快轉到主要內容
  1. Algorithms/

Sliding Window 滑動視窗

·5 分鐘· ·
演算法 Sliding-Window Two-Pointers Hash-Table Monotonic-Queue
Wei Yi Chung
作者
Wei Yi Chung
Working at the contributing of open source, distributed systems, and data engineering.
目錄

介紹
#

滑動視窗處理的是「連續子陣列 / 子字串」的問題。暴力枚舉所有區間是 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 1248 Count Number of Nice Subarrays
  • LC 1493 Longest Subarray of 1’s After Deleting One Element
  • LC 2841 Maximum Sum of Almost Unique Subarray
  • LC 2461 Maximum Sum of Distinct Subarrays With Length K
  • LC 3679 Minimum Discards to Balance Inventory
  • LC 1423 Maximum Points You Can Obtain from Cards
  • LC 3694 Distinct Points Reachable After Substring Removal
  • LC 2134 Minimum Swaps to Group All 1’s Together II
  • LC 3795 Minimum Subarray Length With Distinct Sum At Least K
  • LC 1888 Minimum Number of Flips to Make the Binary String Alternating
  • LC 2653 Sliding Subarray Beauty
  • LC 2953 Count Complete Substrings
  • LC 2269 Find the K-Beauty of a Number
  • LC 2156 Find Substring With Given Hash Value
  • LC 3835 Count Subarrays With Cost Less Than or Equal to K
  • LC 1016 Binary String With Substrings Representing 1 To N
  • LC 220 Contains Duplicate III
  • LC 2024 Maximize the Confusion of an Exam
  • LC 1208 Get Equal Substrings Within Budget
  • LC 3634 Minimum Removals to Balance Array
  • LC 3090 Maximum Length Substring With Two Occurrences
  • LC 2958 Length of Longest Subarray With at Most K Frequency
  • LC 2831 Find the Longest Equal Subarray
  • LC 2779 Maximum Beauty of an Array After Applying Operation
  • LC 2271 Maximum White Tiles Covered by a Carpet
  • LC 1838 Frequency of the Most Frequent Element
  • LC 2730 Find the Longest Semi-Repetitive Substring
  • LC 2516 Take K of Each Character From Left and Right
  • LC 2106 Maximum Fruits Harvested After at Most K Steps
  • LC 2555 Maximize Win From Two Segments
  • LC 2009 Minimum Number of Operations to Make Array Continuous
  • LC 2781 Length of the Longest Valid Substring
  • LC 3413 Maximum Coins From K Consecutive Bags
  • LC 395 Longest Substring with At Least K Repeating Characters
  • LC 1763 Longest Nice Substring
  • LC 2904 Shortest and Lexicographically Smallest Beautiful String
  • LC 2968 Apply Operations to Maximize Frequency Score
  • LC 3258 Count Substrings That Satisfy K-Constraint I
  • LC 1040 Moving Stones Until Consecutive II
  • LC 2302 Count Subarrays With Score Less Than K
  • LC 2875 Minimum Size Subarray in Infinite Array
  • LC 1234 Replace the Substring for Balanced String
  • LC 2962 Count Subarrays Where Max Element Appears at Least K Times
  • LC 1358 Number of Substrings Containing All Three Characters
  • LC 3325 Count Substrings With K-Frequency Characters I
  • LC 930 Binary Subarrays With Sum
  • LC 3298 Count Substrings That Can Be Rearranged to Contain a String II
  • LC 2537 Count the Number of Good Subarrays
  • LC 3306 Count of Substrings Containing Every Vowel and K Consonants II
  • LC 992 Subarrays with K Different Integers
  • LC 2401 Longest Nice Subarray
  • LC 1156 Swap For Longest Repeated Character Substring
  • LC 1984 Minimum Difference Between Highest and Lowest of K Scores
  • LC 3323 Minimize Connected Groups by Inserting Interval
  • LC 1425 Constrained Subsequence Sum
  • LC 4067 Longest Subarray With Restricted Pair Sums
  • LC 424 Longest Repeating Character Replacement
  • LC 219 Contains Duplicate II
  • LC 187 Repeated DNA Sequences

相關文章

LeetCode 142: Linked List Cycle II
·1 分鐘
LeetCode Medium Hash-Table Linked-List Two-Pointers Floyds-Cycle-Finding-Algorithm
LeetCode 142 解題筆記:快慢指針相遇後,一個回到 head,兩個都一次一步,再相遇的地方就是環的入口
LeetCode 187: Repeated DNA Sequences
·1 分鐘
LeetCode Medium Hash-Table String Bit-Manipulation Sliding-Window
LeetCode 187 解題筆記:把每個長度 10 的子字串丟進雜湊表計數,出現超過一次的就是答案
LeetCode 424: Longest Repeating Character Replacement
·1 分鐘
LeetCode Medium Hash-Table String Sliding-Window
LeetCode 424 解題筆記:可變長度滑動視窗,視窗長度減掉最多的字元次數就是要替換的數量,超過 k 就縮左邊
LeetCode 653: Two Sum IV - Input is a BST
·1 分鐘
LeetCode Easy Hash-Table Two-Pointers Tree Depth-First-Search
LeetCode 653 解題筆記:遍歷時用集合記看過的值,k - val 已經在集合裡就找到了
LeetCode 594: Longest Harmonious Subsequence
·1 分鐘
LeetCode Easy Array Hash-Table Sliding-Window Sorting
LeetCode 594 解題筆記:子序列不用連續,所以只要計數,每個 x 看 x 和 x + 1 的次數加起來
LeetCode 349: Intersection of Two Arrays
·1 分鐘
LeetCode Easy Array Hash-Table Two-Pointers Binary-Search
LeetCode 349 解題筆記:兩個陣列各轉成集合取交集