Cho văn bản abababcab và mẫu ababc. Muốn tìm mẫu, ta thử từng vị trí bắt đầu và so từng chữ. Hỏng ở chữ nào thì dịch sang vị trí kế rồi so lại từ đầu mẫu.
Cách đó vứt bỏ phần đã khớp, trong khi phần ấy cho ta biết khá nhiều về những vị trí kế tiếp. Thuật toán Knuth-Morris-Pratt (KMP) giữ lại đúng phần còn dùng được, nhờ một mảng tính sẵn trên mẫu gọi là hàm tiền tố. Con trỏ trên văn bản không bao giờ lùi, nên cả bài chạy trong .
Hàm tiền tố còn là công cụ riêng: nó cho mọi viền của xâu, chu kỳ ngắn nhất của mọi tiền tố, và biến thành một tự động dùng được trong quy hoạch động.
Hàm tiền tố
Một viền (border) của xâu là một xâu khác rỗng, ngắn hơn , vừa là tiền tố vừa là hậu tố của . Xâu aabaabaa có ba viền: aabaa, aa và a. Đánh số từ 1 và gọi là độ dài viền dài nhất của tiền tố , bằng 0 nếu không có viền. Với ababa, : tiền tố dài 5 có viền dài nhất aba.
Tính chất then chốt: viền của một viền cũng là viền. Nếu là viền của và là viền của thì là tiền tố của , nên là tiền tố của ; và là hậu tố của , nên là hậu tố của . Ngược lại, mọi viền ngắn hơn của cũng là viền của viền dài nhất. Vậy các viền của chính là dãy cho tới 0; với aabaabaa là .
Tính từ trái sang phải. Giả sử đã có và đặt . Viền của là một viền của nối thêm . Thử viền dài nhất trước: nếu thì . Nếu không, thử viền dài nhất kế tiếp của , chính là , và cứ thế; về tới mà vẫn không khớp thì .
for (ll i = 2, j = 0; i <= n; i++) {
while (j && s[i] != s[j + 1]) j = pi[j];
if (s[i] == s[j + 1]) j++;
pi[i] = j;
}
Dù có vòng while, tổng thời gian là . Biến tăng nhiều nhất 1 ở mỗi , mỗi lần lùi làm giảm ít nhất 1, và không âm. Số lần giảm không thể vượt số lần tăng, tức không quá .
Hỏi nhanh
Trong xâu ababa, giá trị nói lên điều gì?
là độ dài viền của một tiền tố, không phải tần suất hay số lần xuất hiện. Với ababa, tiền tố và hậu tố dài 3 đều là aba; độ dài 4 không được vì abab khác baba.
Ví dụCác Viền Của Xâu
Đề. Cho xâu dài tới . In độ dài mọi viền theo thứ tự tăng dần, không có viền thì in 0.
Lời giải. Tính của cả xâu, rồi đi theo dãy tới 0: đó đúng là mọi viền, từ dài tới ngắn. Lưu lại rồi in ngược. Thử mọi độ dài và so hai đầu bằng tay là . Với aaaaa, và dãy là .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e6 + 5;
ll n, pi[N];
string s;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
n = s.size();
s = " " + s;
for (ll i = 2, j = 0; i <= n; i++) {
while (j && s[i] != s[j + 1]) j = pi[j];
if (s[i] == s[j + 1]) j++;
pi[i] = j;
}
vector<ll> v;
for (ll k = pi[n]; k; k = pi[k]) v.push_back(k);
reverse(v.begin(), v.end());
if (v.empty()) cout << 0;
for (ll x : v) cout << x << ' ';
cout << '\n';
}
aaaaa
1 2 3 4
Tìm mẫu
Quét văn bản từ trái sang, giữ là số ký tự đầu của khớp với hậu tố dài nhất của phần văn bản đã đọc. Đọc thêm : nếu thì tăng; nếu không, phần khớp mới phải là một viền của nối thêm , nên lùi và thử lại, đúng như lúc tính . Khi là một lần xuất hiện kết thúc tại ; ghi nhận rồi đặt để tiếp tục, chứ không về 0, nhờ vậy các lần xuất hiện chồng lấn không bị bỏ sót.
Con trỏ chạy đúng một lượt qua văn bản, còn tăng nhiều nhất 1 mỗi bước và mỗi lần lùi giảm ít nhất 1.
Độ phức tạp: tổng thời gian là kể cả tính , bộ nhớ .
Một mẹo cài gọn: tính của xâu ghép , với # không xuất hiện trong hai xâu; vị trí nào trong phần có là một lần xuất hiện. KMP chỉ cần so hai phần tử bằng nhau, nên chạy được trên dãy số hay bất cứ thứ gì so sánh được.
Khác biệt với cách thử từng vị trí lớn nhất ở những xâu lặp nhiều:
KMP nhanh gấp hàng trăm lần. Cách thử từng vị trí chỉ đủ nhanh khi mẫu rất ngắn.
Hỏi nhanh
Khi ký tự đầu của mẫu đã khớp nhưng ký tự kế tiếp không khớp, vì sao có thể chuyển thành ?
Sau khi hỏng, phần hậu tố đã đọc muốn còn hữu ích thì phải là tiền tố của ; nó đồng thời là hậu tố của đoạn vừa khớp . Xâu dài nhất thỏa cả hai là viền dài nhất, ; các viền ngắn hơn được thử tiếp ở những lần lùi sau. không phải chỉ số văn bản, không phải độ dài nào cũng hợp lệ, và bước lùi không nhất thiết chỉ một ký tự.
Ví dụĐếm Lần Khớp
Đề. Cho văn bản và mẫu , mỗi xâu dài tới . Đếm số lần xuất hiện trong , kể cả chồng lấn.
Lời giải. Tính trên mẫu, quét văn bản như trên, mỗi lần chạm thì đếm và lùi về . Đặt về 0 sau mỗi lần khớp là lỗi kinh điển: với aaa trong aaaaa nó đếm 1 thay vì 3. Hai xâu đánh số từ 1 bằng cách thêm một ký tự đệm ở đầu. Với ví dụ, mỗi khối năm chữ a có ba vị trí khớp, tổng 6.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e6 + 5;
ll pi[N];
string t, p;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t >> p;
ll n = t.size(), m = p.size();
t = " " + t;
p = " " + p;
for (ll i = 2, j = 0; i <= m; i++) {
while (j && p[i] != p[j + 1]) j = pi[j];
if (p[i] == p[j + 1]) j++;
pi[i] = j;
}
ll ans = 0;
for (ll i = 1, j = 0; i <= n; i++) {
while (j && t[i] != p[j + 1]) j = pi[j];
if (t[i] == p[j + 1]) j++;
if (j == m) {
ans++;
j = pi[j];
}
}
cout << ans << '\n';
}
aaaaabaaaaa
aaa
6
Chu kỳ và cây viền
Số là một chu kỳ của xâu dài nếu với mọi hợp lệ, tức dịch sang phải ô thì phần chồng lên nhau trùng khớp. Xâu có chu kỳ khi và chỉ khi nó có viền dài : phần chồng lên nhau chính là tiền tố và hậu tố dài . Vì vậy chu kỳ ngắn nhất là , ứng với viền dài nhất. Chẳng hạn một thông điệp lặp vô hạn mà ta chỉ thấy ký tự đầu thì dài ít nhất , và không cần chia hết cho độ dài ấy.
Chu kỳ chưa chắc chia hết độ dài: abcabca có chu kỳ 3 dù dài 7. Xâu là lặp nguyên vẹn lần khi và chỉ khi chu kỳ ngắn nhất chia hết và ; khi đó số lần lặp lớn nhất là . Điều này đúng nhờ một bổ đề quen: nếu là chu kỳ nhỏ nhất và một chu kỳ chia hết thì cũng chia hết , nên không có cách lặp nào “thô” hơn bị bỏ sót.
Cây viền. Nối mỗi độ dài với cha ta được một cây gốc 0: mỗi số có đúng một cha nhỏ hơn nó, đi theo cha mãi sẽ về 0. Các số gặp trên đường từ về 0 là tổ tiên của ; các số đi theo cha sẽ qua (kể cả ) tạo thành cây con của . Các viền của tiền tố dài là các tổ tiên của . Ngược lại, tiền tố dài xuất hiện kết thúc tại vị trí khi và chỉ khi là một viền của (hoặc chính ), tức là tổ tiên của . Vậy số lần tiền tố dài xuất hiện trong bằng số nút trong cây con của , tính được bằng cách cộng dồn với giảm dần, xuất phát từ .
Ví dụChu Kỳ Của Các Tiền Tố
Đề. Nhiều test (tổng độ dài ). Với mỗi xâu , liệt kê theo độ dài tăng dần mọi tiền tố là một xâu ngắn hơn lặp nguyên vẹn ít nhất 2 lần, kèm số lần lặp lớn nhất. Mỗi test in dòng Test case #x rồi một dòng trống ở cuối.
Lời giải. Với tiền tố dài , chu kỳ ngắn nhất là . Tiền tố là lặp khi và chia hết ; số lần lặp là . Một lượt tính trả lời mọi tiền tố, . Với ababab, cho và cho ; có chu kỳ 2 không chia hết 5.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e6 + 5;
ll pi[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll T;
cin >> T;
for (ll tc = 1; tc <= T; tc++) {
ll n;
string s;
cin >> n >> s;
s = " " + s;
for (ll i = 2, j = 0; i <= n; i++) {
while (j && s[i] != s[j + 1]) j = pi[j];
if (s[i] == s[j + 1]) j++;
pi[i] = j;
}
cout << "Test case #" << tc << '\n';
for (ll i = 2; i <= n; i++) {
ll k = i - pi[i];
if (pi[i] && i % k == 0) cout << i << ' ' << i / k << '\n';
}
cout << '\n';
}
}
1
6
ababab
Test case #1
4 2
6 3
Dãy số, xâu vòng
Dãy sai khác một hằng số. Muốn biết đoạn có bằng cộng một hằng số không, so hai dãy hiệu kề nhau: cộng cùng một số không đổi hiệu, và hai dãy có cùng dãy hiệu thì sai khác một hằng số. Bài thành tìm dãy hiệu của trong dãy hiệu của bằng KMP trên số nguyên, nhớ trường hợp có dãy hiệu rỗng và khớp mọi vị trí.
Xâu vòng. Các phép xoay của là các đoạn dài của . Hỏi có phải một phép xoay của không, hay có bao nhiêu điểm bắt đầu trên vòng cho ra , là tìm trong (bỏ ký tự cuối để không đếm trùng điểm đầu). Với các tập điểm trên vòng tròn, như vạch trên mặt đồng hồ, ta sắp các điểm rồi lấy khoảng cách giữa hai điểm liên tiếp theo vòng: quay cả hình không đổi các khoảng cách, chỉ xoay vòng dãy của chúng.
Ví dụHai Mặt Số Không Mốc
Đề. Hai hình, mỗi hình có vạch ở các góc nguyên khác nhau trong . Hỏi có quay được hình thứ nhất (theo modulo 360000) để hai tập vạch trùng nhau không; in possible hoặc impossible.
Lời giải. Sắp góc của mỗi hình, lấy dãy khoảng cách theo vòng và . Quay hình là cộng cùng một góc vào mọi vạch, nên tập vạch của hình thứ nhất sau khi quay trùng hình thứ hai khi và chỉ khi dãy khoảng cách của hình hai là một phép xoay vòng của dãy khoảng cách hình một. Tìm dãy của hình hai trong dãy hình một viết hai lần bằng KMP trên số nguyên, sau khi sắp. Với ví dụ, cả hai dãy đều là .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5, M = 360000;
ll n, a[N], b[N], t[2 * N], p[N], pi[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> a[i];
for (ll i = 1; i <= n; i++) cin >> b[i];
sort(a + 1, a + n + 1);
sort(b + 1, b + n + 1);
for (ll i = 1; i <= n; i++) {
t[i] = t[i + n] = (i < n ? a[i + 1] - a[i] : a[1] + M - a[n]);
p[i] = (i < n ? b[i + 1] - b[i] : b[1] + M - b[n]);
}
for (ll i = 2, j = 0; i <= n; i++) {
while (j && p[i] != p[j + 1]) j = pi[j];
if (p[i] == p[j + 1]) j++;
pi[i] = j;
}
bool ok = 0;
for (ll i = 1, j = 0; i < 2 * n && !ok; i++) {
while (j && t[i] != p[j + 1]) j = pi[j];
if (t[i] == p[j + 1]) j++;
if (j == n) ok = 1;
}
cout << (ok ? "possible" : "impossible") << '\n';
}
4
0 90000 180000 270000
45000 135000 225000 315000
possible
Tự động KMP
Trạng thái của KMP, “đang khớp ký tự đầu của mẫu”, gói gọn mọi thứ cần biết về phần văn bản đã đọc để quyết định mẫu có xuất hiện tiếp hay không. Tính sẵn bảng chuyển : từ trạng thái đọc chữ thì sang trạng thái nào. Bảng tính được trong , với là số chữ cái, bằng chính quy tắc lùi: nếu , ngược lại bằng (với khi ). Đây là một tự động (automaton) hữu hạn có trạng thái.
Chính bảng này cũng làm KMP chạy nhanh hơn: mỗi ký tự văn bản chỉ tra một lần, không còn vòng while.
Bảng chuyển nhanh gấp khoảng 2,5 lần khi bảng chữ cái nhỏ. Bảng chữ cái 26 chữ thì bảng ô cũng tốn bộ nhớ, nên chỉ dựng khi cần làm trạng thái quy hoạch động.
Tự động này làm trạng thái cho quy hoạch động trên xâu: đếm xâu độ dài chứa (hoặc không chứa) mẫu, xâu con chung dài nhất tránh một mẫu cấm, điền ký tự để mẫu xuất hiện nhiều nhất. là số cách viết ký tự đầu mà tự động đang ở trạng thái ; thêm một chữ là đi theo . Đây cũng là khuôn của quy hoạch động chữ số (bài 13) với trạng thái là một tự động, và mở rộng tự nhiên sang nhiều mẫu với Aho-Corasick (bài 73).
Ví dụĐếm Xâu Có Mẫu Bắt Buộc
Đề. Cho và mẫu chữ hoa (). Đếm số xâu độ dài trên 26 chữ hoa có chứa ít nhất một lần, theo modulo .
Lời giải. Đếm bù: lấy trừ số xâu không chứa . Đọc một xâu từng chữ qua tự động KMP của ; xâu không chứa khi và chỉ khi trạng thái chưa bao giờ chạm . Vậy chỉ cần đếm số xâu theo trạng thái hiện tại. Với và AB: có xâu, trong đó 52 xâu chứa AB (26 xâu dạng ABx và 26 xâu dạng xAB, không xâu nào thuộc cả hai).
Bước 1: hàm tiền tố của mẫu. Như mọi bài KMP, mẫu đánh số từ 1 nhờ ký tự đệm ở đầu.
for (ll i = 2, j = 0; i <= m; i++) {
while (j && p[i] != p[j + 1]) j = pi[j];
if (p[i] == p[j + 1]) j++;
pi[i] = j;
}
Bước 2: bảng chuyển của tự động. go[j][c] là trạng thái mới khi đang ở (đã khớp ký tự đầu của mẫu) và đọc chữ . Khớp ký tự kế thì lên ; không khớp thì đi như từ trạng thái , đã tính xong vì . Bảng biến mỗi bước của KMP thành một lần tra, không còn vòng while.
for (ll j = 0; j < m; j++)
for (ll c = 0; c < 26; c++) {
if (p[j + 1] - 'A' == c) go[j][c] = j + 1;
else go[j][c] = j ? go[pi[j]][c] : 0;
}
Bước 3: đếm theo trạng thái. là số xâu đã dựng có trạng thái . Thêm một chữ: mọi xâu ở đi sang go[j][c] với 26 chữ ; chuyển nào chạm thì bỏ, vì xâu đó đã chứa mẫu. Song song tính .
for (ll i = 0; i < n; i++) {
all = all * 26 % MOD;
for (ll j = 0; j < m; j++) nd[j] = 0;
for (ll j = 0; j < m; j++)
for (ll c = 0; c < 26; c++)
if (go[j][c] < m) nd[go[j][c]] = (nd[go[j][c]] + dp[j]) % MOD;
for (ll j = 0; j < m; j++) dp[j] = nd[j];
}
Đáp án là trừ tổng các , cộng thêm MOD để không âm.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7;
ll n, m, pi[105], go[105][26], dp[105], nd[105];
string p;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> p;
m = p.size();
p = " " + p;
for (ll i = 2, j = 0; i <= m; i++) {
while (j && p[i] != p[j + 1]) j = pi[j];
if (p[i] == p[j + 1]) j++;
pi[i] = j;
}
for (ll j = 0; j < m; j++)
for (ll c = 0; c < 26; c++) {
if (p[j + 1] - 'A' == c) go[j][c] = j + 1;
else go[j][c] = j ? go[pi[j]][c] : 0;
}
dp[0] = 1;
ll all = 1;
for (ll i = 0; i < n; i++) {
all = all * 26 % MOD;
for (ll j = 0; j < m; j++) nd[j] = 0;
for (ll j = 0; j < m; j++)
for (ll c = 0; c < 26; c++)
if (go[j][c] < m) nd[go[j][c]] = (nd[go[j][c]] + dp[j]) % MOD;
for (ll j = 0; j < m; j++) dp[j] = nd[j];
}
ll bad = 0;
for (ll j = 0; j < m; j++) bad = (bad + dp[j]) % MOD;
cout << (all - bad + MOD) % MOD << '\n';
}
3
AB
52
Chi phí .
Khi đi thi
- Thấy tìm mẫu, viền, chu kỳ hay “lặp nguyên vẹn” trên xâu dài tới : tính ; KMP không có xác suất sai như hash (bài 19) và không tốn bộ nhớ như cây hậu tố.
- Thống nhất một quy ước chỉ số (ở đây đánh số từ 1, theo độ dài) và giữ nó trong cả bài; sau mỗi lần khớp lùi về , không về 0, để giữ các lần chồng lấn.
- Bài so sánh có biến đổi (cộng hằng số, xoay vòng, đổi bảng chữ cái) thường quy về KMP trên dãy hiệu hoặc trên ; bài đếm xâu chứa hay tránh mẫu thì dùng tự động KMP làm trạng thái quy hoạch động.
Tóm tắt
- là độ dài viền dài nhất của ; mọi viền là dãy ; tính trong vì tăng tối đa lần.
- KMP giữ là độ dài khớp hiện tại, lùi theo khi hỏng, không quay lại văn bản; tổng .
- Chu kỳ ngắn nhất là ; xâu là lặp nguyên vẹn khi chu kỳ ấy chia hết và nhỏ hơn .
- Cây viền (cha của là ) cho số lần xuất hiện của mọi tiền tố bằng cộng dồn cây con.
- Bảng chuyển biến KMP thành tự động, làm trạng thái cho quy hoạch động đếm xâu.
Bài tập
Đọc thêm
- Thuật toán KMPVNOI Wiki
- Prefix function. Knuth-Morris-Pratt algorithmcp-algorithms
- 前缀函数与 KMP 算法OI Wiki
- String SearchingUSACO Guide
- Knuth-Morris-Pratt algorithmWikipedia