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

LeetCode 395: Longest Substring with At Least K Repeating Characters

·2 分鐘· ·
LeetCode Medium Sliding-Window Divide-and-Conquer Hash-Table String
Wei Yi Chung
作者
Wei Yi Chung
Working at the contributing of open source, distributed systems, and data engineering.
目錄

基本資料
#

難易度: medium 第一次嘗試:2026-01-17

  • 提交紀錄:WA → RE ×2 → WA ×2 → AC(分治)→ AC ×2(滑動視窗)

解題思路
#

「視窗裡每個字元至少 k 次」不單調:視窗變大可能讓某個字元剛出現 1 次而變得不合法,也可能讓它湊滿 k 次而變得合法。所以沒辦法直接用「不合法就縮左邊」。

分治
#

如果某個字元在整個字串裡出現不到 k 次,答案一定不包含它。用它把字串切開,對每一段遞迴。所有字元都夠 k 次時,整段就是答案。

固定種類數的滑動視窗
#

多加一個限制讓它變單調:視窗裡剛好有 t 種字元。種類數超過 t 就縮左邊,這是單調的。對 t = 1..26 各跑一次,視窗裡種類數是 t、而且這 t 種都達到 k 次時更新答案。

我的嘗試
#

前幾版想直接滑動,用 not_enough 計數「還沒達到 k 次的字元出現次數」,遇到總數不到 k 的字元就把左端跳過它。

版本問題
第一版 WA左端跳過之後,計數沒有重設,視窗外的字元還留在計數裡。例如 s = "cbb"、k = 2:答案是 2("bb"),回傳 0
兩次 RE改成正反各跑一次,第一次少了冒號,第二次輔助函式忘了 return
第四版 WA補上 return,但計數還是沒重設。例如 s = "caab"、k = 2:答案是 2,回傳 0
第五版 WA重設了計數,但這個方法本身就不對:條件不單調,左端不知道該不該縮。例如 s = "bcccbab"、k = 3:答案是 3("ccc"),回傳 0

之後改用分治 AC,再用固定種類數的滑動視窗寫了一次。

正確解法
#

分治:

class Solution:
    def longestSubstring(self, s: str, k: int) -> int:
        for c, cnt in Counter(s).items():
            if cnt < k:                            # 答案不可能包含 c
                return max(self.longestSubstring(part, k) for part in s.split(c))
        return len(s)

固定種類數的滑動視窗:

class Solution:
    def longestSubstring(self, s: str, k: int) -> int:
        best = 0
        for target in range(1, len(set(s)) + 1):   # 視窗裡剛好有 target 種字元
            cnt = Counter()
            left = unique = enough = 0             # enough:達到 k 次的種類數
            for right, c in enumerate(s):
                if cnt[c] == 0:
                    unique += 1
                cnt[c] += 1
                if cnt[c] == k:
                    enough += 1
                while unique > target:
                    d = s[left]
                    if cnt[d] == k:
                        enough -= 1
                    cnt[d] -= 1
                    if cnt[d] == 0:
                        unique -= 1
                    left += 1
                if unique == target and enough == target:
                    best = max(best, right - left + 1)
        return best

複雜度
#

  • 分治:每一層至少拿掉一種字元,最多 26 層,每層 O(n),總共 O(26n)。
  • 滑動視窗:O(26n),空間 O(26)。

要特別避免的錯誤
#

  • 條件不單調時直接滑動視窗。
  • 跳過左端時沒有重設計數。

面試口說整理
#

每個字元至少 k 次這個條件不單調,不能直接滑動。一種做法是分治:出現不到 k 次的字元一定不在答案裡,用它切開字串遞迴。另一種是多加一個限制,枚舉視窗裡剛好有幾種字元,固定種類數之後縮視窗的條件就單調了,再看這幾種是不是都達到 k 次。兩種都是 O(26n)。

相關文章

LeetCode 1763: Longest Nice Substring
·1 分鐘
LeetCode Easy Sliding-Window Divide-and-Conquer Hash-Table String Bit-Manipulation
LeetCode 1763 解題筆記:沒有大小寫配對的字元一定不在答案裡,用它切開字串遞迴;長度相同時取左邊的
LeetCode 2516: Take K of Each Character From Left and Right
·1 分鐘
LeetCode Medium Sliding-Window Hash-Table String
LeetCode 2516 解題筆記:從兩端拿等於中間留一段;留下的每種字元最多是總數 − k,找最長的這種中間視窗
LeetCode 1016: Binary String With Substrings Representing 1 To N
·2 分鐘
LeetCode Medium Sliding-Window String Bit-Manipulation Hash-Table
LeetCode 1016 解題筆記:s 最多 1000 個字元,長度固定時的子字串數量有限,n 太大一定是 False;從 n 往下檢查會很快失敗,或從每個起點往右最多延伸 30 位收集所有 <= n 的值
LeetCode 3694: Distinct Points Reachable After Substring Removal
·1 分鐘
LeetCode Medium Sliding-Window Prefix-Sum Hash-Table String
LeetCode 3694 解題筆記:終點 = 全部的位移 − 被刪掉那段的位移;用長度 k 的視窗維護那段的位移,把每個終點放進 set
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 就縮左邊