Gõ một từ vào ô tìm kiếm, trình soạn thảo tô mọi chỗ nó xuất hiện. Một cách làm việc đó: ở mỗi vị trí của văn bản, hỏi đoạn bắt đầu tại đây khớp với đầu từ cần tìm được bao xa; khớp đủ cả từ thì đó là một lần xuất hiện.
Hàm Z trả lời câu hỏi “khớp với đầu xâu bao xa” cho mọi vị trí cùng lúc: là độ dài đoạn dài nhất bắt đầu tại trùng với một tiền tố của , quy ước . Cả mảng tính trong nhờ một “hộp” nhớ đoạn khớp xa nhất đã thấy, và làm được hầu hết việc của hàm tiền tố KMP (bài 21): tìm mẫu, liệt kê viền, tìm chu kỳ, đếm số lần xuất hiện của mỗi tiền tố.
Hàm Z và hộp
Duyệt từ 1 tới , giữ hộp : đoạn khớp với tiền tố có đầu phải xa nhất đã tìm được, tức . Nếu , vị trí nằm trong hộp nên đoạn bắt đầu tại giống đoạn bắt đầu tại của tiền tố, tới hết hộp: lấy sẵn . Rồi so tiếp từng ký tự từ ra ngoài. Nếu đoạn mới vượt quá thì dời hộp thành .
Mỗi phép so sánh thành công làm tăng 1 (vì so ngoài hộp), còn mỗi có nhiều nhất một phép so sánh hỏng. không vượt , nên tổng số phép so sánh là . Bỏ hộp đi, so lại từ đầu ở mỗi , thì trên xâu ngẫu nhiên vẫn nhanh vì mỗi lần chỉ khớp vài ký tự; nhưng xâu toàn một chữ lại cho :
Hộp nhanh gấp hàng nghìn lần ở đúng loại test mà người ra đề hay cài.
void zfunc(string &s, ll z[]) {
ll n = s.size();
z[0] = n;
for (ll i = 1, l = 0, r = 0; i < n; i++) {
z[i] = 0;
if (i < r) z[i] = min(r - i, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] > r) l = i, r = i + z[i];
}
}
Hỏi nhanh
Hộp hiện tại là và . Khi tính , giá trị ban đầu trước khi so tiếp là bao nhiêu?
nằm trong hộp, , nên lấy . Không lấy 7 vì ngoài hộp ta chưa biết có còn khớp với tiền tố không; phần đó phải so trực tiếp.
Ví dụDấu Vết Tiền Tố
Đề. Xâu dài tới . In mảng với .
Lời giải. Đúng hàm zfunc ở trên; z[0] = n theo quy ước của đề.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 5e5 + 5;
ll z[N];
string s;
void zfunc(string &s, ll z[]) {
ll n = s.size();
z[0] = n;
for (ll i = 1, l = 0, r = 0; i < n; i++) {
z[i] = 0;
if (i < r) z[i] = min(r - i, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] > r) l = i, r = i + z[i];
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
zfunc(s, z);
for (ll i = 0; i < (ll)s.size(); i++) cout << z[i] << ' ';
cout << '\n';
}
abacaba
7 0 1 0 3 0 1
.
Tìm mẫu
Muốn tìm mẫu trong văn bản , tính hàm Z của với # không có trong hai xâu. Ở vị trí thuộc phần , bị chặn bởi vì không đoạn nào vượt qua # được; đúng khi xuất hiện ở đó. Cả bài , bộ nhớ cho xâu ghép.
Ví dụCác Vị Trí Khớp
Đề. Văn bản và mẫu , mỗi xâu dài tới . In mọi vị trí (đánh số từ 1) mà bắt đầu trong , kể cả chồng lấn; không có thì in 0.
Lời giải. Ghép mẫu, một ký tự ngăn cách và văn bản: . # không có trong hai xâu, nên ở mọi vị trí thuộc phần , giá trị không vượt ; nghĩa là khớp trọn tại đó. Hàm zfunc như ví dụ trước.
Bước 1: quét phần văn bản. Vị trí của xâu ghép ứng với vị trí của khi đánh số từ 1.
for (ll i = m + 1; i < (ll)s.size(); i++)
if (z[i] >= m) {
cout << i - m << ' ';
any = 1;
}
Xâu ghép dài tới , mảng z khai báo đủ.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e6 + 5;
ll z[N];
string a, b;
void zfunc(string &s, ll z[]) {
ll n = s.size();
z[0] = n;
for (ll i = 1, l = 0, r = 0; i < n; i++) {
z[i] = 0;
if (i < r) z[i] = min(r - i, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] > r) l = i, r = i + z[i];
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> a >> b;
string s = b + "#" + a;
zfunc(s, z);
ll m = b.size();
bool any = 0;
for (ll i = m + 1; i < (ll)s.size(); i++)
if (z[i] >= m) {
cout << i - m << ' ';
any = 1;
}
if (!any) cout << 0;
cout << '\n';
}
bananana
ana
2 4 6
.
Viền và chu kỳ
Hậu tố bắt đầu tại là một viền (vừa là tiền tố, vừa là hậu tố) khi và chỉ khi nó khớp với tiền tố tới hết xâu: . Viền đó dài . Một số là chu kỳ của xâu khi với mọi , tức hậu tố từ trùng tiền tố cùng độ dài: lại là . Vậy chu kỳ và viền dài là một (như bài 21), và luôn là chu kỳ. Liệt kê tất cả chỉ cần một vòng qua mảng , ví dụ in mọi chu kỳ theo thứ tự tăng:
for (ll p = 1; p < n; p++)
if (z[p] == n - p) cout << p << ' ';
cout << n << '\n';
Hỏi nhanh
Xâu abababa có . Điều đó cho biết gì?
và , nên đoạn từ vị trí 2 khớp tiền tố tới cuối xâu: hậu tố dài 5 bằng tiền tố dài 5. Cùng điều kiện đó nói với mọi , tức 2 là chu kỳ.
Đếm lần xuất hiện
Tiền tố dài xuất hiện tại vị trí khi và chỉ khi . Vậy số lần xuất hiện của mọi tiền tố tính một lượt: đếm là số vị trí có , rồi cộng dồn từ lớn xuống: số vị trí có . Kết hợp với điều kiện viền () cho các bài “tiền tố vừa là hậu tố, xuất hiện bao nhiêu lần”, “viền dài nhất còn xuất hiện ở giữa”.
Ví dụBiên Và Số Lần Xuất Hiện
Đề. Xâu dài tới . Với mọi độ dài mà tiền tố dài cũng là hậu tố (kể cả cả xâu), in và số lần tiền tố đó xuất hiện, theo tăng dần.
Lời giải. Tiền tố dài xuất hiện tại khi và chỉ khi . Vậy số lần xuất hiện là số vị trí có .
Bước 1: đếm theo giá trị . cnt[v] là số vị trí có , kể cả . Cộng dồn từ lớn xuống thì cnt[L] là số vị trí có .
for (ll i = 0; i < n; i++) cnt[z[i]]++;
for (ll v = n - 1; v >= 0; v--) cnt[v] += cnt[v + 1];
Bước 2: chọn các viền. Tiền tố dài cũng là hậu tố khi z[n - L] == L; cả xâu () luôn thỏa và xuất hiện 1 lần.
for (ll L = 1; L <= n; L++)
if (L == n || z[n - L] == L) res.push_back({L, cnt[L]});
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e5 + 5;
ll z[N], cnt[N];
string s;
void zfunc(string &s, ll z[]) {
ll n = s.size();
z[0] = n;
for (ll i = 1, l = 0, r = 0; i < n; i++) {
z[i] = 0;
if (i < r) z[i] = min(r - i, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] > r) l = i, r = i + z[i];
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
ll n = s.size();
zfunc(s, z);
for (ll i = 0; i < n; i++) cnt[z[i]]++;
for (ll v = n - 1; v >= 0; v--) cnt[v] += cnt[v + 1];
vector<pair<ll, ll>> res;
for (ll L = 1; L <= n; L++)
if (L == n || z[n - L] == L) res.push_back({L, cnt[L]});
cout << res.size() << '\n';
for (pair<ll, ll> p : res) cout << p.first << ' ' << p.second << '\n';
}
ABACABA
3
1 4
3 2
7 1
.
Ví dụMật Khẩu Ba Lần
Đề. Xâu dài tới . Tìm xâu dài nhất vừa là tiền tố, vừa là hậu tố, và còn xuất hiện ở một vị trí bắt đầu khác; không có thì in Just a legend.
Lời giải. Viền dài ứng với . Lần xuất hiện ở giữa phải bắt đầu tại một với và ; bắt đầu từ trở đi thì không còn đủ ký tự trước cuối xâu (trừ chính viền). Duyệt tăng dần, giữ mx là trên các . tăng thì giảm, nên viền đầu tiên thỏa là dài nhất.
for (ll i = 1; i < n; i++) {
if (z[i] == n - i && mx >= n - i) {
cout << s.substr(0, n - i) << '\n';
return 0;
}
mx = max(mx, z[i]);
}
mx cập nhật sau khi xét , để chỉ tính các đứng trước.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e6 + 5;
ll z[N];
string s;
void zfunc(string &s, ll z[]) {
ll n = s.size();
z[0] = n;
for (ll i = 1, l = 0, r = 0; i < n; i++) {
z[i] = 0;
if (i < r) z[i] = min(r - i, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] > r) l = i, r = i + z[i];
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
ll n = s.size();
zfunc(s, z);
ll mx = 0;
for (ll i = 1; i < n; i++) {
if (z[i] == n - i && mx >= n - i) {
cout << s.substr(0, n - i) << '\n';
return 0;
}
mx = max(mx, z[i]);
}
cout << "Just a legend\n";
}
aaaaa
aaa
.
Ghép bằng tiền tố
Hàm Z của cho, tại mỗi vị trí của , độ dài của tiền tố dài nhất của bắt đầu ở đó. Mọi tiền tố ngắn hơn cũng bắt đầu được ở . Vậy từ vị trí của có thể nhảy tới bất kỳ vị trí nào trong bằng một mảnh. Ghép bằng ít mảnh nhất là bài nhảy xa quen thuộc: duyệt theo từng “tầng” của số mảnh, mỗi tầng mở rộng tới vị trí xa nhất mà các điểm trong tầng trước nhảy tới được, .
Ví dụGhép Đích Từ Các Tiền Tố
Đề. Xâu , dài tới . Ghép từ ít tiền tố của nhất (được dùng lại), hoặc in .
Lời giải. Từ vị trí của , một mảnh là tiền tố của khớp tại . Tiền tố dài nhất khớp tại là , lấy từ hàm Z của ; mọi tiền tố ngắn hơn cũng khớp, nên từ nhảy được tới bất kỳ vị trí nào trong . Bài thành: đi từ 0 tới bằng ít bước nhảy nhất.
Bước 1: độ dài nhảy. z[m + 1 + i] là .
Bước 2: tham lam theo tầng. cur là vị trí xa nhất tới được bằng ans mảnh, far là vị trí xa nhất tới được bằng thêm một mảnh từ các vị trí . Khi chạm cur, mọi vị trí tới được bằng ans mảnh đã xét, nên phải dùng thêm một mảnh và đặt cur = far. Nếu far không vượt thì kẹt, in .
for (ll i = 0; i < n && cur < n; i++) {
far = max(far, i + z[m + 1 + i]);
if (i == cur) {
if (far <= i) {
cout << -1 << '\n';
return 0;
}
ans++;
cur = far;
}
}
Mỗi tầng đi xa nhất có thể, nên số mảnh ít nhất.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e6 + 5;
ll z[N];
string a, t;
void zfunc(string &s, ll z[]) {
ll n = s.size();
z[0] = n;
for (ll i = 1, l = 0, r = 0; i < n; i++) {
z[i] = 0;
if (i < r) z[i] = min(r - i, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] > r) l = i, r = i + z[i];
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> a >> t;
string s = a + "#" + t;
zfunc(s, z);
ll m = a.size(), n = t.size(), ans = 0, cur = 0, far = 0;
for (ll i = 0; i < n && cur < n; i++) {
far = max(far, i + z[m + 1 + i]);
if (i == cur) {
if (far <= i) {
cout << -1 << '\n';
return 0;
}
ans++;
cur = far;
}
}
cout << ans << '\n';
}
aba
ababa
2
.
Khi đi thi
- Hàm Z và hàm tiền tố làm được cùng lớp bài; Z tiện khi câu hỏi là “khớp với đầu xâu bao xa” tại từng vị trí, hàm tiền tố tiện khi đi một chiều qua văn bản dài (luồng vào) hay cần tự động.
- Xâu ghép phải dùng ký tự ngăn không có trong hai xâu; muốn hỏi khớp với cuối xâu, đảo ngược cả hai trước.
- là viền và cũng là chu kỳ ; đếm lần xuất hiện của mọi tiền tố bằng đếm theo giá trị rồi cộng dồn.
Tóm tắt
- : độ dài khớp dài nhất giữa đoạn từ và tiền tố; tính trong nhờ hộp và .
- Tìm mẫu: trong hàm Z của .
- Viền dài và chu kỳ khi .
- Số lần xuất hiện của tiền tố dài là số có .
- Hàm Z trên xâu ghép cho bước nhảy xa nhất của bài ghép bằng tiền tố.
Bài tập
Đọc thêm
- Z-functionVNOI Wiki
- Z-function and its calculationcp-algorithms
- Z 函数(扩展 KMP)OI Wiki
- String SearchingUSACO Guide