Mọi xâu con là tiền tố của một hậu tố. Mảng hậu tố sắp mọi hậu tố theo thứ tự từ điển, nên các lần xuất hiện của một mẫu nằm liền nhau; mảng LCP cho độ dài tiền tố chung của hai hậu tố kề nhau. Từ hai mảng này ra đếm xâu con phân biệt, đoạn lặp dài nhất, đoạn chung của nhiều xâu. Ôtômat hậu tố là cấu trúc bổ sung: một đồ thị trạng thái nhận đúng các xâu con, tiện cho bài đếm số lần xuất hiện.
Mảng hậu tố bằng nhân đôi
sa[j] là vị trí bắt đầu của hậu tố đứng thứ . Nhân đôi: giả sử đã có hạng rk[i] của đoạn dài bắt đầu tại (đoạn có thể bị cắt ở cuối xâu). Đoạn dài tại so sánh được bằng cặp , với khi . Sắp các cặp, đánh lại hạng, gấp đôi ; dừng khi mọi hạng khác nhau.
Sắp cặp bằng sắp đếm hai lượt (khóa phụ trước, khóa chính sau) để mỗi vòng , tổng . Khóa phụ có sẵn thứ tự: các vị trí (khóa phụ ) đứng đầu, rồi theo thứ tự của mảng hậu tố vòng trước.
Ví dụThứ Tự Các Hậu Tố
Đề. Xâu chữ thường dài tới . In mảng hậu tố, vị trí đánh số từ 0.
Lời giải. Nhân đôi độ dài so sánh: sau bước , rk[i] là hạng của ký tự đầu của hậu tố (hậu tố ngắn hơn thì so cả hậu tố). Thứ tự theo ký tự đầu là thứ tự theo cặp (rk[i], rk[i + k]), cặp thiếu nửa sau coi nửa sau là nhỏ nhất. Sắp cặp bằng hai lượt: theo khóa phụ trước, rồi sắp đếm ổn định theo khóa chính. Dừng khi mọi hạng đã khác nhau.
Bước 1: sắp theo ký tự đầu. Hạng ban đầu là mã ký tự nên lượt đếm đầu dùng 128 ô.
for (int i = 0; i < n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[i]]] = i;
Bước 2: sắp theo khóa phụ, không cần đếm. Các hậu tố không có nửa sau nên đứng đầu. Các hậu tố còn lại có nửa sau là hậu tố , mà sa đã sắp theo đúng khóa đó; duyệt sa và lấy sa[i] - k là xong.
int p = 0;
for (int i = n - k; i < n; i++) tp[p++] = i;
for (int i = 0; i < n; i++)
if (sa[i] >= k) tp[p++] = sa[i] - k;
Bước 3: sắp đếm ổn định theo khóa chính. Duyệt tp từ cuối: hậu tố cùng khóa chính giữ đúng thứ tự khóa phụ. Giờ sa đã sắp theo ký tự đầu.
for (int i = 0; i < m; i++) cnt[i] = 0;
for (int i = 0; i < n; i++) cnt[rk[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[tp[i]]]] = tp[i];
Bước 4: hạng mới. swap(rk, tp): tp giữ hạng cũ, rk được tính lại. Hai hậu tố kề nhau trong sa cùng hạng mới khi cả khóa chính lẫn khóa phụ bằng nhau; khóa phụ thiếu ghi . p là số hạng khác nhau: p == n thì dừng, ngược lại m = p cho lượt đếm sau.
swap(rk, tp);
rk[sa[0]] = 0, p = 1;
for (int i = 1; i < n; i++) {
int a = sa[i], b = sa[i - 1];
int x = a + k < n ? tp[a + k] : -1, y = b + k < n ? tp[b + k] : -1;
rk[a] = tp[a] == tp[b] && x == y ? p - 1 : p++;
}
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e5 + 5;
int n, sa[N], rk[N], tp[N], cnt[N];
string s;
void buildSA() {
int m = 128;
for (int i = 0; i < n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[i]]] = i;
for (int k = 1;; k <<= 1) {
int p = 0;
for (int i = n - k; i < n; i++) tp[p++] = i;
for (int i = 0; i < n; i++)
if (sa[i] >= k) tp[p++] = sa[i] - k;
for (int i = 0; i < m; i++) cnt[i] = 0;
for (int i = 0; i < n; i++) cnt[rk[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[tp[i]]]] = tp[i];
swap(rk, tp);
rk[sa[0]] = 0, p = 1;
for (int i = 1; i < n; i++) {
int a = sa[i], b = sa[i - 1];
int x = a + k < n ? tp[a + k] : -1, y = b + k < n ? tp[b + k] : -1;
rk[a] = tp[a] == tp[b] && x == y ? p - 1 : p++;
}
if (p == n) break;
m = p;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
n = s.size();
buildSA();
for (int i = 0; i < n; i++) cout << sa[i] << ' ';
cout << '\n';
}
cacao
1 3 0 2 4
Mỗi bước , có bước.
Mảng LCP và thuật toán Kasai
lcp[j] là độ dài tiền tố chung của hai hậu tố sa[j-1] và sa[j]. Kasai duyệt vị trí theo thứ tự trong xâu: nếu hậu tố có LCP bằng với hậu tố đứng ngay trước nó, thì hậu tố có LCP ít nhất với hậu tố đứng ngay trước nó (bỏ ký tự đầu của cả hai). Biến chỉ giảm tối đa lần nên tổng .
Hai hệ quả dùng nhiều nhất:
- LCP của hai hậu tố bất kỳ là cực tiểu của
lcptrên đoạn giữa chúng trong mảng hậu tố. - Hậu tố
sa[j]đóng góp xâu con mới (các tiền tố dài hơn ), nên số xâu con phân biệt là .
Hỏi nhanh
Xâu aaaa có bao nhiêu xâu con phân biệt?
Mảng LCP của các hậu tố a, aa, aaa, aaaa là , tổng 6. Đáp án : các xâu a, aa, aaa, aaaa.
Ví dụĐếm Xâu Con Khác Nhau
Đề. Xâu chữ thường dài tới . Đếm số xâu con liên tiếp phân biệt.
Lời giải. Mỗi xâu con là tiền tố của một hậu tố. Duyệt các hậu tố theo thứ tự mảng hậu tố: hậu tố sa[j] có tiền tố, trong đó tiền tố đầu đã xuất hiện ở hậu tố đứng trước. Các tiền tố dài hơn là mới, vì xâu con nào lặp lại cũng phải là tiền tố chung với hậu tố kề trước theo thứ tự. Tổng là . buildSA giống hệt ví dụ trước.
Bước 1: mảng LCP bằng Kasai. rk dựng lại thành nghịch đảo của sa. Duyệt hậu tố theo vị trí trong xâu, so với hậu tố đứng ngay trước nó trong sa. h không cần đếm lại từ 0: bỏ ký tự đầu của hai hậu tố cặp trước cho hai hậu tố có tiền tố chung ít nhất , và hậu tố đứng ngay trước trong sa chỉ có thể chung nhiều hơn. h giảm tối đa lần nên vòng while chạy tổng . Hậu tố đứng đầu sa không có hậu tố trước, đặt h = 0.
void buildLCP() {
for (int i = 0; i < n; i++) rk[sa[i]] = i;
for (int i = 0, h = 0; i < n; i++) {
if (rk[i] == 0) {
h = 0;
continue;
}
if (h) h--;
int j = sa[rk[i] - 1];
while (i + h < n && j + h < n && s[i + h] == s[j + h]) h++;
lcp[rk[i]] = h;
}
}
Bước 2: đáp án. Tới nên dùng long long.
ll ans = (ll)n * (n + 1) / 2;
for (int i = 1; i < n; i++) ans -= lcp[i];
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
int n, sa[N], rk[N], tp[N], cnt[N], lcp[N];
string s;
void buildSA() {
int m = 128;
for (int i = 0; i < n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[i]]] = i;
for (int k = 1;; k <<= 1) {
int p = 0;
for (int i = n - k; i < n; i++) tp[p++] = i;
for (int i = 0; i < n; i++)
if (sa[i] >= k) tp[p++] = sa[i] - k;
for (int i = 0; i < m; i++) cnt[i] = 0;
for (int i = 0; i < n; i++) cnt[rk[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[tp[i]]]] = tp[i];
swap(rk, tp);
rk[sa[0]] = 0, p = 1;
for (int i = 1; i < n; i++) {
int a = sa[i], b = sa[i - 1];
int x = a + k < n ? tp[a + k] : -1, y = b + k < n ? tp[b + k] : -1;
rk[a] = tp[a] == tp[b] && x == y ? p - 1 : p++;
}
if (p == n) break;
m = p;
}
}
void buildLCP() {
for (int i = 0; i < n; i++) rk[sa[i]] = i;
for (int i = 0, h = 0; i < n; i++) {
if (rk[i] == 0) {
h = 0;
continue;
}
if (h) h--;
int j = sa[rk[i] - 1];
while (i + h < n && j + h < n && s[i + h] == s[j + h]) h++;
lcp[rk[i]] = h;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
n = s.size();
buildSA();
buildLCP();
ll ans = (ll)n * (n + 1) / 2;
for (int i = 1; i < n; i++) ans -= lcp[i];
cout << ans << '\n';
}
ababa
9
cho mảng hậu tố, cho LCP.
Ví dụĐoạn Lặp Dài Nhất
Đề. Xâu dài tới . In một xâu con dài nhất xuất hiện ở ít nhất hai vị trí (được chồng lấn), hoặc .
Lời giải. Xâu con xuất hiện ít nhất hai lần là tiền tố chung của hai hậu tố khác nhau. LCP của hai hậu tố bất kỳ là cực tiểu lcp trên đoạn giữa chúng trong mảng hậu tố, nên không vượt lcp của một cặp kề nào đó. Vậy độ dài lớn nhất là lcp lớn nhất, và xâu cần in là tiền tố dài chừng ấy của hậu tố ở vị trí đó. buildSA, buildLCP như các ví dụ trước.
best là vị trí có lcp lớn nhất; lcp[0] = 0 nên khởi đầu best = 0 ổn. Không có lcp dương nghĩa là mọi ký tự khác nhau, in .
int best = 0;
for (int i = 1; i < n; i++)
if (lcp[i] > lcp[best]) best = i;
if (lcp[best] == 0) cout << -1 << '\n';
else cout << s.substr(sa[best], lcp[best]) << '\n';
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
int n, sa[N], rk[N], tp[N], cnt[N], lcp[N];
string s;
void buildSA() {
int m = 128;
for (int i = 0; i < n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[i]]] = i;
for (int k = 1;; k <<= 1) {
int p = 0;
for (int i = n - k; i < n; i++) tp[p++] = i;
for (int i = 0; i < n; i++)
if (sa[i] >= k) tp[p++] = sa[i] - k;
for (int i = 0; i < m; i++) cnt[i] = 0;
for (int i = 0; i < n; i++) cnt[rk[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[tp[i]]]] = tp[i];
swap(rk, tp);
rk[sa[0]] = 0, p = 1;
for (int i = 1; i < n; i++) {
int a = sa[i], b = sa[i - 1];
int x = a + k < n ? tp[a + k] : -1, y = b + k < n ? tp[b + k] : -1;
rk[a] = tp[a] == tp[b] && x == y ? p - 1 : p++;
}
if (p == n) break;
m = p;
}
}
void buildLCP() {
for (int i = 0; i < n; i++) rk[sa[i]] = i;
for (int i = 0, h = 0; i < n; i++) {
if (rk[i] == 0) {
h = 0;
continue;
}
if (h) h--;
int j = sa[rk[i] - 1];
while (i + h < n && j + h < n && s[i + h] == s[j + h]) h++;
lcp[rk[i]] = h;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
n = s.size();
buildSA();
buildLCP();
int best = 0;
for (int i = 1; i < n; i++)
if (lcp[i] > lcp[best]) best = i;
if (lcp[best] == 0) cout << -1 << '\n';
else cout << s.substr(sa[best], lcp[best]) << '\n';
}
banana
ana
.
Tìm mẫu trên mảng hậu tố
Các hậu tố có tiền tố tạo thành một đoạn liên tiếp trong mảng hậu tố. Đầu đoạn là hậu tố nhỏ nhất không nhỏ hơn (so ký tự đầu), cuối đoạn là hậu tố cuối cùng có ký tự đầu bằng . Hai lần chặt nhị phân, mỗi lần so sánh tốn : một mẫu tốn , không phụ thuộc số lần xuất hiện.
s.compare(pos, len, P) so đoạn với ; khi hậu tố ngắn hơn , đoạn bị cắt và vẫn so đúng thứ tự từ điển.
Ví dụĐếm Vị Trí Của Mẫu
Đề. Văn bản dài tới , tới mẫu, tổng độ dài mẫu tới . Đếm số lần xuất hiện (được chồng lấn) của từng mẫu.
Lời giải. Các hậu tố có tiền tố nằm liền một đoạn trong mảng hậu tố. Đầu đoạn là hậu tố đầu tiên có ký tự đầu không nhỏ hơn ; cuối đoạn cộng 1 là hậu tố đầu tiên có ký tự đầu lớn hơn . Hai lần chặt nhị phân, hiệu hai kết quả là số lần xuất hiện. Chặt được vì theo thứ tự mảng hậu tố, kết quả so với chỉ đổi từ “nhỏ hơn” sang “bằng” sang “lớn hơn” một lần. buildSA như ví dụ đầu.
Bước 1: một lần chặt. s.compare(sa[mid], p.size(), p) so đoạn bắt đầu ở sa[mid] dài tối đa với ; hậu tố ngắn hơn bị cắt ngắn và vẫn so đúng thứ tự từ điển. strict = 1 tìm vị trí đầu tiên lớn hơn , strict = 0 tìm vị trí đầu tiên không nhỏ hơn ; không có thì trả .
int bound(string &p, int strict) {
int l = 0, r = n - 1, res = n;
while (l <= r) {
int mid = (l + r) / 2;
int c = s.compare(sa[mid], p.size(), p);
if (c > 0 || (c == 0 && !strict)) res = mid, r = mid - 1;
else l = mid + 1;
}
return res;
}
Bước 2: đếm. bound(p, 1) - bound(p, 0).
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
int n, sa[N], rk[N], tp[N], cnt[N];
string s;
void buildSA() {
int m = 128;
for (int i = 0; i < n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[i]]] = i;
for (int k = 1;; k <<= 1) {
int p = 0;
for (int i = n - k; i < n; i++) tp[p++] = i;
for (int i = 0; i < n; i++)
if (sa[i] >= k) tp[p++] = sa[i] - k;
for (int i = 0; i < m; i++) cnt[i] = 0;
for (int i = 0; i < n; i++) cnt[rk[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[tp[i]]]] = tp[i];
swap(rk, tp);
rk[sa[0]] = 0, p = 1;
for (int i = 1; i < n; i++) {
int a = sa[i], b = sa[i - 1];
int x = a + k < n ? tp[a + k] : -1, y = b + k < n ? tp[b + k] : -1;
rk[a] = tp[a] == tp[b] && x == y ? p - 1 : p++;
}
if (p == n) break;
m = p;
}
}
int bound(string &p, int strict) {
int l = 0, r = n - 1, res = n;
while (l <= r) {
int mid = (l + r) / 2;
int c = s.compare(sa[mid], p.size(), p);
if (c > 0 || (c == 0 && !strict)) res = mid, r = mid - 1;
else l = mid + 1;
}
return res;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
n = s.size();
buildSA();
int k;
cin >> k;
while (k--) {
string p;
cin >> p;
cout << bound(p, 1) - bound(p, 0) << '\n';
}
}
aaaaa
4
a
aa
aaa
b
5
4
3
0
Mỗi mẫu .
Ghép nhiều xâu
Đoạn chung của và là tiền tố chung của một hậu tố của và một hậu tố của . Ghép với ký tự phân cách không có trong cả hai xâu: LCP không thể vượt qua #, nên mọi tiền tố chung giữa hậu tố của và của trong đúng là đoạn chung. Đáp án là lcp[j] lớn nhất trên các cặp kề có một hậu tố bắt đầu trong và một trong ; cặp tốt nhất luôn kề nhau ở một chỗ nào đó vì LCP giữa hai hậu tố là cực tiểu đoạn giữa.
Nhiều xâu thì mỗi xâu một ký tự phân cách khác nhau, rồi chặt nhị phân độ dài hay dùng cửa sổ trượt trên mảng hậu tố sao cho đoạn chứa đủ các xâu cần có.
Ví dụĐộ Dài Đoạn Chung Dài Nhất
Đề. Hai xâu chữ thường, mỗi xâu dài tới . Tìm độ dài đoạn chung dài nhất.
Lời giải. Ghép , với ` là ký tự đứng ngay trước a trong bảng mã và không có trong hai xâu. Tiền tố chung của hai hậu tố không thể vượt qua `, vì ký tự này chỉ xuất hiện một lần. Đoạn chung dài nhất là LCP lớn nhất giữa một hậu tố bắt đầu trong và một hậu tố bắt đầu trong . buildSA, buildLCP như các ví dụ trước, trên xâu dài .
Bước 1: chỉ xét cặp kề khác phía. Lấy cặp tốt nhất ở hai phía. Trên đoạn mảng hậu tố giữa chúng có một chỗ đổi phía, và lcp ở đó không nhỏ hơn cực tiểu của cả đoạn, tức LCP của . Hậu tố bắt đầu ngay ở ` được tính vào phía , nhưng LCP của nó với mọi hậu tố khác là 0 nên không ảnh hưởng.
for (int i = 1; i < n; i++)
if ((sa[i] < la) != (sa[i - 1] < la)) ans = max(ans, lcp[i]);
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e5 + 5;
int n, sa[N], rk[N], tp[N], cnt[N], lcp[N];
string s;
void buildSA() {
int m = 128;
for (int i = 0; i < n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[i]]] = i;
for (int k = 1;; k <<= 1) {
int p = 0;
for (int i = n - k; i < n; i++) tp[p++] = i;
for (int i = 0; i < n; i++)
if (sa[i] >= k) tp[p++] = sa[i] - k;
for (int i = 0; i < m; i++) cnt[i] = 0;
for (int i = 0; i < n; i++) cnt[rk[i]]++;
for (int i = 1; i < m; i++) cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--cnt[rk[tp[i]]]] = tp[i];
swap(rk, tp);
rk[sa[0]] = 0, p = 1;
for (int i = 1; i < n; i++) {
int a = sa[i], b = sa[i - 1];
int x = a + k < n ? tp[a + k] : -1, y = b + k < n ? tp[b + k] : -1;
rk[a] = tp[a] == tp[b] && x == y ? p - 1 : p++;
}
if (p == n) break;
m = p;
}
}
void buildLCP() {
for (int i = 0; i < n; i++) rk[sa[i]] = i;
for (int i = 0, h = 0; i < n; i++) {
if (rk[i] == 0) {
h = 0;
continue;
}
if (h) h--;
int j = sa[rk[i] - 1];
while (i + h < n && j + h < n && s[i + h] == s[j + h]) h++;
lcp[rk[i]] = h;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
string a, b;
cin >> a >> b;
s = a + '`' + b;
n = s.size();
int la = a.size();
buildSA();
buildLCP();
int ans = 0;
for (int i = 1; i < n; i++)
if ((sa[i] < la) != (sa[i - 1] < la)) ans = max(ans, lcp[i]);
cout << ans << '\n';
}
abacaba
zzacabx
4
với .
Ôtômat hậu tố
Ôtômat hậu tố của là DAG nhỏ nhất nhận đúng các xâu con của : mỗi trạng thái là một lớp xâu con có cùng tập vị trí kết thúc (endpos). Trạng thái lưu len (độ dài dài nhất trong lớp), liên kết hậu tố lnk (trạng thái của hậu tố dài nhất có endpos khác) và các cung theo ký tự. Thêm từng ký tự vào cuối với thủ tục chuẩn có nhân bản trạng thái; tổng số trạng thái không quá .
Số lần xuất hiện của mọi xâu trong lớp là : đánh dấu 1 ở trạng thái vừa tạo khi thêm mỗi ký tự (không đánh dấu bản sao), rồi cộng dồn theo liên kết hậu tố từ trạng thái có len lớn tới nhỏ. Lớp chứa các xâu có độ dài trong .
Hỏi nhanh
Trong ôtômat hậu tố của ababa, xâu aba xuất hiện bao nhiêu lần, tức endpos của trạng thái chứa nó có mấy phần tử?
aba kết thúc tại vị trí 3 và 5 (đánh số từ 1), hai lần xuất hiện chồng lấn nhau. Trạng thái chứa nó có endpos gồm 2 phần tử.
Ví dụTần Suất Lớn Nhất Theo Độ Dài
Đề. Xâu chữ thường dài . Với mỗi , in số lần xuất hiện lớn nhất của một xâu con dài .
Lời giải. Dựng ôtômat hậu tố. Mọi xâu trong lớp xuất hiện đúng lần, và xâu dài nhất của lớp có độ dài , nên trạng thái cho giá trị ở độ dài . Xâu dài xuất hiện lần thì mọi xâu con ngắn hơn của nó xuất hiện ít nhất lần. Vậy ghi ans[len] cho từng trạng thái rồi lấy cực đại hậu tố từ về 1.
Bước 1: thêm một ký tự. Trạng thái mới cur có occ = 1 vì nó ứng với một vị trí kết thúc mới. Đi theo liên kết hậu tố từ last, thêm cung c cho các trạng thái chưa có. Gặp trạng thái p đã có cung tới q: nếu len[q] == len[p] + 1 thì q là liên kết của cur; ngược lại tách q thành bản sao cl có độ dài đúng len[p] + 1. Bản sao có occ = 0, vì nó không ứng với vị trí kết thúc mới nào.
void extend(int c) {
int cur = sz++, p = last;
len[cur] = len[last] + 1, occ[cur] = 1;
while (p != -1 && !nxt[p][c]) nxt[p][c] = cur, p = lnk[p];
if (p == -1) lnk[cur] = 0;
else {
int q = nxt[p][c];
if (len[p] + 1 == len[q]) lnk[cur] = q;
else {
int cl = sz++;
len[cl] = len[p] + 1, lnk[cl] = lnk[q];
memcpy(nxt[cl], nxt[q], sizeof(nxt[q]));
while (p != -1 && nxt[p][c] == q) nxt[p][c] = cl, p = lnk[p];
lnk[q] = lnk[cur] = cl;
}
}
last = cur;
}
Bước 2: dồn occ theo liên kết. Sắp đếm các trạng thái theo len vào ord. Liên kết hậu tố luôn trỏ tới trạng thái có len nhỏ hơn, nên duyệt ord từ cuối thì mọi trạng thái đã nhận đủ occ trước khi cộng tiếp cho liên kết của mình. Cùng vòng đó ghi ans[len[v]]; gốc (độ dài 0) bị bỏ qua.
for (int i = sz - 1; i > 0; i--) {
int v = ord[i];
occ[lnk[v]] += occ[v];
ans[len[v]] = max(ans[len[v]], occ[v]);
}
for (int L = n - 1; L >= 1; L--) ans[L] = max(ans[L], ans[L + 1]);
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e5 + 5;
int sz, last, len[N], lnk[N], nxt[N][26], occ[N], cntLen[N], ord[N], ans[N];
string s;
void extend(int c) {
int cur = sz++, p = last;
len[cur] = len[last] + 1, occ[cur] = 1;
while (p != -1 && !nxt[p][c]) nxt[p][c] = cur, p = lnk[p];
if (p == -1) lnk[cur] = 0;
else {
int q = nxt[p][c];
if (len[p] + 1 == len[q]) lnk[cur] = q;
else {
int cl = sz++;
len[cl] = len[p] + 1, lnk[cl] = lnk[q];
memcpy(nxt[cl], nxt[q], sizeof(nxt[q]));
while (p != -1 && nxt[p][c] == q) nxt[p][c] = cl, p = lnk[p];
lnk[q] = lnk[cur] = cl;
}
}
last = cur;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
int n = s.size();
sz = 1, last = 0, lnk[0] = -1;
for (char ch : s) extend(ch - 'a');
for (int v = 0; v < sz; v++) cntLen[len[v]]++;
for (int i = 1; i <= n; i++) cntLen[i] += cntLen[i - 1];
for (int v = sz - 1; v >= 0; v--) ord[--cntLen[len[v]]] = v;
for (int i = sz - 1; i > 0; i--) {
int v = ord[i];
occ[lnk[v]] += occ[v];
ans[len[v]] = max(ans[len[v]], occ[v]);
}
for (int L = n - 1; L >= 1; L--) ans[L] = max(ans[L], ans[L + 1]);
for (int L = 1; L <= n; L++) cout << ans[L] << '\n';
}
ababa
3
2
2
1
1
cho ôtômat, cho phần còn lại.
Khi đi thi
- Đếm xâu con phân biệt, xâu con thứ , đoạn lặp, đoạn chung: mảng hậu tố + LCP, . Nhớ LCP của hai hậu tố bất kỳ là cực tiểu đoạn LCP giữa chúng (dựng bảng thưa nếu cần hỏi nhiều).
- Nhiều xâu: ghép với ký tự phân cách riêng, đánh dấu mỗi hậu tố thuộc xâu nào; điều kiện “xuất hiện trong đủ xâu” thành cửa sổ trượt trên mảng hậu tố.
- Đếm số lần xuất hiện theo lớp, đoạn chung với một xâu dài: ôtômat hậu tố, trạng thái, cộng endpos theo
lengiảm dần.
Tóm tắt
- Nhân đôi: hạng đoạn là thứ tự cặp hạng hai nửa; sắp đếm cho .
- Kasai: LCP kề nhau trong , vì LCP chỉ giảm tối đa 1 khi sang vị trí kế tiếp.
- Số xâu con phân biệt ; mẫu chiếm một đoạn liên tiếp của mảng hậu tố.
- Ôtômat hậu tố gom xâu con theo endpos; số lần xuất hiện là số vị trí kết thúc.
Bài tập
sa.Đọc thêm
- Mảng hậu tốVNOI WikiNhân đôi, LCP và ứng dụng bằng tiếng Việt.
- Suffix Arraycp-algorithmsDựng bằng sắp đếm và các ứng dụng LCP.
- Suffix AutomatonVNOI WikiÔtômat hậu tố bằng tiếng Việt.
- Suffix Automatoncp-algorithmsChứng minh số trạng thái, endpos và ứng dụng.
- Suffix ArrayUSACO GuideBài luyện có lời giải.