Chương 36 - Z Function
Z function của chuỗi
s:z[i]= độ dài đoạn dài nhất bắt đầu tạis[i]đồng thời là prefix củas.z[0]được set bằngntheo quy ước. Tính được trongO(n).
Z-box invariant
Z function dùng “Z-box” [l, r) = đoạn matched gần nhất với prefix của s. Đây là chìa khoá để hiểu vì sao tính được trong O(n).
[l ........... r)
s : ┌───┐ ┌─────────────┐
│ A │ ...│ B = prefix │ ...
└───┘ └─────────────┘
prefix matched với prefix
│ │
└──────────────┴── đoạn này = prefix của s, độ dài r - l
Khi xét z[i] với i ∈ [l, r):
• Đã biết z[i - l] (vì s[l..r) = s[0..r-l) tương ứng).
• Lấy lower bound: z[i] = min(r - i, z[i - l]).
• Sau đó mở rộng bằng so sánh ký tự.
Khi i ≥ r:
• Không có thông tin → compare từ s[0].
Update Z-box khi tìm được đoạn mới dài hơn (i + z[i] > r).
Trace với s = "aabxaabxaab":
i l r z[i] tính từ
0 0 0 11 (z[0] = n)
1 0 0 ? i ≥ r, compare từ đầu
s[1]=a, s[0]=a → match. z[1]=1 (vì s[2]=b ≠ s[1]=a)
Update Z-box: l=1, r=2.
2 1 2 ? i = r, không có info → compare từ đầu.
s[2]=b ≠ s[0]=a → z[2]=0.
3 1 2 0 Tương tự, z[3]=0.
4 1 2 ? i ≥ r, compare: s[4..]=aabxaab, prefix=aabxaab
Match cả 7 → z[4]=7. Update Z-box: l=4, r=11.
5 4 11 ? i ∈ [4, 11). z[5] = min(11-5, z[5-4]) = min(6, z[1]) = min(6, 1) = 1.
Compare thêm: s[6]=b ≠ s[1]=a → dừng. z[5]=1.
6 4 11 ? z[6] = min(5, z[2]) = min(5, 0) = 0.
7 4 11 ? z[7] = min(4, z[3]) = min(4, 0) = 0.
8 4 11 ? z[8] = min(3, z[4]) = min(3, 7) = 3.
Compare thêm: i+z[i]=11=r → có thể vượt? s[11] out of bound. z[8]=3.
9 4 11 ? z[9] = min(2, z[5]) = min(2, 1) = 1.
10 4 11 ? z[10] = min(1, z[6]) = min(1, 0) = 0.
Final z: [11, 1, 0, 0, 7, 1, 0, 0, 3, 1, 0]
Amortize chứng minh O(n): mỗi lần “compare extend” trong while loop làm r tăng. r đơn điệu không giảm và ≤ n → tổng số extend ≤ n. Cộng O(1) cho mỗi i không extend → tổng O(n).
Mục tiêu chương
Sau chương này, bạn sẽ:
- Z function = mảng song song KMP, dễ hình dung hơn.
z[i]= độ dài đoạn từs[i]khớp với prefix.- Pattern
[l, r]invariant: đoạn matched gần nhất, amortizeO(n). - Z thay KMP cho hầu hết bài; KMP nhanh hơn 1 chút với prefix function.
Khi nào dùng pattern này?
- Thay thế KMP cho bài substring matching (Z function dễ hiểu hơn).
- Bài “đoạn vừa prefix vừa suffix”, “score of built strings”.
- Pattern matching với pre-computed structure (giống KMP).
Template code
def z_function(s: str) -> list[int]:
n = len(s)
z = [0] * n
z[0] = n
l = r = 0
for i in range(1, n):
if i < r:
z[i] = min(r - i, z[i - l])
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
if i + z[i] > r:
l, r = i, i + z[i]
return z
Bài tự luyện cuối chương
- LC 1392 - Longest Happy Prefix
- LC 1163 - Last Substring in Lexicographical Order
36.1 Z-function - implementation & visualization
Đề bài
Tính mảng z[] cho chuỗi s. Đây là cài đặt cốt lõi để mọi bài Z function khác dùng tới.
Ví dụ
Input: s = "aabxaabxaab"
Output: [11, 1, 0, 0, 7, 1, 0, 0, 3, 1, 0]
Giải thích vài giá trị tiêu biểu (xem trace đầy đủ ở mục "Z-box invariant"):
z[0] = 11 (quy ước: bằng độ dài chuỗi)
z[4] = 7 (s[4..10] = "aabxaab" trùng prefix "aabxaab")
z[8] = 3 (s[8..10] = "aab" trùng prefix "aab")
Ràng buộc
- 1 <= len(s) <= 10^5
Clarifying questions
- s rỗng? → Trả [].
- Tất cả ký tự khác nhau? → z[i] = 0 cho mọi i > 0.
Hướng tiếp cận
Maintain [l, r] = đoạn matched gần nhất với prefix. Tại mỗi i: - Nếu i < r: dùng giá trị z[i - l] đã biết để skip. - Mở rộng z[i] bằng cách so sánh s[z[i]] với s[i + z[i]]. - Update [l, r] nếu vùng matched mới lớn hơn.
Amortize: mỗi ký tự được “extend” tối đa 1 lần → O(n).
Code Python 3
def z_function(s: str) -> list[int]:
n = len(s)
z = [0] * n
z[0] = n
l = r = 0
for i in range(1, n):
if i < r:
z[i] = min(r - i, z[i - l])
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
if i + z[i] > r:
l, r = i, i + z[i]
return z
Trực giác Z function với s = "aabcaabxaaaz":
i : 0 1 2 3 4 5 6 7 8 9 10 11
s : a a b c a a b x a a a z
z : 12 1 0 0 3 1 0 0 2 2 1 0
z[1] = 1:s[1..1] = "a"là prefix length 1.z[4] = 3:s[4..6] = "aab"là prefix length 3.z[8] = 2:s[8..9] = "aa"là prefix length 2.
Phân tích độ phức tạp
- Thời gian:
O(n). - Bộ nhớ:
O(n).
Bình luận
- Pattern: Z function là building block cho mọi bài Chương 36.
- Bẫy: convention
z[0] = nquan trọng cho 1 số áp dụng.
Bài tự luyện liên quan
- LC 28 - strStr() (Z version, bài 36.2).
36.2 Find the Index of the First Occurrence (LC 28) - Z version
Đề bài
Tìm index đầu tiên của needle trong haystack, hoặc -1. Bài này dùng Z function thay vì KMP (Chương 35.1).
Ví dụ
Input: haystack="sadbutsad", needle="sad"
Output: 0
Ràng buộc
- 1 <= len(haystack), len(needle) <= 10^4
Clarifying questions
- needle rỗng? → Trả 0 (convention).
- haystack chứa
#? → Đề LC chỉ chữ thường → an toàn.
Hướng tiếp cận
combined = pattern + '#' + text. Tính z. Tìm i đầu tiên có z[i] == len(pattern), trả i - len(pattern) - 1 (vị trí trong text).
Code Python 3
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
if not needle: return 0
combined = needle + '#' + haystack
z = self.z_function(combined)
target = len(needle)
for i, v in enumerate(z):
if v == target:
return i - target - 1
return -1
@staticmethod
def z_function(s: str) -> list[int]:
n = len(s)
z = [0] * n
z[0] = n
l = r = 0
for i in range(1, n):
if i < r:
z[i] = min(r - i, z[i - l])
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
if i + z[i] > r:
l, r = i, i + z[i]
return z
Phân tích độ phức tạp
- Thời gian:
O(n + m). - Bộ nhớ:
O(n + m).
Bình luận
#là sentinel - đảm bảoz[i]không “vượt qua” boundary giữa pattern và text.
Bài tự luyện liên quan
- LC 28 - strStr() (KMP version, bài 35.1)
- LC 1392 - Longest Happy Prefix
36.3 Sum of Scores of Built Strings (LC 2223) - recap
Đã giải đầy đủ ở Chương 34.6 (dùng Z function).
Insight
score(s_i) = z[n - len(s_i)] (số ký tự khớp prefix khi build dần). Cộng dồn chính là sum(z[i] for i in range(1, n)) + n (cho cả z[0] = n).
Code Python 3 (recap)
class Solution:
def sumScores(self, s: str) -> int:
n = len(s)
z = [0] * n
z[0] = n
l = r = 0
for i in range(1, n):
if i < r:
z[i] = min(r - i, z[i - l])
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
if i + z[i] > r:
l, r = i, i + z[i]
return sum(z)
Phân tích độ phức tạp
- Thời gian:
O(n)(Z function). Bộ nhớ:O(n).
Bài tự luyện liên quan
- LC 1392 - Longest Happy Prefix.
36.4 Maximum Deletions on a String (LC 2430)
Đề bài
Cho chuỗi s. Mỗi lượt thao tác, bạn được chọn một trong hai cách:
- Chọn chỉ số
ivới1 ≤ i ≤ len(s) / 2sao chos[0..i-1] == s[i..2i-1](tiền tố độ dàiitrùng với đoạn ngay sau nó). Xoá tiền tố độ dàii; phần còn lại củaslàs[i:]. - Nếu không tồn tại
ithoả mãn, xoá toàn bộs(kết thúc).
Trả về số lượt thao tác lớn nhất có thể thực hiện cho đến khi s rỗng.
Ví dụ
Input: s = "abcabcdabc"
Output: 2 (số phép xoá tối đa; mỗi phép xoá tiền tố P bằng tiền tố tiếp theo)
Ràng buộc
- 1 <= len(s) <= 4000
Clarifying questions
- s = 1 ký tự? → 1 turn (xoá luôn).
- s toàn cùng ký tự? → O(n) turn.
Hướng tiếp cận
DP từ phải sang trái. dp[i] = max turn từ s[i..].
Transition: dp[i] = max(1, max(dp[i + j] + 1 for j với s[i..i+j-1] == s[i+j..i+2j-1])).
So sánh substring nhanh bằng Z function hoặc precomputed LCP.
Code Python 3
class Solution:
def deleteString(self, s: str) -> int:
n = len(s)
# lcp[i][j] = LCP của s[i:] và s[j:].
lcp = [[0] * (n + 1) for _ in range(n + 1)]
for i in range(n - 1, -1, -1):
for j in range(n - 1, -1, -1):
if s[i] == s[j]:
lcp[i][j] = lcp[i + 1][j + 1] + 1
dp = [1] * (n + 1)
dp[n] = 0
for i in range(n - 1, -1, -1):
for j in range(1, (n - i) // 2 + 1):
if lcp[i][i + j] >= j:
dp[i] = max(dp[i], dp[i + j] + 1)
return dp[0]
Phân tích độ phức tạp
- Thời gian:
O(n²). Bộ nhớ:O(n²).
Bình luận
- Bẫy: so sánh substring bằng slice → O(L) mỗi check; dùng LCP table.
- Follow-up: LC 2105 (Watering Plants II) cùng kỹ thuật.
Bài tự luyện liên quan
- LC 2223 - Sum of Scores (bài 36.3)
- LC 1316 - Distinct Echo Substrings
36.5 Match Substring After Replacement (LC 2301)
Đề bài
Cho s, sub, và mappings[i] = [old, new] (mỗi ký tự old có thể đổi thành new ≤ 1 lần khi match). Kiểm tra sub (sau khi áp dụng tuỳ ý mappings) có là substring của s không.
Ví dụ
Input: s="fool3e7bar", sub="leet", mappings=[["e","3"],["t","7"],["t","8"]]
Output: True
Ràng buộc
- 1 <= len(s), len(sub) <= 5000
- 0 <= len(mappings) <= 1000
Clarifying questions
- sub > s? → Trả False.
- mappings rỗng? → Match strict; chỉ True nếu sub xuất hiện trong s.
Hướng tiếp cận
Brute force O(n · m) với check thông minh (mỗi ký tự match nếu == hoặc ∈ mapping).
Z function hoặc KMP có thể adapt - phức tạp hơn.
Code Python 3
from collections import defaultdict
from typing import List
class Solution:
def matchReplacement(self, s: str, sub: str, mappings: List[List[str]]) -> bool:
m: dict[str, set[str]] = defaultdict(set)
for a, b in mappings:
m[a].add(b)
def matches(a: str, b: str) -> bool:
return a == b or b in m[a]
n, ns = len(s), len(sub)
for i in range(n - ns + 1):
if all(matches(sub[j], s[i + j]) for j in range(ns)):
return True
return False
Phân tích độ phức tạp
- Thời gian:
O(n · m). - Bộ nhớ:
O(k)với k = số mappings.
Bình luận
- Bài này KMP/Z không trực tiếp work vì điều kiện match “đa nghĩa”.
Bài tự luyện liên quan
- LC 28 - strStr()
- LC 686 - Repeated String Match
36.6 Distinct Echo Substrings (LC 1316) - recap
Cùng bài 34.3, nhưng dùng Z function thay rolling hash để tránh collision.
Đề bài
Đếm số substring phân biệt có dạng a + a (chuỗi nối với chính nó).
Ví dụ
Input: text = "aaaa"
Output: 1 (echo substring duy nhất là "aa"; "aaaa" có dạng a+a nhưng đếm theo distinct)
Ràng buộc
- 1 <= len(text) <= 2000
Clarifying questions
- s = 1 ký tự? → Trả 0 (không có echo nào).
Hướng tiếp cận
Với mỗi i, tính z trên s[i:]. Tìm các j với z[j] >= j (= echo length j tại vị trí i). Gom các echo substring distinct dùng set lưu (start_i, len).
Code Python 3
class Solution:
def distinctEchoSubstrings(self, s: str) -> int:
def z_function(t: str) -> list[int]:
n = len(t)
z = [0] * n
z[0] = n
l = r = 0
for i in range(1, n):
if i < r:
z[i] = min(r - i, z[i - l])
while i + z[i] < n and t[z[i]] == t[i + z[i]]:
z[i] += 1
if i + z[i] > r:
l, r = i, i + z[i]
return z
seen: set[tuple[int, int]] = set()
n = len(s)
for i in range(n):
z = z_function(s[i:])
for j in range(1, len(z)):
if z[j] >= j:
seen.add((i, j)) # echo "aa" với mỗi a = s[i..i+j-1]
# Lọc distinct theo nội dung substring.
return len({s[i:i + 2 * j] for i, j in seen})
O(n²) worst case.
Phân tích độ phức tạp
- Thời gian:
O(n²)worst. - Bộ nhớ:
O(n²).
Bài tự luyện liên quan
- LC 1044 - Longest Duplicate Substring.
Tóm tắt chương & Quyết định
Z-box visualization - aabcaabxaaaz
i: 0 1 2 3 4 5 6 7 8 9 10 11
s: a a b c a a b x a a a z
Z: – 1 0 0 3 1 0 0 2 1 1 0
Box [l, r): vùng đang khớp tiền tố hiện tại. Tại mỗi i ∈ [l, r): - Nếu i + Z[i-l] < r: copy Z[i] = Z[i-l]. - Ngược lại: brute extend từ Z[i] = r - i.
Cập nhật box khi mở rộng:
i = 4: chưa có box, brute: s[4..]=aabxaaaz vs s[0..]=aabcaab... ⇒ khớp 3 ⇒ Z[4]=3, [l,r)=[4,7)
i = 5: i-l=1, Z[1]=1, i+Z[1]=6 < 7 ⇒ Z[5] = 1 (copy)
i = 6: i-l=2, Z[2]=0, 6+0=6 < 7 ⇒ Z[6] = 0
i = 7: ngoài box, brute: Z[7]=0
i = 8: brute, khớp aaz vs aab → 2 ⇒ Z[8]=2, [l,r)=[8,10)
Maximum Deletions (LC 2430) - LCP DP, không thuần Z
- Cần
lcp[i][j]= LCP của suffixs[i:]vàs[j:]. Có thể compute bằng Z theo cách indirect, nhưng DP 2D là cách tự nhiên hơn. - Bài đặt trong chương này để minh hoạ rằng LCP / Z liên quan họ hàng nhưng không luôn dùng trực tiếp Z.
Match Replacement contrast
KMP/Z giả định so sánh bằng. Nếu match relation không đối xứng (vd wildcards có ràng buộc), failure function không xài được - phải DP.
Distinct Echo recap
Bài đầy đủ ở Chương 34 (rolling hash). Recap ở đây để so sánh: Z-function có thể đếm echo s[i:i+L] == s[i+L:i+2L] bằng cách kiểm tra Z[i+L] ≥ L.