Với xâu dài (đánh số từ 0), 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 . Mảng trả lời trực tiếp những câu hỏi “hậu tố này khớp với đầu xâu tới đâu”, nên 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ố. Thuật toán tính cả mảng trong nhờ một “hộp” ghi nhớ đoạn khớp xa nhất đã thấy.
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à .
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. Giữ khung : đoạn khớp tiền tố có đầu phải xa nhất đã tìm được, tức trùng . Vị trí nằm trong khung thì đã biết trước một phần độ dài khớp, chỉ so tiếp từ đó.
Bước 1: mượn kết quả cũ. thì trùng , mà ở vị trí đã biết . Lấy min với r - i vì ngoài khung chưa biết gì.
if (i < r) z[i] = min(r - i, z[i - l]);
Bước 2: so tiếp và dời khung. So từng ký tự; đoạn khớp vươn xa hơn r thì dời khung về . Mỗi lần so thành công khi là một ký tự mới ngoài khung, nên tổng số lần so thêm không quá .
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];
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 bằng xâu ghép
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ỳ đọc từ Z
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 .
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ỳ.
Ví dụViền Của Chuỗi
Đề. Xâu dài tới . In độ dài mọi viền (ngắn hơn cả xâu) theo thứ tự tăng dần, không có thì in 0.
Lời giải. z[i] == n - i nghĩa là hậu tố bắt đầu ở khớp trọn với tiền tố cùng độ dài: nó là một viền dài . Duyệt giảm dần thì độ dài tăng dần, in đúng thứ tự. Bỏ vì đó là cả xâu.
for (ll i = n - 1; i >= 1; i--)
if (z[i] == n - i) {
cout << n - i << ' ';
any = 1;
}
#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);
bool any = 0;
for (ll i = n - 1; i >= 1; i--)
if (z[i] == n - i) {
cout << n - i << ' ';
any = 1;
}
if (!any) cout << 0;
cout << '\n';
}
abcababcab
2 5
.
Ví dụNhịp Lặp Khuyết
Đề. Xâu dài tới . In mọi chu kỳ (bản cuối được phép bị cắt) theo thứ tự tăng dần.
Lời giải. là chu kỳ khi với mọi , tức hậu tố từ trùng tiền tố cùng độ dài: z[p] == n - p. Đây cũng chính là điều kiện có viền dài . luôn là chu kỳ (không có cặp nào để kiểm), in ở cuối.
for (ll p = 1; p < n; p++)
if (z[p] == n - p) cout << p << ' ';
cout << n << '\n';
#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);
for (ll p = 1; p < n; p++)
if (z[p] == n - p) cout << p << ' ';
cout << n << '\n';
}
abababa
2 4 6 7
.
Đếm lần xuất hiện của mọi tiền tố
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 xâu bằng các 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