Trong xâu banana, đoạn ana xuất hiện mấy lần? Viết ra mọi hậu tố của xâu, rồi sắp theo thứ tự từ điển: các hậu tố bắt đầu bằng ana đứng ngay cạnh nhau. Đó là mấu chốt, vì mọi xâu con đều là tiền tố của một hậu tố nào đó.
Mảng hậu tố sắp mọi hậu tố theo thứ tự từ điển; 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, đ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ố
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.
Trong hình, mỗi hàng là một hậu tố theo thứ tự hiện tại; xanh dương là nửa đầu dài , cam là nửa sau. Bước cuối cho mảng LCP.
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;
#define 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. Chỗ “sắp đếm” tưởng nhỏ, nhưng thay nó bằng sort thì thêm một hệ số và hằng số cũng nặng hơn:
Sắp đếm nhanh hơn gọi sort mỗi vòng khoảng 6 lần. Xâu càng ít loại ký tự, càng nhiều vòng nhân đôi, khoảng cách càng lớn. Quy tắc: dựng mảng hậu tố cho từ vài trăm nghìn thì dùng sắp đếm.
Mảng LCP
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;
#define 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.
Tìm mẫu
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 . Hậu tố ngắn hơn thì đ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;
#define 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;
#define 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;
#define 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ố cộng LCP, . LCP của hai hậu tố bất kỳ là cực tiểu LCP đoạn giữa chúng (dựng bảng thưa nếu hỏi nhiều).
- Nhiều xâu: ghép bằng ký tự phân cách riêng, đánh dấu mỗi hậu tố thuộc xâu nào; “xuất hiện trong đủ xâu” thành cửa sổ trượt trên mảng hậu tố.
- Số lần xuất hiện theo lớp, đoạn chung với 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
Đọc thêm
- Mảng hậu tốVNOI Wiki
- Suffix Arraycp-algorithms
- Suffix AutomatonVNOI Wiki
- Suffix Automatoncp-algorithms
- Suffix ArrayUSACO Guide