細聊滑動視窗

freephp發表於2024-10-20

前段時間忙於寫系列文章,怒刷演算法題的進度算是耽誤了不少。剛好遇到了一道需要滑動視窗的題目,做完之後覺得挺有意思,有必要好好聊一下滑動視窗。
所謂滑動視窗(slide window)是一種最佳化演算法的抽象概念,主要於解決陣列、字串等線性結構中的子陣列或子序列問題。它的整個思路是透過維護一個視窗(window)在陣列上滑動,每次滑動一個單元距離,從而減少重複計算。
滑動視窗一般分為2種:

固定大小視窗:視窗的大小是固定的,透過移動視窗在陣列或字串上的位置來解決問題。例如:求陣列種固定長度子陣列的最大和。

可變大小視窗:根據條件動態增大或者縮小視窗。例如:求不超過給定和的最大子陣列。

對於固定大小視窗問題,最典型的就是leetCode第三十題:

給定一個字串 s 和一個包含多個長度相同的單詞的陣列 words,要求找出 s 中所有的起始索引,這些起始索引所對應的子串正好是 words 中所有單詞的串聯排列順序不限定,但每個單詞必須用一次)。

示例:

輸入:
s = "barfoothefoobarman"
words = ["foo", "bar"]

輸出:
[0, 9]

解釋:
從索引 0 的子串是 "barfoo",它是 ["foo", "bar"] 的串聯。
從索引 9 的子串是 "foobar",它也是 ["foo", "bar"] 的串聯。

利用滑動視窗,可以很容易實現如下程式碼:

from collections import Counter
from typing import List

class Solution:
    def findSubstring(self, s: str, words: List[str]) -> List[int]:
        if not s or not words:
            return []

        word_len = len(words[0])
        word_count = len(words)
        total_len = word_len * word_count
        word_freq = Counter(words)
        result = []

        # 只遍歷 word_len 次
        for i in range(word_len):
            left = i
            right = i
            seen = Counter()
            matched_count = 0

            while right + word_len <= len(s):
                # 提取當前單詞
                word = s[right:right + word_len]
                right += word_len

                if word in word_freq:
                    seen[word] += 1
                    matched_count += 1

                    # 如果當前單詞出現次數超過了預期,移動左指標
                    while seen[word] > word_freq[word]:
                        left_word = s[left:left + word_len]
                        seen[left_word] -= 1
                        matched_count -= 1
                        left += word_len

                    # 如果匹配的單詞數量等於 words 中的單詞數量,記錄起始索引
                    if matched_count == word_count:
                        result.append(left)
                else:
                    # 當前單詞不在 words 中,重置視窗
                    seen.clear()
                    matched_count = 0
                    left = right

        return result

在上述的程式碼中,定義了left和right兩個指標,不斷地移動left和right的位置,來讓視窗中的字串符合條件。當單詞出現的頻率大於預期的數量,則把left變數向右移動一個單詞長度(word_len),並且減少seen變數中對應單詞的數量,同時減少matched_count。

當遇到不在規定中的單詞,則重置整個視窗。

這裡第一個迴圈比較取巧,只迴圈單詞長度(word_len)就能遍歷出所有的不重複結果。

第二個例子是大小不固定的視窗案例,題目是:

求陣列中和不超過 k 的最大長度子陣列。

def get_max_sub_arry_length(nums: List[int], k: int) -> int:
    left = 0
    max_length = 0
    sum = 0
    for right in range(len(nums)):
        # 擴大視窗
        sum += nums[right]

        while sum > k and left <= right:
            # 縮小視窗
            sum -= nums[left]
            left += 1
        # 重新計算最大長度
        max_length = max(max_length, right - left + 1)
    
    return max_length

nums = [1,2,3,4,5]
k = 8
print(get_max_sub_arry_length(nums, k))

相關文章