Bài 14: Hash Xâu & Z-Algorithm¶
Tác giả: FPTOJ Team
Nội dung tham khảo từ: VNOI Wiki - Hash, Z-function
1. Hash Xâu¶
Bản chất vấn đề¶
Cho xâu \(S\) độ dài \(N\), cần so sánh nhanh hai xâu con bất kỳ hoặc tìm tất cả vị trí xuất hiện của mẫu \(P\) (độ dài \(M\)) trong \(S\).
So sánh trực tiếp từng ký tự mất \(O(NM)\) — quá chậm khi \(N, M\) lớn.
Tư duy cốt lõi¶
Polynomial Hash chuyển mỗi xâu thành một số nguyên:
Với \(B\) là cơ số (thường dùng 31 hoặc 131) và \(M\) là số nguyên tố lớn (thường \(10^9 + 7\)).
Hash tiền tố cho phép tính hash của bất kỳ đoạn con \(S[l..r]\) trong \(O(1)\):
trong đó \(H[i]\) là hash của \(i\) ký tự đầu tiên.
Phân tích tính đúng đắn¶
Hash tiền tố hoạt động như thế nào?
Tương tự prefix sum, \(H[i]\) lưu hash của \(i\) ký tự đầu. Khi cần hash đoạn \(S[l..r]\), ta "trừ đi" phần tiền tố \(S[0..l-1]\) sau khi nhân lên để cân bậc.
Ví dụ với \(S = \texttt{"abcab"}\), \(B = 31\), tính hash đoạn \([2, 4] = \texttt{"cab"}\):
| \(i\) | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| \(H[i]\) | \(0\) | \(H(\texttt{"a"})\) | \(H(\texttt{"ab"})\) | \(H(\texttt{"abc"})\) | \(H(\texttt{"abca"})\) | \(H(\texttt{"abcab"})\) |
Phần \(H[2] \cdot B^3\) chính là hash của \(\texttt{"ab"}\) được "dịch lên" bậc cao để khi trừ đi sẽ loại bỏ đúng phần đóng góp của \(S[0..1]\) trong \(H[5]\).
Tại sao có thể collision?
Với modulo \(M\), chỉ có tối đa \(M\) giá trị hash khác nhau. Theo Birthday Paradox, khi có \(n\) xâu, xác suất ít nhất một cặp trùng hash là:
| Số xâu (\(n\)) | \(M = 10^9+7\) | \(M = 10^9+9\) | \(2\) hash |
|---|---|---|---|
| \(10^3\) | \(\sim 0.00005\%\) | \(\sim 0.00005\%\) | \(\sim 0\%\) |
| \(10^5\) | \(\sim 0.5\%\) | \(\sim 0.5\%\) | \(\sim 0\%\) |
| \(10^6\) | \(\sim 39\%\) | \(\sim 39\%\) | \(\sim 5 \times 10^{-11}\%\) |
| \(10^7\) | \(\sim 99.99\%\) | \(\sim 99.99\%\) | \(\sim 5 \times 10^{-6}\%\) |
Với \(1\) hash, chỉ cần \(\sim 44{,}721\) xâu (\(\approx \sqrt{2 \times 10^9}\)) là xác suất collision đã đạt \(\sim 50\%\).
Giải pháp: Double Hash — dùng \(2\) modulo khác nhau. Chỉ khi cả hai hash đều khớp mới kết luận xâu trùng. Không gian giá trị khi đó là \(10^9 \times 10^9 = 10^{18}\), xác suất collision gần bằng \(0\).
Đánh giá độ phức tạp¶
- Thời gian: \(O(N + M)\) — tính trước hash tiền tố và lũy thừa \(B\) trong \(O(N)\), mỗi đoạn con kiểm tra \(O(1)\)
- Không gian: \(O(N)\) — mảng hash tiền tố và lũy thừa
- Double Hash: thời gian và không gian gấp đôi, vẫn \(O(N + M)\)
2. Z-Algorithm¶
Bản chất vấn đề¶
Tính mảng \(Z\) cho xâu \(S\) độ dài \(N\), trong đó \(Z[i]\) là độ dài đoạn con dài nhất bắt đầu tại vị trí \(i\) trùng với tiền tố của \(S\).
Khi kết hợp với ký tự phân tách, Z-function giải bài toán tìm xâu mẫu trong \(O(N + M)\).
Tư duy cốt lõi¶
Cài đặt trực tiếp (dễ hiểu, \(O(N^2)\) worst-case):
Tối ưu hóa: Duy trì vùng \([l, r]\) là vùng khớp xa nhất bên phải đã tìm được. Nếu \(i \leq r\), ta có thể reuse kết quả đã tính.
Phân tích tính đúng đắn¶
Minh họa tính mảng Z với \(S = \texttt{"aabaaab"}\) (độ dài \(7\)):
| \(i\) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| \(S[i]\) | a |
a |
b |
a |
a |
a |
b |
| \(Z[i]\) | — | ? | ? | ? | ? | ? | ? |
Khởi tạo: \(l = 0,\ r = 0\). Ta bỏ qua \(Z[0]\) (không sử dụng).
Bước 1: \(i = 1\)
\(i = 1\) nằm ngoài vùng \([l, r] = [0, 0]\) → so sánh từ đầu.
So sánh: \(S[0] = \texttt{'a'}\) vs \(S[1] = \texttt{'a'}\) → khớp, \(S[1] = \texttt{'a'}\) vs \(S[2] = \texttt{'b'}\) → không khớp.
\(Z[1] = 1\). Cập nhật: \(l = 1,\ r = 1\).
| \(i\) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| \(Z[i]\) | — | \(1\) | ? | ? | ? | ? | ? |
Bước 2: \(i = 2\)
\(i = 2\) nằm ngoài vùng \([l, r] = [1, 1]\) → so sánh từ đầu.
So sánh: \(S[0] = \texttt{'a'}\) vs \(S[2] = \texttt{'b'}\) → không khớp.
\(Z[2] = 0\). Không cập nhật \(l, r\).
| \(i\) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| \(Z[i]\) | — | \(1\) | \(0\) | ? | ? | ? | ? |
Bước 3: \(i = 3\)
\(i = 3\) nằm ngoài vùng \([l, r] = [1, 1]\) → so sánh từ đầu.
So sánh: \(S[0] = \texttt{'a'}\) vs \(S[3] = \texttt{'a'}\) → khớp, \(S[1] = \texttt{'a'}\) vs \(S[4] = \texttt{'a'}\) → khớp, \(S[2] = \texttt{'b'}\) vs \(S[5] = \texttt{'a'}\) → không khớp.
\(Z[3] = 2\). Cập nhật: \(l = 3,\ r = 4\).
| \(i\) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| \(Z[i]\) | — | \(1\) | \(0\) | \(2\) | ? | ? | ? |
Bước 4: \(i = 4\)
\(i = 4\) nằm trong vùng \([l, r] = [3, 4]\) → reuse.
\(Z[i - l] = Z[4 - 3] = Z[1] = 1\). Đặt \(Z[4] = \min(r - i + 1,\ Z[i - l]) = \min(1, 1) = 1\).
So sánh tiếp: \(S[1] = \texttt{'a'}\) vs \(S[5] = \texttt{'a'}\) → khớp, \(S[2] = \texttt{'b'}\) vs \(S[6] = \texttt{'b'}\) → khớp.
\(Z[4] = 3\). Cập nhật: \(l = 4,\ r = 6\).
| \(i\) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| \(Z[i]\) | — | \(1\) | \(0\) | \(2\) | \(3\) | ? | ? |
Bước 5: \(i = 5\)
\(i = 5\) nằm trong vùng \([l, r] = [4, 6]\) → reuse.
\(Z[i - l] = Z[5 - 4] = Z[1] = 1\). Đặt \(Z[5] = \min(6 - 5 + 1,\ 1) = \min(2, 1) = 1\).
So sánh tiếp: \(S[1] = \texttt{'a'}\) vs \(S[6] = \texttt{'b'}\) → không khớp.
\(Z[5] = 1\). Không cập nhật \(l, r\).
| \(i\) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| \(Z[i]\) | — | \(1\) | \(0\) | \(2\) | \(3\) | \(1\) | ? |
Bước 6: \(i = 6\)
\(i = 6\) nằm trong vùng \([l, r] = [4, 6]\) → reuse.
\(Z[i - l] = Z[6 - 4] = Z[2] = 0\). Đặt \(Z[6] = \min(6 - 6 + 1,\ 0) = \min(1, 0) = 0\).
So sánh trực tiếp: \(S[0] = \texttt{'a'}\) vs \(S[6] = \texttt{'b'}\) → không khớp.
\(Z[6] = 0\).
Kết quả:
| \(i\) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| \(S[i]\) | a |
a |
b |
a |
a |
a |
b |
| \(Z[i]\) | — | \(1\) | \(0\) | \(2\) | \(3\) | \(1\) | \(0\) |
Tại sao reuse đúng?
Vì \(S[l..r]\) đã khớp với \(S[0..r-l]\), và \(i\) nằm trong \([l, r]\), nên:
Do đó \(Z[i] \geq Z[i-l]\), nhưng không vượt quá \(r - i + 1\) (phần đã biết khớp).
Đánh giá độ phức tạp¶
- Thời gian: \(O(N)\) cho Z-function, \(O(N + M)\) cho tìm kiếm mẫu. Mỗi lần so sánh ký tự, \(r\) tăng ít nhất \(1\), và \(r\) chỉ tăng, nên tổng số phép so sánh tối đa \(2N\).
- Không gian: \(O(N + M)\) — mảng \(Z\) trên xâu kết hợp.
Tìm kiếm xâu mẫu bằng Z-Algorithm:
Nối xâu thành \(P + \texttt{"\$"} + T\), ký tự \(\texttt{"\$"}\) không xuất hiện trong \(P\) hay \(T\). Tính mảng \(Z\) trên xâu kết hợp. Nếu \(Z[i] = |P|\) thì \(P\) xuất hiện trong \(T\) tại vị trí \(i - |P| - 1\).
3. So sánh các thuật toán tìm xâu mẫu¶
| Thuật toán | Độ phức tạp | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Brute-force | \(O(NM)\) | Đơn giản nhất | Chậm với dữ liệu lớn |
| KMP | \(O(N + M)\) | Ổn định, không dùng modulo | Khó hiểu hơn |
| Hash (Rabin-Karp) | \(O(N + M)\) | Linh hoạt, dễ code | Có thể collision |
| Z-Algorithm | \(O(N + M)\) | Không cần hash, không lo collision | Cần ký tự phân tách |
4. Lưu ý và bẫy hay gặp¶
Hash collision — Sai kết quả¶
Quên \(+\) MOD khi trừ¶
Chọn BASE không tốt¶
Quên modulo khi nhân¶
Z-Algorithm: Quên ký tự phân tách¶
Hash: Ký tự 'a' có giá trị 0 hay 1?¶
Bài tập luyện tập¶
Cơ bản¶
| Bài | FPTOJ | Độ khó | Chủ đề |
|---|---|---|---|
strh-hash |
Tính hash cơ bản | ⭐ | Hash cơ bản |
strh-find |
Tìm xâu con bằng Hash | ⭐⭐ | Tìm xâu bằng Hash |
strh-dist |
Đếm xâu con phân biệt | ⭐⭐ | Hash + Set |
strh-period |
Chu kỳ xâu | ⭐⭐ | Hash chu kỳ xâu |
strh-border |
Border xâu (Hash) | ⭐⭐ | Hash tìm border |
hsp-half |
Hai nửa xâu giống nhau | ⭐ | Hash so sánh nửa |
strk-zfunc |
Z-function | ⭐ | Z-function cơ bản |
Trung bình¶
| Bài | FPTOJ | Độ khó | Chủ đề |
|---|---|---|---|
strh-repeat |
Xâu con lặp dài nhất | ⭐⭐⭐ | Hash + Binary Search |
strh-palind |
Palindrome với Hash | ⭐⭐⭐ | Hash palindrome |
strh-lcs |
Xâu con chung dài nhất (Hash) | ⭐⭐⭐ | Hash xâu con chung |
strk-zmatch |
Tìm xâu với Z | ⭐⭐ | Z-algorithm tìm mẫu |
strk-kmp |
Tìm xâu mẫu KMP | ⭐⭐ | KMP tìm mẫu |
hsp-mx |
Xâu con xuất hiện nhiều nhất | ⭐⭐⭐ | Hash tần suất |
hsp-pal |
Đếm palindrome bằng Hash | ⭐⭐ | Hash palindrome |
hsp-nn |
Xâu con phân biệt khác chính nó | ⭐⭐ | Hash + Set |
hsp-dis |
Đếm xâu con phân biệt | ⭐⭐ | Hash + Set |
Nâng cao¶
| Bài | FPTOJ | Độ khó | Chủ đề |
|---|---|---|---|
stra-mana |
Palindrome dài nhất | ⭐⭐⭐ | Manacher palindrome |
stra-cntpal |
Đếm palindrome con | ⭐⭐⭐ | Đếm palindrome |
stra-sa |
Suffix Array | ⭐⭐⭐ | Suffix Array |
stra-lcp |
LCP trên SA | ⭐⭐⭐⭐ | LCP + RMQ |
stra-dist |
Đếm xâu con phân biệt | ⭐⭐⭐⭐ | SA + LCP đếm xâu |
hsp-cmp |
So sánh hai xâu con Double Hash | ⭐⭐⭐ | Double Hash |
hsp-prefix |
So sánh hash xâu con trực tuyến | ⭐⭐⭐ | Hash trực tuyến |
hsp-fc |
Xâu con phân biệt bắt đầu bằng ký tự | ⭐⭐⭐ | Hash + Set |
Bài viết liên quan¶
Tài liệu tham khảo¶
- VNOI Wiki - Hash
- VNOI Wiki - Z-function
- CP-Algorithms - String Hashing
- CP-Algorithms - Z-function
- GeeksforGeeks - Rabin-Karp Algorithm
- YouTube - String Hashing (Errichto)
Bài tiếp theo: Deque & Sliding Window →