KMP và Z-function
Tìm mọi vị trí mẫu p xuất hiện trong xâu t. So từng vị trí mất |t| × |p| bước. Hash làm nhanh nhưng có thể (rất hiếm) trùng. KMP và Z-function làm trong |t| + |p| bước và luôn đúng, nhờ tận dụng phần đã khớp thay vì so lại từ đầu.
Hàm tiền tố
Phần tiêu đề “Hàm tiền tố”pi[i] là độ dài biên dài nhất của i + 1 ký tự đầu s[0..i]: tiền tố dài nhất (không phải cả đoạn) đồng thời là hậu tố của đoạn đó.
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| s | a | b | a | c | a | b | a |
| pi | 0 | 0 | 1 | 0 | 1 | 2 | 3 |
pi[6] = 3 vì aba vừa là tiền tố vừa là hậu tố của abacaba.
Cách tính: biết pi[i - 1] = k (k ký tự đầu khớp với k ký tự kết thúc ở i - 1). Nếu s[i] = s[k] thì biên dài thêm 1: pi[i] = k + 1. Nếu không, thử biên ngắn hơn: biên tiếp theo của s[0..i - 1] là biên của s[0..k - 1], tức là k = pi[k - 1]. Lặp đến khi khớp hoặc k = 0.
Mỗi bước k tăng nhiều nhất 1, nên tổng số lần giảm không quá n: cả việc tính chỉ khoảng 2n bước.
def ham_tien_to(s): pi = [0] * len(s) for i in range(1, len(s)): k = pi[i - 1] while k > 0 and s[i] != s[k]: k = pi[k - 1] # thử biên ngắn hơn if s[i] == s[k]: k += 1 pi[i] = k return pi
t = input().strip()p = input().strip()pi = ham_tien_to(p + '#' + t) # '#' không có trong t, pm = len(p)vi_tri = [i - 2 * m + 1 for i in range(len(pi)) if pi[i] == m] # vị trí bắt đầu trong t, đánh số từ 1print(*vi_tri if vi_tri else [-1])#include <iostream>#include <string>#include <vector>using namespace std;
vector<int> hamTienTo(const string &s) { vector<int> pi(s.size(), 0); for (size_t i = 1; i < s.size(); i++) { int k = pi[i - 1]; while (k > 0 && s[i] != s[k]) k = pi[k - 1]; // thử biên ngắn hơn if (s[i] == s[k]) k++; pi[i] = k; } return pi;}
int main() { string t, p; cin >> t >> p; int m = p.size(); vector<int> pi = hamTienTo(p + '#' + t); // '#' không có trong t, p vector<int> viTri; for (int i = 0; i < (int)pi.size(); i++) if (pi[i] == m) viTri.push_back(i - 2 * m + 1); if (viTri.empty()) viTri.push_back(-1); for (size_t k = 0; k < viTri.size(); k++) cout << viTri[k] << (k + 1 < viTri.size() ? ' ' : '\n'); return 0;}Tìm mẫu
Phần tiêu đề “Tìm mẫu”Ghép xâu p + ’#’ + t (ký tự # không có trong cả hai xâu). Ở một vị trí trong phần t, pi bằng |p| khi và
chỉ khi |p| ký tự kết thúc ở đó đúng bằng p. Ký tự # ngăn không cho biên dài hơn |p|.
Z-function
Phần tiêu đề “Z-function”z[i] là độ dài tiền tố chung dài nhất của s và s[i:] (đoạn bắt đầu tại i). Quy ước z[0] = 0.
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| s | a | b | a | c | a | b | a |
| z | 0 | 0 | 1 | 0 | 3 | 0 | 1 |
z[4] = 3 vì s[4:] = aba khớp 3 ký tự đầu của s.
Cách tính: giữ “cửa sổ khớp” [l, r) là đoạn khớp với tiền tố mà r xa nhất đã biết. Ở vị trí i nằm trong cửa sổ, s[i:r] giống s[i - l : r - l], nên z[i] ít nhất là min(r - i, z[i - l]); chỉ cần so tiếp các ký tự sau r. Mỗi lần so thành công r tăng lên, nên cả bài khoảng 2n bước.
def ham_z(s): n = len(s) 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 zTìm mẫu bằng Z: tính Z của p + ’#’ + t, vị trí có z bằng |p| là chỗ p xuất hiện.
Luyện tập
Phần tiêu đề “Luyện tập”Gồm 12 bài, chia thành 3 nhóm từ dễ đến khó. Mỗi bài có một dữ liệu kiểm tra: các bạn chạy chương trình của mình với dữ liệu đó rồi so với đáp số.
Nhóm 1. Hàm tiền tố và Z-function
Phần tiêu đề “Nhóm 1. Hàm tiền tố và Z-function”Bài 1. Tính hàm tiền tố
Phần tiêu đề “Bài 1. Tính hàm tiền tố”Nhập xâu s (chữ cái thường, 1 ≤ |s| ≤ 106). In ra pi[0], pi[1], …, pi[|s| - 1], với pi[i] là độ dài biên dài nhất của s[0..i] (tiền tố dài nhất, khác cả đoạn, đồng thời là hậu tố).
Ví dụ: nhập abacaba thì in ra 0 0 1 0 1 2 3.
Kiểm tra: nhập aabaaabaaab.
Bài 2. Tính Z-function
Phần tiêu đề “Bài 2. Tính Z-function”Nhập xâu s (chữ cái thường, 1 ≤ |s| ≤ 106). In ra z[0], z[1], …, z[|s| - 1], với z[0] = 0 và z[i] là độ dài tiền tố chung dài nhất của s và s[i:].
Ví dụ: nhập abacaba thì in ra 0 0 1 0 3 0 1.
Kiểm tra: nhập aabaaabaaab.
Bài 3. Mọi vị trí xuất hiện
Phần tiêu đề “Bài 3. Mọi vị trí xuất hiện”Dòng đầu là xâu t, dòng thứ hai là mẫu p (chữ cái thường, 1 ≤ |p| ≤ |t| ≤ 106). In ra mọi vị trí (đánh số từ 1) mà p xuất hiện bắt đầu từ đó trong t, theo thứ tự tăng dần, hoặc -1 nếu không có.
Ví dụ:
| Nhập | In ra |
|---|---|
abababaaba | 1 3 5 |
Kiểm tra: nhập
bumbiibumbiibumbiibiiNhóm 2. Biên và chu kỳ
Phần tiêu đề “Nhóm 2. Biên và chu kỳ”Bài 4. Mọi biên của xâu
Phần tiêu đề “Bài 4. Mọi biên của xâu”Nhập xâu s (chữ cái thường, 1 ≤ |s| ≤ 106). In ra theo thứ tự tăng dần mọi độ dài k (1 ≤ k nhỏ hơn |s|) mà k ký tự đầu bằng k ký tự cuối, hoặc -1 nếu không có.
Ví dụ: nhập abacaba thì in ra 1 3.
Kiểm tra: nhập aabaaabaabaa.
Bài 5. Chu kỳ ngắn nhất
Phần tiêu đề “Bài 5. Chu kỳ ngắn nhất”Nhập xâu s (chữ cái thường, 1 ≤ |s| ≤ 106). Tìm độ dài k nhỏ nhất sao cho s[i] = s[i + k] với mọi i hợp lệ, tức là s là đoạn đầu của khối dài k viết lặp lại (k không cần là ước của |s|). In ra k.
Ví dụ: nhập abcabca thì in ra 3.
Kiểm tra: nhập aabaaabaaabaa.
Bài 6. Số lần xuất hiện của mỗi tiền tố
Phần tiêu đề “Bài 6. Số lần xuất hiện của mỗi tiền tố”Nhập xâu s (chữ cái thường, 1 ≤ |s| ≤ 106). Với mỗi k từ 1 đến |s|, in ra số lần tiền tố dài k của s xuất hiện trong s (tính cả lần ở đầu, các lần được chồng lên nhau).
Ví dụ: nhập abacaba thì in ra 4 2 2 1 1 1 1.
Kiểm tra: nhập aabaaabaa.
Bài 7. Mật khẩu ẩn giữa xâu
Phần tiêu đề “Bài 7. Mật khẩu ẩn giữa xâu”Nhập xâu s (chữ cái thường, 1 ≤ |s| ≤ 106). Tìm đoạn x dài nhất vừa là tiền tố, vừa là hậu tố của s, và còn xuất hiện ở giữa s (không phải ở đầu, không phải ở cuối). In ra x, hoặc NONE nếu không có.
Ví dụ: nhập fixprefixsuffix thì in ra fix.
Kiểm tra: nhập abcdabcabcdabc.
Nhóm 3. Ghép xâu để tìm kiếm
Phần tiêu đề “Nhóm 3. Ghép xâu để tìm kiếm”Bài 8. Xâu đối xứng ngắn nhất bằng cách thêm vào đầu
Phần tiêu đề “Bài 8. Xâu đối xứng ngắn nhất bằng cách thêm vào đầu”Nhập xâu s (chữ cái thường, 1 ≤ |s| ≤ 106). Thêm ít ký tự nhất vào đầu s để được xâu đối xứng. In ra số ký tự phải thêm.
Ví dụ: nhập aacecaaa thì in ra 1.
Kiểm tra: nhập abcdcbaxyz.
Bài 9. Ghép hai xâu chồng nhau
Phần tiêu đề “Bài 9. Ghép hai xâu chồng nhau”Nhập hai xâu a và b trên hai dòng (chữ cái thường, mỗi xâu dài từ 1 đến 106). Ghép b vào sau a, cho phép phần đầu của b trùng với phần cuối của a (chỉ viết một lần). In ra độ dài ngắn nhất của xâu ghép.
Ví dụ:
| Nhập | In ra |
|---|---|
abcdecdefg | 7 |
Kiểm tra: nhập
bumbiibumbbiibumbiiBài 10. Mẫu trên vòng tròn
Phần tiêu đề “Bài 10. Mẫu trên vòng tròn”Xâu t được viết trên một vòng tròn (sau ký tự cuối là ký tự đầu). Dòng đầu là t, dòng thứ hai là mẫu p (chữ cái thường, 1 ≤ |p| ≤ |t| ≤ 106). In ra số vị trí bắt đầu trên vòng tròn mà đọc |p| ký tự theo chiều kim đồng hồ được đúng p.
Ví dụ:
| Nhập | In ra |
|---|---|
abcabbca | 1 |
Kiểm tra: nhập
abaababaBài 11. Ghép từ các tiền tố
Phần tiêu đề “Bài 11. Ghép từ các tiền tố”Dòng đầu là xâu p, dòng thứ hai là xâu t (chữ cái thường, mỗi xâu dài từ 1 đến 106). Viết t thành ghép của ít đoạn nhất, mỗi đoạn là một tiền tố (khác rỗng) của p. In ra số đoạn ít nhất, hoặc -1 nếu không viết được.
Ví dụ:
| Nhập | In ra |
|---|---|
abcabababcaa | 3 |
Kiểm tra: nhập
bumbiibumbubbumbibuBài 12. Xoay vòng nhỏ nhất theo từ điển
Phần tiêu đề “Bài 12. Xoay vòng nhỏ nhất theo từ điển”Nhập xâu s (chữ cái thường, 1 ≤ |s| ≤ 106). Trong |s| cách xoay s (chuyển k ký tự đầu xuống cuối, 0 ≤ k nhỏ hơn |s|), in ra cách xoay cho xâu nhỏ nhất theo thứ tự từ điển (nếu nhiều k cùng cho xâu đó, chọn k nhỏ nhất) dưới dạng số k.
Ví dụ: nhập bcaab thì in ra 2.
Kiểm tra: nhập cabcabcab.