滑動視窗 — 進階視窗形態
範圍 — 第一輪可以先跳過的滑動視窗技巧:至多 K 個相異的家族、帶禁用元素重置或一次掃描的恰好 K 計數、同時處理多個不重疊的視窗,以及以單字而非字元為鍵的視窗;七個必會模板留在主文件,而由別份文件負責的進階型態(雙端佇列求極值、補集視窗、數值分桶)在這裡只留指引。 另見:sliding_window.md — 這裡每個技巧所依據的七個標準模板;sliding_window_examples.md — 不需要進階技巧的實作題;monotonic_queue.md — 用雙端佇列求視窗極值;prefix_sum_advanced.md — 補集視窗(「從兩端拿」);prefix_sum.md — 視窗總和可能為負時,改用前綴和的做法。
LeetCode 題目清單
總覽
下面每個技巧都是 sliding_window.md 的某個模板換掉一個欄位:收縮條件數的是相異鍵的個數、視窗被禁用元素重置、合法起點的個數跨步驟帶著走、同時替多個視窗打分,或視窗一次走一整個單字。至多 K 個相異(模板 1)和恰好 K 的重置(模板 2)在 L3 面試會出現;其餘較少見,每張卡都會說明通常哪個較簡單的模板就夠了。
先備知識
- sliding_window § 模板 2:先擴後縮的迴圈。這裡每個模板都保留它。
- sliding_window § 模板 3:最長的合法視窗。模板 1 就是它加上相異個數的判斷。
- sliding_window § 模板 6:用
at_most(K) − at_most(K − 1)求恰好 K。模板 2 與 3 延伸它。 - sliding_window § 模板 1:固定大小的視窗。模板 4 與 5 建立在它上面。
辨識
| 題目敘述中的訊號 | 限制提示 | 該用 | 不適用時 |
|---|---|---|---|
| 「至多 K 個相異值的最長子陣列」 | n ≤ 10⁵ | 模板 1 | 相異值的個數必須恰好是 K,而且是計數不是量長度 → 模板 2 |
| 「只用這些字元、恰好 K 個相異的子字串有幾個」 | n ≤ 10⁵ | 模板 2 | 每個元素都允許 → 照原樣用 sliding_window § 模板 6 |
| 「恰好 k 個奇數/1 的子陣列有幾個」,一次掃描 | n ≤ 5·10⁴,值 ≥ 0 | 模板 3 | 值可能為負,或目標是總和 → 前綴和 + hash map |
| 「三個(或兩個)長度為 k、不重疊、總和最大的子陣列」 | n ≤ 2·10⁴ | 模板 4 | 視窗個數是參數 → 在(已用視窗數、前綴)上做 DP |
| 「所有單字串接而成」,每個單字等長 | 單字長度 ≤ 30 | 模板 5 | 視窗是單一字元 → hash_map § 模板 5 |
| 「大小為 k 的每個視窗的最大/最小值」 | n ≤ 10⁵ | 單調雙端佇列 — monotonic_queue § 模板 1 | 答案是中位數 → 兩個 heap,monotonic_queue § 模板 4 |
| 「從兩端拿 k 張牌」、「從兩端移除直到總和為 x」 | n ≤ 10⁵,值 ≥ 1 | 補集視窗 — prefix_sum_advanced § 模板 9 | 可以從中間拿 → 剩下的不是一個視窗 |
「abs(i − j) ≤ k 而且 abs(nums[i] − nums[j]) ≤ t」 |
n ≤ 10⁵ | 寬度 t + 1 的數值分桶 — sort § 模板 8 |
值必須相等 → 在索引視窗上用一般的 set(LC 219) |
至多 K 與恰好 K 計數
模板 1:至多 K 個相異 — LC 904 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
辨識 — 「至多 K 個相異值的最長子陣列/子字串」、「兩個籃子,每籃只放一種水果」。n ≤ 10⁵ → O(n)。
關鍵 — len(basket) 就是視窗中相異值的個數,因為某個鍵的計數一到 0 就被刪掉。至多 K 個相異的視窗收縮後仍然合法,所以模板 3 的迴圈加上 while len(basket) > K 就能用。
# python
# LC 904 - Fruit Into Baskets
# IDEA: longest window with at most 2 distinct values; shrink while the map has more than 2 keys
# time = O(n), space = O(1) (the map holds at most 3 keys)
def totalFruit(fruits):
basket = {} # fruit type -> count in the window
left = 0
best = 0
for right in range(len(fruits)):
f = fruits[right]
basket[f] = basket.get(f, 0) + 1
while len(basket) > 2: # more than 2 types -> shrink
out = fruits[left]
basket[out] -= 1
if basket[out] == 0:
del basket[out] # len(basket) must stay the true number of types
left += 1
best = max(best, right - left + 1)
return best
// java
// LC 904 - Fruit Into Baskets
// IDEA: longest window with at most 2 distinct values; shrink while the map has more than 2 keys
// time = O(n), space = O(1)
public int totalFruit(int[] fruits) {
Map<Integer, Integer> basket = new HashMap<>(); // fruit type -> count in the window
int left = 0;
int best = 0;
for (int right = 0; right < fruits.length; right++) {
basket.put(fruits[right], basket.getOrDefault(fruits[right], 0) + 1);
while (basket.size() > 2) { // more than 2 types -> shrink
int out = fruits[left];
basket.put(out, basket.get(out) - 1);
if (basket.get(out) == 0) {
basket.remove(out); // size() must stay the true type count
}
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}
複雜度
- 時間 O(n):
left只往前走,內層while總共最多跑 n 次。 - 空間 O(K):
basket最多有 K + 1 個鍵,因為while會立刻移掉多出來的那一個。
陷阱
- 只減計數不
del:在[1, 2, 3, 2, 2]上,1 的零計數項讓len(basket)停在 3,while永遠停不下來,把left推出陣列尾端(IndexError,而不是 4)。 - 在
while裡面記錄best,量到的是有 K + 1 種的視窗:[1, 2, 3]會回報 3。
不適用時 — 題目問的是有幾個子陣列恰好有 K 個相異值 → 帶計數欄位把這個迴圈跑兩次(sliding_window § 模板 6)。
練習 — LC 904 · LC 159 (同一題換成字元) · LC 340 (> K 取代 > 2) · LC 992 (恰好 K:這個迴圈跑兩次並計數)
模板 2:帶禁用元素重置的恰好 K — LC 2062 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
辨識 — 「只用母音、而且五個都要有的子字串有幾個」、「恰好 K 個相異,而且子字串不能含 …」。n ≤ 10⁵。
關鍵 — 至多 K 個相異值的視窗,減掉至多 K − 1 個的,剛好就是恰好 K 個的視窗。被計入的視窗都不能含禁用字元,所以 at_most 遇到它就重置:清空 map、把 left 移到它後面。相減的式子不變,因為兩次呼叫在同樣的位置重置。
# python
# LC 2062 - Count Vowel Substrings of a String
# IDEA: exactly 5 distinct vowels = at_most(5) - at_most(4); a consonant resets the window
# time = O(n), space = O(1) (at most 5 keys)
def countVowelSubstrings(word):
vowels = set("aeiou")
def at_most(max_distinct):
count = {}
left = 0
total = 0
for right in range(len(word)):
ch = word[right]
if ch not in vowels: # a consonant ends every window: reset past it
count.clear()
left = right + 1
continue
count[ch] = count.get(ch, 0) + 1
while len(count) > max_distinct:
out = word[left]
count[out] -= 1
if count[out] == 0:
del count[out]
left += 1
total += right - left + 1 # vowel-only substrings ending at right with <= max_distinct
return total
return at_most(5) - at_most(4)
// java
// LC 2062 - Count Vowel Substrings of a String
// IDEA: exactly 5 distinct vowels = atMost(5) - atMost(4); a consonant resets the window
// time = O(n), space = O(1)
public int countVowelSubstrings(String word) {
return atMost(word, 5) - atMost(word, 4);
}
private int atMost(String word, int maxDistinct) {
Map<Character, Integer> count = new HashMap<>();
int left = 0;
int total = 0;
for (int right = 0; right < word.length(); right++) {
char ch = word.charAt(right);
if ("aeiou".indexOf(ch) < 0) { // a consonant ends every window
count.clear();
left = right + 1;
continue;
}
count.put(ch, count.getOrDefault(ch, 0) + 1);
while (count.size() > maxDistinct) {
char out = word.charAt(left);
count.put(out, count.get(out) - 1);
if (count.get(out) == 0) {
count.remove(out);
}
left++;
}
total += right - left + 1;
}
return total;
}
複雜度
- 時間 O(n):兩次
at_most,每次的left都只往前走。 - 空間 O(1):
count最多放六個母音。
試跑 — 為什麼相減剛好算出恰好 K 個,以 [1, 2, 1, 3]、K = 2 為例:
right at_most(2) adds at_most(1) adds
0 [1] 1 [1] 1
1 [2] [1,2] 2 [2] 1
2 [1] [2,1] [1,2,1] 3 [1] 1
3 [3] [1,3] 2 [3] 1
total 8 4 exactly 2 = 8 - 4 = 4: [1,2] [1,2,1] [2,1] [1,3]
陷阱
- 忘了重置:在
"aeiouxa"上視窗會跨過x,答案變成 3 而不是 1。 - 推廣成
exactly(k)時,k = 0會呼叫at_most(−1),它的while len(count) > −1永遠在收縮;相減前先對k = 0回傳 0。 - 把兩個最長視窗的長度相減沒有任何意義:這個恆等式對計數(
total += right − left + 1)成立,對max(len)不成立。
不適用時 — 條件是可能為負的總和,或是任何「至多」都表達不了的等式(負數下的「總和等於 k」,LC 560)→ hash_map § 模板 4。
練習 — LC 2062 · LC 992 (沒有禁用元素) · LC 930 (恰好 goal 個 1) · LC 1248 (恰好 k 個奇數;也可用模板 3)
模板 3:一次掃描的恰好 K — 數左側間隙 — LC 1248 優先度 3/5 — 值得會 — 多半是必備模式的變形
辨識 — 「恰好 k 個奇數/1 的子陣列有幾個」,值是指示性的(奇或偶、1 或 0),而且要求一次掃描、O(1) 空間。
關鍵 — 視窗一旦含有 k 個奇數,從 left 到第一個奇數之間的每個起點都給出一個合法子陣列。while odd == k 迴圈把 left 走過這些起點,並把它們數進 gap。在新的奇數出現之前,gap 對之後每個 right 都仍然成立,所以每一步都加上它,只在遇到奇數時重置。
# python
# LC 1248 - Count Number of Nice Subarrays
# IDEA: gap = number of valid starts for the current k odds; add it at every right, reset on a new odd
# time = O(n), space = O(1)
def numberOfSubarrays(nums, k):
left = 0
odd = 0
gap = 0 # valid starts for the current run of k odds
total = 0
for right in range(len(nums)):
if nums[right] % 2 == 1:
odd += 1
gap = 0 # a new odd: the k odds in play changed, recount the starts
while odd == k:
gap += 1 # start at `left` is valid; try the next one
if nums[left] % 2 == 1:
odd -= 1
left += 1
total += gap # every start counted in gap still works for this right
return total
// java
// LC 1248 - Count Number of Nice Subarrays
// IDEA: gap = number of valid starts for the current k odds; add it at every right, reset on a new odd
// time = O(n), space = O(1)
public int numberOfSubarrays(int[] nums, int k) {
int left = 0;
int odd = 0;
int gap = 0; // valid starts for the current run of k odds
int total = 0;
for (int right = 0; right < nums.length; right++) {
if (nums[right] % 2 == 1) {
odd++;
gap = 0; // a new odd: recount the valid starts
}
while (odd == k) {
gap++;
if (nums[left] % 2 == 1) {
odd--;
}
left++;
}
total += gap;
}
return total;
}
複雜度
- 時間 O(n):
left總共只走過陣列一次。 - 空間 O(1):四個整數。前綴和 + hash map 的版本是 O(n) 空間。
試跑 — nums = [2, 2, 1, 2, 1], k = 2:
right value odd gap action total
0 2 0 0 0
1 2 0 0 0
2 1 1 0 new odd, gap reset 0
3 2 1 0 0
4 1 2 0 new odd -> while: skip 2, 2, 1 3
3 (starts 0, 1, 2 are valid)
answer 3: [2,2,1,2,1] [2,1,2,1] [1,2,1]
陷阱
- 遇到新奇數時沒有重置
gap:[1, 1, 1], k = 1在第二個奇數之後還在加舊的間隙,回傳 6 而不是 3。 - 只在
while裡面計數:第 k 個奇數之後的偶數([1, 2, 2], k = 1)每個都會延長所有已算過的子陣列,這些會被漏掉 — 得到 1 而不是 3。
不適用時 — 值可能為負,或目標是任意值的總和 → 前綴和 + hash map,prefix_sum § 模板 6(同一題,用 {prefix: count} 計數);重視清楚勝過一次掃描時,模板 2 的相減比較好解釋。
練習 — LC 1248 · LC 930 (用 1 取代奇數) · LC 1358 (數包含三種字母的視窗:同樣是「第一個必要字元之前的每個起點」的計數)
同時處理多個視窗
模板 4:不重疊的固定視窗 — 前綴/後綴 argmax — LC 689 優先度 3/5 — 值得會 — 多半是必備模式的變形
辨識 — 「三個長度為 k、不重疊、總和最大的子陣列」、「兩個長度分別為 L 與 M 的不重疊子陣列」。n ≤ 2·10⁴。
關鍵 — 固定中間的視窗。最好的左視窗就落在 [0, mid − k] 的任何位置,最好的右視窗落在 [mid + k, end] 的任何位置,兩者互相獨立,所以在視窗總和上做一次前綴 argmax 和一次後綴 argmax,每個 mid 都能 O(1) 回答。
# python
# LC 689 - Maximum Sum of 3 Non-Overlapping Subarrays
# IDEA: window sums w[i]; prefix argmax and suffix argmax of w; try every middle window
# time = O(n), space = O(n)
def maxSumOfThreeSubarrays(nums, k):
n = len(nums)
w = [0] * (n - k + 1) # w[i] = sum of the window starting at i
cur = sum(nums[:k])
w[0] = cur
for i in range(k, n):
cur += nums[i] - nums[i - k]
w[i - k + 1] = cur
m = len(w)
left = [0] * m # left[i] = best start in [0, i]
right = [0] * m # right[i] = best start in [i, m - 1]
best = 0
for i in range(m):
if w[i] > w[best]: # strict: the earliest start wins a tie
best = i
left[i] = best
best = m - 1
for i in range(m - 1, -1, -1):
if w[i] >= w[best]: # >= while scanning leftwards: the earliest wins
best = i
right[i] = best
ans = None
best_sum = -1
for mid in range(k, m - k): # the middle window leaves room on both sides
l = left[mid - k]
r = right[mid + k]
s = w[l] + w[mid] + w[r]
if s > best_sum:
best_sum = s
ans = [l, mid, r]
return ans
// java
// LC 689 - Maximum Sum of 3 Non-Overlapping Subarrays
// IDEA: window sums w[i]; prefix argmax and suffix argmax of w; try every middle window
// time = O(n), space = O(n)
public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
int n = nums.length;
int[] w = new int[n - k + 1]; // w[i] = sum of the window starting at i
int cur = 0;
for (int i = 0; i < n; i++) {
cur += nums[i];
if (i >= k) cur -= nums[i - k];
if (i >= k - 1) w[i - k + 1] = cur;
}
int m = w.length;
int[] left = new int[m];
int[] right = new int[m];
int best = 0;
for (int i = 0; i < m; i++) { // strict > : earliest tie wins
if (w[i] > w[best]) best = i;
left[i] = best;
}
best = m - 1;
for (int i = m - 1; i >= 0; i--) { // >= scanning leftwards : earliest tie wins
if (w[i] >= w[best]) best = i;
right[i] = best;
}
int[] ans = null;
int bestSum = -1;
for (int mid = k; mid + k < m; mid++) { // fix the middle window
int l = left[mid - k];
int r = right[mid + k];
int sum = w[l] + w[mid] + w[r];
if (sum > bestSum) {
bestSum = sum;
ans = new int[]{l, mid, r};
}
}
return ans;
}
複雜度
- 時間 O(n):一次滾動算出
w,left與right各掃一次,再掃一次mid。 - 空間 O(n):三個存視窗總和與 argmax 的陣列。
試跑 — nums = [1, 2, 1, 2, 6, 7, 5, 1], k = 2:
w = [3, 3, 3, 8, 13, 12, 6]
left = [0, 0, 0, 3, 4, 4, 4]
right = [4, 4, 4, 4, 4, 5, 6]
mid=2: w[0] + w[2] + w[4] = 3 + 3 + 13 = 19
mid=3: w[0] + w[3] + w[5] = 3 + 8 + 12 = 23 <- best
mid=4: w[0] + w[4] + w[6] = 3 + 13 + 6 = 22
answer [0, 3, 5]
陷阱
- 平手:LC 689 要字典序最小的答案。前綴掃描用
>=會把左視窗移到最後一個平手的位置:[1, 1, 2, 2], k = 1回傳[1, 2, 3]而不是[0, 2, 3]。 - 中間的迴圈兩側都必須留一整個視窗:
mid的範圍是[k, m − k),不是[0, m)。
不適用時 — 視窗個數是輸入 j → DP,dp[j][i] = max(dp[j][i − 1], dp[j − 1][i − k] + w[i − k + 1]),O(n·j)。
練習 — LC 689 · LC 1031 (兩個長度不同的視窗:兩種先後順序都試,維護目前最好的左視窗;在 prefix_sum_examples.md 中解)
鍵不是字元的視窗
模板 5:單字層級的視窗,每個偏移跑一次 — LC 30 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
辨識 — 「找出所有單字全部串接而成的子字串的起點」,每個單字等長。視窗一次移動一整個單字。
關鍵 — 合法起點 s 滿足 s % wordLen == r,其中 r < wordLen,餘數相同的起點共用同樣的單字邊界。所以跑 wordLen 個獨立的視窗,每個偏移一個,每次走 wordLen;在單一視窗裡,它就是以單字為鍵的字元計數視窗。
# python
# LC 30 - Substring with Concatenation of All Words
# IDEA: one sliding window per offset in [0, wordLen); count words; shrink on a surplus copy
# time = O(wordLen * n), space = O(m * wordLen)
from collections import Counter, defaultdict
def findSubstring(s, words):
if not s or not words:
return []
wl = len(words[0])
m = len(words)
need = Counter(words)
res = []
for offset in range(wl): # one window per alignment
left = offset
count = 0 # words in the window
window = defaultdict(int)
for right in range(offset, len(s) - wl + 1, wl):
word = s[right:right + wl]
if word not in need: # unusable word -> hard reset past it
window.clear()
count = 0
left = right + wl
continue
window[word] += 1
count += 1
while window[word] > need[word]: # a surplus copy -> shrink
window[s[left:left + wl]] -= 1
left += wl
count -= 1
if count == m: # every word used once: a match at left
res.append(left)
window[s[left:left + wl]] -= 1 # drop one word to keep scanning
left += wl
count -= 1
return res
// java
// LC 30 - Substring with Concatenation of All Words
// IDEA: one sliding window per offset in [0, wordLen); count words; shrink on a surplus copy
// time = O(wordLen * n), space = O(m * wordLen)
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
if (s == null || s.isEmpty() || words.length == 0) return res;
int wl = words[0].length();
int m = words.length;
Map<String, Integer> need = new HashMap<>();
for (String w : words) need.put(w, need.getOrDefault(w, 0) + 1);
for (int offset = 0; offset < wl; offset++) { // one window per alignment
int left = offset;
int count = 0;
Map<String, Integer> window = new HashMap<>();
for (int right = offset; right + wl <= s.length(); right += wl) {
String word = s.substring(right, right + wl);
if (!need.containsKey(word)) { // unusable word -> hard reset
window.clear();
count = 0;
left = right + wl;
continue;
}
window.put(word, window.getOrDefault(word, 0) + 1);
count++;
while (window.get(word) > need.get(word)) { // a surplus copy -> shrink
String out = s.substring(left, left + wl);
window.put(out, window.get(out) - 1);
left += wl;
count--;
}
if (count == m) { // a match at left
res.add(left);
String out = s.substring(left, left + wl);
window.put(out, window.get(out) - 1);
left += wl;
count--;
}
}
}
return res;
}
複雜度
- 時間 O(wordLen · n):
wordLen次掃描,每次約n / wordLen步,每一步都要切出並雜湊一個長度wordLen的單字。 - 空間 O(m · wordLen):
need與window最多存 m 個不同的單字。
試跑 — s = "barfoothefoobarman", words = ["foo", "bar"]:
offset 0: bar foo -> count 2 == m, record 0, drop "bar"
the -> not a word, reset, left = 9
foo bar -> count 2 == m, record 9
offset 1: arf oot hef oob arm -> none is a word
offset 2: rfo oth efo oba rma -> none is a word
result [0, 9]
陷阱
- 只跑一次、每次前進 1、在每個起點重數 m 個單字,是 O(n · m · wordLen);正是偏移讓每個單字在一個視窗裡只進出一次。
- 重複的單字(
["word", "good", "best", "word"]):要和need比較計數,不能用 set,否則多餘副本的收縮永遠不會觸發。
不適用時 — 視窗是固定長度、只問「之前看過嗎?」(LC 187,重複的 DNA)→ rolling hash,在 string_matching_kmp_rolling_hash.md。
練習 — LC 30 · LC 438 (同一個視窗,換成字元) · LC 187 (固定長度視窗加 rolling hash)
看起來像但不是單一視窗的題目
Meeting Scheduler — LC 1229
關鍵 — 兩個排好序的串列,各一個指標。先結束的區間和另一個串列裡後面的任何區間,重疊都不會比現在更多,所以把它的指標往前移;第一個長度至少
duration的重疊就是最早的答案。
歸約到:intervals § 模板 3 — 兩個排序區間串列的交集,只是回傳第一個夠長的重疊,而不是全部收集起來。這裡沒有視窗也沒有合法性判斷,所以它不是滑動視窗的模板。
# python
# LC 1229 - Meeting Scheduler
# IDEA: sort both lists; overlap = [max(starts), min(ends)]; advance whichever slot ends first
# time = O(n log n + m log m), space = O(n + m) extra (sorted() copies both lists)
def minAvailableDuration(slots1, slots2, duration):
slots1 = sorted(slots1)
slots2 = sorted(slots2)
i = 0
j = 0
while i < len(slots1) and j < len(slots2):
start = max(slots1[i][0], slots2[j][0])
end = min(slots1[i][1], slots2[j][1])
if end - start >= duration:
return [start, start + duration]
if slots1[i][1] < slots2[j][1]: # slots1[i] ends first: it has nothing more to give
i += 1
else:
j += 1
return []
// java
// LC 1229 - Meeting Scheduler
// IDEA: sort both lists; overlap = [max(starts), min(ends)]; advance whichever slot ends first
// time = O(n log n + m log m), space = O(1) extra
public List<Integer> minAvailableDuration(int[][] slots1, int[][] slots2, int duration) {
Arrays.sort(slots1, (a, b) -> Integer.compare(a[0], b[0]));
Arrays.sort(slots2, (a, b) -> Integer.compare(a[0], b[0]));
int i = 0;
int j = 0;
while (i < slots1.length && j < slots2.length) {
int start = Math.max(slots1[i][0], slots2[j][0]);
int end = Math.min(slots1[i][1], slots2[j][1]);
if (end - start >= duration) {
return Arrays.asList(start, start + duration);
}
if (slots1[i][1] < slots2[j][1]) {
i++;
} else {
j++;
}
}
return new ArrayList<>();
}
陷阱
- 移動開始較早的那個指標:
slots1 = [[0, 50]], slots2 = [[10, 15], [20, 40]], duration = 10會丟掉[0, 50],回傳[]而不是[20, 30]。 - 輸入沒排序:題目沒有保證時段已排序,所以兩邊都要先排序。
相似題 — LC 986 (收集所有交集) · LC 759 (多個串列之間的空閒時間)
不適用時
- 模板 1(至多 K 個相異) — 條件是「沒有重複字元」時,它就是用 set 的 sliding_window § 模板 3;不需要計數。(K = 1 是另一個條件,同一個字元重複:
"aaab"在那裡是 3,在「沒有重複」下是 2。保留計數,或改掃連續段。) - 模板 2(帶重置的恰好 K) — 沒有禁用元素時,它就是照原樣的 sliding_window § 模板 6。也可以先在禁用字元處把字串切開,再對每一段跑模板 6。
- 模板 3(一次掃描數間隙) — 模板 2 的兩次掃描一樣快、也更好解釋;只有被要求一次掃描時才用數間隙。有負數或真正的總和時兩者都不行:改用前綴和計數表。
- 模板 4(前綴/後綴 argmax) — 只有兩個視窗時,維護目前最好的左視窗、每種順序掃一次就夠了(LC 1031);j 個視窗時用 DP。
- 模板 5(每個偏移跑一次) — 單字長度不同時完全不適用(回溯或 trie);視窗是單一字元時,就是 hash_map § 模板 5 的一般固定視窗。
總結
該用哪種進階型態?— 決策表
| 題目說 | 該用 | 在哪 |
|---|---|---|
| 「至多 K 個相異」(最長) | 模板 3 的迴圈加 len(map) > K |
模板 1 |
| 「恰好 K …」(計數),字元不受限 | at_most(K) − at_most(K − 1) |
sliding_window § 模板 6 |
| 「恰好 K …」但只允許某些字元 | 至多計數,遇到禁用字元硬重置 | 模板 2 |
| 「恰好 k 個奇數/1」,一次掃描 | 數左側間隙 | 模板 3 |
| 「恰好 K …」而且值可能為負 | 前綴和 + hash map — 沒有視窗能用 | hash_map § 模板 4 |
| 「兩個/三個不重疊子陣列,總和最大」 | 視窗總和 + 前綴/後綴 argmax | 模板 4 |
| 「所有單字的串接」 | 每個對齊偏移一個視窗 | 模板 5 |
| 「兩份行程中最早的共同空檔」 | 兩個指標,移動先結束的那個 | Meeting Scheduler — LC 1229 |
決定工具的那一個問題
合法性是單調的嗎? 如果一個不合法的視窗在
right前進時仍然不合法,一個while (invalid) shrink就夠了,一切都是普通的視窗。如果不是 — 「恰好 k」、負值,或無法改寫成「至多」的等式 — 就需要至多相減、前綴 map,或數間隙。這一個問題就能把本頁每個技巧分開。
放在別處的題目
完整解法在別份文件、用同一個做法的進階視窗題。
| LC | 題目 | 技巧 | 解在 |
|---|---|---|---|
| 239 | Sliding Window Maximum | 存索引的遞減雙端佇列 | monotonic_queue § 模板 1 |
| 480 | Sliding Window Median | 兩個 heap 加延遲刪除 | monotonic_queue § 模板 4 |
| 1423 | Maximum Points You Can Obtain from Cards | total − 大小為 n − k 的最小視窗 |
prefix_sum_advanced § 模板 9 |
| 1658 | Minimum Operations to Reduce X to Zero | n − 總和為 total − x 的最長視窗 |
prefix_sum_advanced § 模板 9(它的變化題) |
| 220 | Contains Duplicate III | 索引視窗 + 寬度 t + 1 的數值分桶 |
sort § 模板 8;有序集合版本在 scanning_line § 模板 11 |
| 187 | Repeated DNA Sequences | 長度 10 的固定視窗 + 2-bit rolling hash | string_matching_kmp_rolling_hash.md |
| 718 | Maximum Length of Repeated Subarray | DP,或二分搜尋 + rolling hash | string_matching_kmp_rolling_hash.md |
| 1031 | Maximum Sum of Two Non-Overlapping Subarrays | 前綴和 + 目前最好的左視窗 | prefix_sum_examples.md |
| 1248 | Count Number of Nice Subarrays(前綴 map 版本) | {奇數個數: 次數} |
prefix_sum § 模板 6 |
只供辨識、沒有新技巧:
| 題目 | LC# | 一句話重點 |
|---|---|---|
| Maximum Number of Occurrences of a Substring | 1297 | 只用大小為 minSize 的固定視窗:較長的合法子字串一定包含一個合法的 minSize 子字串,所以 maxSize 是幌子 |
| New 21 Game | 837 | 在 DP 陣列上的滑動視窗:dp[i] 是前 maxPts 個機率的視窗總和除以 maxPts |
| Max Value of Equation | 1499 | 以 xj − xi ≤ k 為界的視窗,在 yi − xi 上用單調雙端佇列(monotonic_queue.md) |
這裡不涵蓋的內容
- 無界串流上的視窗 — 右邊界是即時資料流而不是陣列索引的視窗。本頁沒有任何東西假設可以隨機存取,所以模板可以直接套用;串流用的 reservoir/衰減計數器技巧在 streaming_algorithms.md。
- 深入的雙端佇列家族 — monotonic_queue.md。