LeetCode 1297. 子串的最大出現次數(定長滑動視窗圖解)
1297. 子串的最大出現次數
題目描述
給你一個字串 s,請你回傳滿足以下條件且出現次數最大的任意子串的出現次數:
- 子串中不同字母的數目必須小於等於
maxLetters。 - 子串的長度必須大於等於
minSize且小於等於maxSize。
範例 1
1 | |
範例 2
1 | |
範例 3
1 | |
範例 4
1 | |
注意:出現次數允許子串互相重疊(範例 2)。
本文題解思路與程式碼整理自 LeetCode 題解作者「灵茶山艾府」,題解連結。圖解與步驟說明為自行補充。
前置知識:定長滑動視窗
定長視窗每次右移一格,少一個最左邊的元素、多一個新元素,視窗內的資訊可以用「加新的、減舊的」在 O(1) 內更新。前面幾篇已經用過,例如 2841,同樣要統計視窗內的不同元素個數。
思路
這題有三個條件互相牽制(不同字母數、最短長度、最長長度),乍看很複雜,其實可以先把它簡化成只看一種長度。
第一步:只需要看長度恰好等於 minSize 的子串
兩個觀察:
- 子串越短,包含的不同字母越少,越容易滿足
不同字母數 <= maxLetters。 - 子串越短,在
s裡出現的次數越多(或一樣多)。
把這兩點合起來,用一句話說明為什麼 maxSize 是多餘的:
設一個合法的子串
T,長度大於minSize。取T的前minSize個字元,得到P。
P的不同字母數 ≤T的不同字母數 ≤maxLetters,所以P也合法。T每出現一次,P就跟著在同一個起點出現一次,所以P的出現次數 ≥T的出現次數。
也就是說,任何比 minSize 長的合法子串,都有一個長度恰好是 minSize 的合法子串不比它差。因此答案只需要在長度等於 minSize 的子串裡找。
下面用範例 1 實際比較。切換長度 3 和長度 4,看每個子串是否有效、各出現幾次:
可以看到長度 3 的最大次數(2)不小於長度 4 的最大次數(1),和上面的推論一致。
第二步:用定長滑動視窗列舉所有長度為 minSize 的子串
視窗長度固定為 minSize,每滑一格需要維護兩件事:
- 視窗內有幾種不同的字母(
kinds),用來判斷是否<= maxLetters。 - 這個子串之前出現過幾次(
strCnt),用雜湊表記錄,每次更新答案。
第三步:怎麼維護「不同字母的數目」
不需要每次重新數。用長度 26 的陣列 charCnt 記錄視窗內每個字母出現幾次:
- 字母進入視窗:先看它之前的次數,如果是 0,代表是新字母,
kinds加一;再把次數加一。 - 字母離開視窗:先把次數減一,如果減完是 0,代表視窗裡沒有這個字母了,
kinds減一。
這和 2841 用雜湊表統計不同元素是同一個想法,這裡字母只有小寫 26 個,用陣列比雜湊表更快。
第四步:統計子串出現次數
題目保證 minSize <= 26,子串很短,直接把子串字串當雜湊表的 key 就夠了(substring 一次最多複製 26 個字元)。如果 minSize 可以很大,就需要用字典樹來加速,這題不用。
圖解(可互動)
用範例 1:s = "aababcaab",maxLetters = 2,視窗長度 minSize = 3。重點看三件事:
- 視窗
"abc"、"bca"有 3 種不同字母,超過maxLetters,直接跳過,不計入子串次數。 - 最後一個視窗
"aab"是第二次出現,所以ans從 1 變成 2。 - 字母離開視窗時,只有在次數歸零才讓
kinds減一。
把過程寫成表格:
| 視窗 | 子串 | kinds |
是否有效 | strCnt 之後 |
ans |
|---|---|---|---|---|---|
[0,2] |
aab |
2 | 是 | aab:1 |
1 |
[1,3] |
aba |
2 | 是 | aab:1, aba:1 |
1 |
[2,4] |
bab |
2 | 是 | aab:1, aba:1, bab:1 |
1 |
[3,5] |
abc |
3 | 否(超過 2) | 不變 | 1 |
[4,6] |
bca |
3 | 否(超過 2) | 不變 | 1 |
[5,7] |
caa |
2 | 是 | …, caa:1 |
1 |
[6,8] |
aab |
2 | 是 | aab:2, … |
2 |
最後 ans = 2,與範例 1 一致。
範例 2 s = "aaaa"、minSize = 3:視窗是 [0,2] 和 [1,3],都是 "aaa",只有 1 種字母,strCnt["aaa"] = 2。題目允許重疊,所以答案是 2。
解法(灵茶山艾府)
1 | |
幾個細節:
maxSize參數完全沒用到,這就是第一步推論的結果。- 進入視窗時先判斷
charCnt[in] == 0再加一;離開視窗時先減一再判斷== 0,順序反了kinds就會算錯。 strCnt.merge(subStr, 1, Integer::sum)等價於++strCnt[subStr],並直接回傳更新後的次數。- 只有
kinds <= maxLetters的視窗才把子串放進strCnt,不合格的子串完全不計。
複雜度分析
- 時間複雜度:O((n − minSize) · minSize),其中 n 是
s的長度。有n − minSize + 1個視窗,每個視窗取子串並計算雜湊要 O(minSize)。 - 空間複雜度:O((n − minSize) · minSize)。雜湊表最多存
n − minSize + 1個子串,每個長minSize。