Chương 35 - KMP (Knuth–Morris–Pratt)
KMP giải bài substring matching trong
O(n + m), cốt lõi là mảng LPS (Longest Prefix Suffix) - với mỗi vị tríitrong pattern,lps[i]= độ dài prefix dài nhất củapattern[0..i]đồng thời là suffix của nó (không phải chính nó).
LPS - visualize bằng ví dụ ababaca
LPS là khái niệm khó nhất KMP. Trace tay với pattern = "ababaca":
index : 0 1 2 3 4 5 6
char : a b a b a c a
LPS : 0 0 1 2 3 0 1
Giải thích từng ô:
LPS[0]=0 (1 ký tự, không có proper prefix nào)
LPS[1]=0 "ab" - prefix "a" ≠ suffix "b"
LPS[2]=1 "aba" - prefix "a" = suffix "a" → 1
LPS[3]=2 "abab" - prefix "ab" = suffix "ab" → 2
LPS[4]=3 "ababa" - prefix "aba" = suffix "aba" → 3
LPS[5]=0 "ababac" - không có prefix "_" = suffix "_c" → 0
LPS[6]=1 "ababaca" - prefix "a" = suffix "a" → 1
Trực giác: LPS[i] cho biết khi mismatch tại pattern[i+1], ta không cần restart từ đầu - có thể nhảy về pattern[LPS[i]] vì các ký tự pattern[0..LPS[i]-1] chắc chắn đã match (do nó cũng là suffix của những gì đã match).
Mục tiêu chương
Sau chương này, bạn sẽ:
- LPS array:
lps[i]= longest proper prefix = suffix củapattern[0..i]. - Mỗi ký tự pattern bị compare tối đa 2 lần →
O(n + m). - Sentinel
#cần KHÔNG xuất hiện trong input (chọn ký tự an toàn). - Trick:
len(s) - lps[-1]chia hếtlen(s)↔︎ s là string lặp.
Khi nào dùng pattern này?
- Substring matching, đặc biệt khi pattern xuất hiện nhiều.
- Bài “longest happy prefix”, “shortest palindrome”, “repeated pattern”.
- Khi không thể chấp nhận collision risk của rolling hash.
Template code
def build_lps(p: str) -> list[int]:
n = len(p)
lps = [0] * n
length = 0
i = 1
while i < n:
if p[i] == p[length]:
length += 1
lps[i] = length
i += 1
elif length > 0:
length = lps[length - 1]
else:
lps[i] = 0
i += 1
return lps
def kmp_search(text: str, p: str) -> list[int]:
lps = build_lps(p)
result = []
i = j = 0
while i < len(text):
if text[i] == p[j]:
i += 1; j += 1
if j == len(p):
result.append(i - j)
j = lps[j - 1]
elif j > 0:
j = lps[j - 1]
else:
i += 1
return result
Bài tự luyện cuối chương
- LC 686 - Repeated String Match
- LC 1392 - Longest Happy Prefix
- LC 3008 - Find Beautiful Indices
35.1 Implement strStr() (LC 28)
Đề bài
Tìm index đầu tiên của needle trong haystack, hoặc -1.
Ví dụ
Input: haystack="sadbutsad", needle="sad"
Output: 0
Ràng buộc
- 1 <= len(haystack), len(needle) <= 10^4
- haystack, needle chỉ chứa chữ thường
Clarifying questions
- needle rỗng? → Trả 0.
- haystack rỗng? → Trả -1 (trừ khi needle rỗng).
Hướng tiếp cận
KMP O(n + m) time, O(m) space cho lps.
Code Python 3
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
n, m = len(haystack), len(needle)
if m == 0: return 0
lps = [0] * m
length = 0
i = 1
while i < m:
if needle[i] == needle[length]:
length += 1
lps[i] = length
i += 1
elif length > 0:
length = lps[length - 1]
else:
i += 1
i = j = 0
while i < n:
if haystack[i] == needle[j]:
i += 1; j += 1
if j == m:
return i - j
elif j > 0:
j = lps[j - 1]
else:
i += 1
return -1
Phân tích độ phức tạp
- Thời gian:
O(n + m). - Bộ nhớ:
O(m)cho LPS.
Bình luận
- Brute force
O(n·m)vẫn pass vớim, n ≤ 10⁴trên LC, nhưng KMP là lời giải tối ưu hơn về mặt thuật toán và thể hiện được khả năng nắm pattern matching tuyến tính.
Bài tự luyện liên quan
- LC 214 - Shortest Palindrome (bài 35.2)
- LC 459 - Repeated Substring Pattern (bài 35.3)
35.2 Shortest Palindrome (LC 214) - KMP version
Đề bài
Như Chương 34.4 nhưng dùng KMP.
Ví dụ
Input: s = "aacecaaa"
Output: "aaacecaaa" (thêm ký tự ÍT NHẤT vào ĐẦU s để thành palindrome)
Ràng buộc
- 0 <= len(s) <= 5·10^4
Clarifying questions
- s đã palindrome? → Trả s.
- s rỗng? → Trả s.
Hướng tiếp cận
Tạo combined = s + '#' + reverse(s). Tính lps của combined; giá trị lps[-1] chính là độ dài prefix dài nhất của s đồng thời là suffix của reverse(s) = prefix palindrome dài nhất.
Code Python 3
class Solution:
def shortestPalindrome(self, s: str) -> str:
combined = s + '#' + s[::-1]
lps = [0] * len(combined)
length = 0
for i in range(1, len(combined)):
while length > 0 and combined[i] != combined[length]:
length = lps[length - 1]
if combined[i] == combined[length]:
length += 1
lps[i] = length
return s[lps[-1]:][::-1] + s
Phân tích độ phức tạp
- Thời gian:
O(n). - Bộ nhớ:
O(n).
Bình luận
- Trick
#tránh prefix overlap suffix vô tình (palindrome ảo).
Bài tự luyện liên quan
- LC 1392 - Longest Happy Prefix (bài 35.6)
- LC 5 - Longest Palindromic Substring
35.3 Repeated Substring Pattern (LC 459)
Đề bài
Kiểm tra s có thể tạo bằng concat nhiều lần 1 substring không.
Ví dụ
Input: s = "abab"
Output: True
("abab" = "ab" lặp 2 lần ⇒ True)
Ràng buộc
- 1 <= len(s) <= 10^4
- s chỉ chứa chữ thường
Clarifying questions
- s chỉ 1 ký tự? → Trả False (cần ≥ 2 lần lặp).
Hướng tiếp cận
Trick KMP: nếu s = pattern * k (k ≥ 2), thì lps[-1] > 0 và n - lps[-1] chia hết n.
Cụ thể: len(pattern) = n - lps[-1].
Code Python 3
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
n = len(s)
lps = [0] * n
length = 0
for i in range(1, n):
while length > 0 and s[i] != s[length]:
length = lps[length - 1]
if s[i] == s[length]:
length += 1
lps[i] = length
return lps[-1] > 0 and n % (n - lps[-1]) == 0
Phân tích độ phức tạp
- Thời gian:
O(n). - Bộ nhớ:
O(n).
Bình luận
- One-liner trick:
(s + s)[1:-1].find(s) != -1. Tinh tế nhưng less efficient.
Bài tự luyện liên quan
- LC 28 - strStr() (bài 35.1)
- LC 1392 - Longest Happy Prefix (bài 35.6)
35.4 Find All Anagrams in a String (LC 438)
Đề bài
Tìm tất cả start index trong s mà substring length-|p| là anagram của p.
Ví dụ
Input: s="cbaebabacd", p="abc"
Output: [0, 6]
Ràng buộc
- 1 <= len(s), len(p) <= 3·10^4
Clarifying questions
- p > s? → Trả [].
- Anagram phải sort? → Không, dùng Counter equality.
Hướng tiếp cận
Sliding window + Counter. KMP không trực tiếp giải bài này - Anagram không phải substring match. Nhưng vẫn được liệt kê vì nằm trong family bài “string pattern”.
Code Python 3
from collections import Counter
from typing import List
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
if len(s) < len(p): return []
need = Counter(p)
have = Counter(s[:len(p)])
result = []
if have == need:
result.append(0)
for i in range(len(p), len(s)):
have[s[i]] += 1
have[s[i - len(p)]] -= 1
if have[s[i - len(p)]] == 0:
del have[s[i - len(p)]]
if have == need:
result.append(i - len(p) + 1)
return result
Phân tích độ phức tạp
- Thời gian:
O(n + m · k)với k = bảng chữ; thườngO(n). - Bộ nhớ:
O(k).
Bình luận
- Bẫy: so sánh Counter mỗi vòng →
O(26)không phảiO(1); vẫn ổn cho LC. - Follow-up: LC 30 (Substring with Concatenation) tổng quát hơn.
Bài tự luyện liên quan
- LC 567 - Permutation in String
- LC 30 - Substring with Concatenation of All Words
35.5 Maximum Number of Occurrences of a Substring (LC 1297)
Đề bài
Cho s, maxLetters, minSize, maxSize. Tìm substring có maxOccurrence mà length ∈ [minSize, maxSize] và số ký tự phân biệt ≤ maxLetters.
Ví dụ
Input: s="aababcaab", maxLetters=2, minSize=3, maxSize=4
Output: 2
Ràng buộc
- 1 <= len(s) <= 10^5
- 1 <= maxLetters <= 26
Clarifying questions
- maxLetters > 26? → Cho phép mọi substring length-minSize.
- maxSize không dùng? → Đúng - chỉ minSize matter.
Hướng tiếp cận
Trick: chỉ cần xét length = minSize (substring lớn hơn không thể xuất hiện nhiều hơn).
Sliding window + Counter.
Code Python 3
from collections import Counter, defaultdict
class Solution:
def maxFreq(self, s: str, maxLetters: int, minSize: int, maxSize: int) -> int:
freq: dict[str, int] = defaultdict(int)
for i in range(len(s) - minSize + 1):
sub = s[i:i + minSize]
if len(set(sub)) <= maxLetters:
freq[sub] += 1
return max(freq.values(), default=0)
Phân tích độ phức tạp
- Thời gian:
O(n · minSize). - Bộ nhớ:
O(n).
Bình luận
- Insight
minSizeonly: nếu substring length-L xuất hiện k lần thì substring length-(L-1) cũng xuất hiện ≥ k lần (mỗi cái → prefix length-(L-1)).
Bài tự luyện liên quan
- LC 187 - Repeated DNA Sequences
- LC 30 - Substring with Concatenation of All Words
35.6 Longest Happy Prefix (LC 1392)
Đề bài
“Happy prefix” = prefix non-trivial đồng thời là suffix. Trả về cái dài nhất.
Ví dụ
Input: s = "level"
Output: "l" (prefix DÀI NHẤT của s cũng là suffix của s, KHÔNG bằng chính s;
empty string nếu không tồn tại)
Ràng buộc
- 1 <= len(s) <= 10^5
- s chỉ chứa chữ thường
Clarifying questions
- s không có happy prefix? → Trả ““.
Hướng tiếp cận
Trực tiếp lps[-1] của s chính là đáp án.
Code Python 3
class Solution:
def longestPrefix(self, s: str) -> str:
n = len(s)
lps = [0] * n
length = 0
for i in range(1, n):
while length > 0 and s[i] != s[length]:
length = lps[length - 1]
if s[i] == s[length]:
length += 1
lps[i] = length
return s[:lps[-1]]
Phân tích độ phức tạp
- Thời gian:
O(n). - Bộ nhớ:
O(n).
Bình luận
- Bẫy: trả
s[:lps[-1]]- vẫn còn case rỗng nếu không có happy prefix. - Follow-up: LC 459 (Repeated Substring Pattern) - cùng LPS.
Bài tự luyện liên quan
- LC 28 - strStr().
Tóm tắt chương & Quyết định
LPS trace - ababaca
i | char | j (prev LPS) | lps[i] |
|---|---|---|---|
| 0 | a | – | 0 |
| 1 | b | lps[0]=0; s[0]!=s[1] | 0 |
| 2 | a | j=0; s[0]==s[2] ⇒ j=1 | 1 |
| 3 | b | j=1; s[1]==s[3] ⇒ j=2 | 2 |
| 4 | a | j=2; s[2]==s[4] ⇒ j=3 | 3 |
| 5 | c | j=3; s[3]!=s[5]; fallback j=lps[2]=1; s[1]!=s[5]; fallback j=lps[0]=0; s[0]!=s[5] | 0 |
| 6 | a | j=0; s[0]==s[6] ⇒ j=1 | 1 |
⇒ lps = [0, 0, 1, 2, 3, 0, 1].
Sentinel safety
Khi concat P + '#' + T (LC tham khảo), # phải không nằm trong charset cho phép. Nếu input có thể chứa mọi ký tự Unicode, dùng cặp 2 sentinel hoặc Z-function (Chương 36) trực tiếp.
Find All Anagrams (LC 438) ≠ KMP
- Bài này là sliding window + counter. Đặt ở chương này chỉ để so sánh với “pattern matching theo cấu trúc”.
- KMP matching liên tục theo thứ tự; anagram matching không quan tâm thứ tự ký tự.
KMP vs Z (Chương 36)
| KMP | Z | |
|---|---|---|
| Preprocess | LPS array của P | Z array của P + '#' + T |
| Tư duy | “Failure → quay lui khôn ngoan” | “Khớp tiền tố tại mọi vị trí” |
| Cài đặt | Failure function, 2 con trỏ | 1 box [l, r), dễ off-by-one |
| Ứng dụng đặc thù | Period of string, find pattern | LCP, distinct substring, Z-array properties |