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

LeetCode 2953: Count Complete Substrings

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

基本資料
#

難易度: hard 第一次嘗試:2026-01-04

  • 提交紀錄:Wrong Answer ×3 → Accepted

解題思路
#

兩個條件分開處理:

  1. 相鄰字元差 <= 2:只要某個位置跟前一個字元差 > 2,任何完整子字串都不能跨過它。所以先在這些地方切成好幾段,每段分開算。
  2. 每個字元剛好 k 次:如果子字串裡有 d 種字元,長度一定是 d × k。d 只有 1 到 26,所以對每個 d 做一次固定長度 d × k 的滑動視窗。

視窗裡維護 good = 剛好出現 k 次的字元數。長度是 d × k 而且 good == d,就代表這 d 種字元各 k 次、沒有別的字元。

good 只在計數跨過 k 的時候變動:加入後變成 k 就 +1,變成 k + 1 就 -1;移出時反過來。

我的嘗試
#

我的寫法是對每個視窗長度 k, 2k, ..., 26k 掃整個字串,遇到差 > 2 的位置就重設視窗。

版本問題
第一版縮視窗的條件寫成 right - left > win_size,也沒檢查長度剛好等於 win_size,長度不對的視窗也被算進去。例如 word = "bd"、k = 1:答案是 3(b、d、bd),回傳 4
第二版補上長度檢查,但遇到斷點時重設視窗後直接 continue,斷點那個字元沒加進新視窗。例如 word = "ah"、k = 1:答案是 2,回傳 1
第三版斷點的字元有加進去,但還是 continue,跳過了長度 1 的視窗的檢查,同一個例子還是回傳 1
第四版 AC調整順序並加上 win_size > 1 的條件,跑了 5.5 秒才過

每個視窗都用 for c in count.values() 檢查一遍,所以很慢。改成先切段、維護 good 計數,每次移動是 O(1)。

正確解法
#

class Solution:
    def countCompleteSubstrings(self, word: str, k: int) -> int:
        def count_segment(seg: str) -> int:
            res = 0
            for d in range(1, 27):                 # 子字串裡有 d 種字元
                size = d * k
                if size > len(seg):
                    break
                cnt = [0] * 26
                good = 0                           # 剛好出現 k 次的字元數
                for right, ch in enumerate(seg):
                    c = ord(ch) - 97
                    cnt[c] += 1
                    if cnt[c] == k:
                        good += 1
                    elif cnt[c] == k + 1:
                        good -= 1
                    if right >= size:
                        c = ord(seg[right - size]) - 97
                        if cnt[c] == k:
                            good -= 1
                        elif cnt[c] == k + 1:
                            good += 1
                        cnt[c] -= 1
                    if right >= size - 1 and good == d:
                        res += 1
            return res

        total = start = 0
        for i in range(1, len(word) + 1):
            if i == len(word) or abs(ord(word[i]) - ord(word[i - 1])) > 2:
                total += count_segment(word[start:i])
                start = i
        return total

複雜度
#

時間 O(26 · n),空間 O(26)(不算切出來的子字串)。

要特別避免的錯誤
#

  • 沒有固定視窗長度:「每個字元剛好 k 次」不單調,要先固定有幾種字元。
  • 斷點的處理:最乾淨的方式是先切段,不要在視窗裡特判。
  • 每個視窗都重新檢查所有字元的次數:常數很大。

面試口說整理
#

相鄰字元差大於 2 的地方不能跨,所以我先把字串切段。每個字元剛好 k 次代表長度是種類數乘 k,所以對每段枚舉種類數 d 從 1 到 26,用長度 d × k 的固定視窗,維護剛好出現 k 次的字元數,等於 d 就是完整子字串。總共 O(26n)。

相關文章

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 2156: Find Substring With Given Hash Value
·1 分鐘
LeetCode Hard Sliding-Window Rolling-Hash String
LeetCode 2156 解題筆記:雜湊的第一個字元乘 p^0,從右往左滑動時只要乘 p 再加新字元、扣掉 p^k 倍的出去字元,不需要除法;modulo 不一定是質數,不能用模反元素
LeetCode 3694: Distinct Points Reachable After Substring Removal
·1 分鐘
LeetCode Medium Sliding-Window Prefix-Sum Hash-Table String
LeetCode 3694 解題筆記:終點 = 全部的位移 − 被刪掉那段的位移;用長度 k 的視窗維護那段的位移,把每個終點放進 set
LeetCode 3518: Smallest Palindromic Rearrangement II
·2 分鐘
LeetCode Hard Math Combinatorics Counting Hash-Table String
LeetCode 3518 解題筆記:回文由左半邊決定,變成「多重集合的第 k 個排列」;逐位試每個字元,用排列數判斷第 k 個落在哪個分支,排列數可以 O(1) 更新
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 就縮左邊