Cho một xâu gồm ký tự. Tìm xâu con liên tiếp dài nhất mà đọc từ trái sang phải hay từ phải sang trái đều giống nhau, trong .
Xâu như vậy gọi là xâu đối xứng (palindrome), chẳng hạn aba, abba, racecar; xâu một ký tự cũng là xâu đối xứng. Cách dễ nghĩ ra là lấy từng vị trí làm tâm, rồi mở rộng sang hai bên chừng nào hai ký tự còn bằng nhau; cách này tốn . Hash (bài 1.10.1) cho phép chặt nhị phân độ dài mở rộng ở mỗi tâm, được . Thuật toán Manacher làm nhanh hơn nữa: khi một tâm mới nằm trong một xâu đối xứng đã tìm được, ta dùng lại kết quả của tâm đối xứng với nó, nên tổng cộng chỉ và không cần hash.
IKiểm một xâu con có đối xứng không
Đánh số các ký tự của từ tới và viết cho đoạn từ ký tự thứ tới ký tự thứ . Gọi là xâu đảo của , tức ; với = dabacab thì = bacabad. Đoạn viết ngược chính là , nên đối xứng khi và chỉ khi hai đoạn này bằng nhau. Có hash tiền tố của cả và (bài 1.10.1), việc đó là một phép so sánh hai số, tốn sau chuẩn bị.
Hỏi nhanh
Xâu dài . Để biết đoạn có đối xứng không, ta so hash của nó với hash đoạn nào của xâu đảo ?
Đoạn viết ngược là , với , , cho .
Ký tự nằm ở vị trí của và nằm ở vị trí , nên đoạn cần so nằm ở chứ không phải cùng chỗ với đoạn của .
Ví dụCửa Sổ Đối Xứng
Đề. Cho số nguyên và xâu gồm chữ thường, . Đếm số cửa sổ gồm đúng ký tự liên tiếp của là xâu đối xứng; hai cửa sổ ở hai vị trí khác nhau được đếm riêng dù nội dung giống nhau.
Lời giải. Có cửa sổ. Kiểm từng cửa sổ bằng cách so lần lượt các cặp ký tự đối nhau tốn , cả bài . Với hash của và của , mỗi cửa sổ chỉ còn một phép so sánh. Ta dùng modulo với cơ số ngẫu nhiên và hàm mul như ở bài 1.10.1. Hash của dựng bằng cách đọc từ cuối về đầu.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
using ull = unsigned long long;
const ull MOD = (1ULL << 61) - 1;
const ll N = 3e4 + 5;
mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
ull B = rng() % (MOD - 1000) + 500, h[N], g[N], pw[N];
ll n, k;
string s;
ull mul(ull a, ull b) {
__int128 t = (__int128)a * b;
ull r = (ull)(t & MOD) + (ull)(t >> 61);
return r >= MOD ? r - MOD : r;
}
ull get(ull f[], ll l, ll r) {
return (f[r] + MOD - mul(f[l - 1], pw[r - l + 1])) % MOD;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> k >> s;
n = s.size();
pw[0] = 1;
for (ll i = 1; i <= n; i++) {
pw[i] = mul(pw[i - 1], B);
h[i] = (mul(h[i - 1], B) + s[i - 1]) % MOD;
g[i] = (mul(g[i - 1], B) + s[n - i]) % MOD;
}
ll ans = 0;
for (ll l = 1; l + k - 1 <= n; l++) {
ll r = l + k - 1;
if (get(h, l, r) == get(g, n + 1 - r, n + 1 - l)) ans++;
}
cout << ans << '\n';
}
3
abacaba
3
Độ phức tạp: , với cần khoảng phép tính.
IIBán kính đối xứng tại mỗi tâm
Một xâu đối xứng độ dài lẻ có một ký tự ở giữa, một xâu độ dài chẵn có hai ký tự giữa bằng nhau. Bỏ ký tự đầu và ký tự cuối của một xâu đối xứng thì phần còn lại vẫn đối xứng, nên các xâu đối xứng cùng tâm lồng vào nhau như những vòng tròn đồng tâm. Với mỗi vị trí , ta đặt hai đại lượng:
- là số xâu đối xứng có độ dài lẻ với ký tự giữa là . Xâu dài nhất trong số đó có độ dài , và vì một ký tự luôn đối xứng.
- là số xâu đối xứng có độ dài chẵn với hai ký tự giữa là và . Xâu dài nhất có độ dài , và có thể bằng .
Ta gọi chung , là bán kính của tâm tương ứng. Với = abaaba thì và .
Hỏi nhanh
Xâu abaaba có hai ký tự giữa là ký tự thứ 3 và thứ 4, cùng bằng a. Có bao nhiêu xâu đối xứng có độ dài chẵn với tâm ở giữa hai ký tự đó?
Các xâu đối xứng cùng tâm lồng nhau: aa, rồi baab, rồi abaaba. Số xâu chính là bán kính , còn độ dài xâu lớn nhất là . Đáp án 6 nhầm số xâu với độ dài.
Cách dễ nghĩ ra nhất là mở rộng từng ký tự một cho mọi tâm:
for (ll i = 1; i <= n; i++) {
d1[i] = 1;
while (i - d1[i] >= 1 && i + d1[i] <= n && s[i - d1[i]] == s[i + d1[i]]) d1[i]++;
while (i - d2[i] - 1 >= 1 && i + d2[i] <= n && s[i - d2[i] - 1] == s[i + d2[i]]) d2[i]++;
}
Độ phức tạp: khi xâu gồm toàn một chữ cái, với cần khoảng phép tính.
Ta có thể thấy điểm lãng phí của cách làm này: bán kính mà đúng thì bán kính cũng đúng, nên với một tâm cố định, ta chặt nhị phân được bán kính lớn nhất thay vì thử từng giá trị. Mỗi lần thử là kiểm một đoạn có đối xứng không, đã làm được trong bằng hash ở mục trước.
Ví dụĐối Xứng Dài Nhất
Đề. Cho xâu dài , gồm các ký tự ASCII in được khác khoảng trắng. In độ dài lớn nhất của một xâu con liên tiếp đối xứng.
Lời giải. Với mỗi tâm lẻ , bán kính nằm trong và đoạn cần kiểm là . Với mỗi tâm chẵn , bán kính nằm trong và đoạn cần kiểm là . Hàm pal(l, r) so hash của với hash đoạn tương ứng trên xâu đảo. Đáp án là giá trị lớn nhất của và trên mọi .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
using ull = unsigned long long;
const ull MOD = (1ULL << 61) - 1;
const ll N = 5e4 + 5;
mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
ull B = rng() % (MOD - 1000) + 500, h[N], g[N], pw[N];
ll n;
string s;
ull mul(ull a, ull b) {
__int128 t = (__int128)a * b;
ull r = (ull)(t & MOD) + (ull)(t >> 61);
return r >= MOD ? r - MOD : r;
}
ull get(ull f[], ll l, ll r) {
return (f[r] + MOD - mul(f[l - 1], pw[r - l + 1])) % MOD;
}
bool pal(ll l, ll r) {
return get(h, l, r) == get(g, n + 1 - r, n + 1 - l);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> s;
pw[0] = 1;
for (ll i = 1; i <= n; i++) {
pw[i] = mul(pw[i - 1], B);
h[i] = (mul(h[i - 1], B) + s[i - 1]) % MOD;
g[i] = (mul(g[i - 1], B) + s[n - i]) % MOD;
}
ll ans = 1;
for (ll i = 1; i <= n; i++) {
ll l = 1, r = min(i, n - i + 1), d1 = 1;
while (l <= r) {
ll mid = (l + r) >> 1;
if (pal(i - mid + 1, i + mid - 1)) l = (d1 = mid) + 1;
else r = mid - 1;
}
l = 0, r = min(i - 1, n - i + 1);
ll d2 = 0;
while (l <= r) {
ll mid = (l + r) >> 1;
if (pal(i - mid, i + mid - 1)) l = (d2 = mid) + 1;
else r = mid - 1;
}
ans = max(ans, max(2 * d1 - 1, 2 * d2));
}
cout << ans << '\n';
}
6
cabbad
4
Độ phức tạp: , với cần khoảng lần so hash.
Cách này còn lần so hash ở mỗi tâm, và vì dùng hash nên có xác suất sai rất nhỏ. Mục sau bỏ được cả hai.
IIIThuật toán Manacher
Gộp tâm lẻ và tâm chẵn. Để khỏi viết mọi thứ hai lần, ta dựng xâu từ bằng cách đặt ký tự # vào trước ký tự đầu, giữa mọi cặp ký tự kề nhau và sau ký tự cuối; abba thành #a#b#b#a#, dài . Trong mọi tâm đều là một ký tự: tâm lẻ của là ký tự thật, tâm chẵn của là dấu # nằm giữa hai ký tự. Khi mở rộng, hai ký tự đem so luôn cùng chẵn hoặc cùng lẻ vị trí, nên dấu # không bao giờ bị so với ký tự thật, kể cả khi cũng có ký tự #.
Gọi là bán kính lớn nhất của xâu đối xứng trong có tâm , tức số lớn nhất để đối xứng. Xâu lớn nhất đó luôn bắt đầu và kết thúc bằng #: nếu hai đầu là chữ thì hai ký tự kế bên là #, bằng nhau, nên còn mở rộng được. Vậy nó gồm dấu # và ký tự thật, và chính là độ dài xâu đối xứng dài nhất của có tâm đó. Với = #a#b#a#b# ta có .
Hỏi nhanh
Với = abab, xâu là #a#b#a#b#. Giá trị lớn nhất là bao nhiêu, và nó cho biết điều gì?
Bán kính bằng độ dài xâu đối xứng tương ứng trong . Tâm ở b đầu tiên cho #a#b#a#, bán kính 3 và xâu aba, tâm ở a thứ hai cho bab. Xâu abab không đối xứng.
Dùng lại bán kính của điểm đối xứng. Ta tính lần lượt từ tới , và giữ xâu đối xứng tìm được cho tới lúc này có đầu phải xa nhất. Khi tâm nằm trong xâu đó (), điểm đối xứng với qua tâm của . Phần đọc xuôi hay ngược như nhau, nên mọi thứ quanh , trong phạm vi còn nằm trong , giống hệt quanh .
Do đó ít nhất bằng , với là khoảng cách từ tới đầu phải . Ta lấy giá trị này làm điểm xuất phát rồi mở rộng thêm từng ký tự như cách làm đơn giản. Nếu thì phép mở rộng dừng ngay ở bước đầu; nếu không, ký tự đã được kiểm và ta chỉ cần xét các ký tự ngoài .
Chứng minh
Chặn dưới. Với , tâm của không sau (nó là một tâm đã xét trước đó), nên . Lấy . Đoạn nằm trong , và phép đối xứng biến nó thành đọc ngược. Đoạn này đối xứng vì , nên cũng đối xứng. Vậy .
Khi thì . Hai ký tự và khác nhau và cùng nằm trong . Ảnh của chúng qua phép đối xứng là và , nên hai ký tự này cũng khác nhau và không thể lớn hơn .
Độ phức tạp. Mỗi lần vòng mở rộng thành công tăng thêm 1, vị trí vượt qua hiện tại. Nếu vòng không chạy; nếu không, xuất phát của là hoặc (khi ), nên mỗi lần tăng đẩy vượt một đơn vị, và sau khi xong ta đặt . Vậy tăng ít nhất bằng số lần mở rộng thành công, mà . Tổng số lần mở rộng không quá , cả thuật toán .
ll l = 1, r = 0;
for (ll i = 1; i <= m; i++) {
ll k = 0;
if (i <= r) k = min(p[l + r - i], r - i);
while (i - k - 1 >= 1 && i + k + 1 <= m && u[i - k - 1] == u[i + k + 1]) k++;
p[i] = k;
if (i + k > r) l = i - k, r = i + k;
}
Độ phức tạp: , với cần khoảng phép tính.
Chương trình đầy đủ cho bài toán mở đầu in độ dài và một xâu đối xứng dài nhất. Nếu lớn nhất tại tâm thì xâu đó là , bắt đầu ở ký tự thứ của .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e6 + 5;
ll n, m, p[N];
char u[N];
string s;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
n = s.size();
u[++m] = '#';
for (ll i = 0; i < n; i++) {
u[++m] = s[i];
u[++m] = '#';
}
ll l = 1, r = 0, best = 1;
for (ll i = 1; i <= m; i++) {
ll k = 0;
if (i <= r) k = min(p[l + r - i], r - i);
while (i - k - 1 >= 1 && i + k + 1 <= m && u[i - k - 1] == u[i + k + 1]) k++;
p[i] = k;
if (i + k > r) l = i - k, r = i + k;
if (p[i] > p[best]) best = i;
}
ll st = (best - p[best] + 1) / 2;
cout << p[best] << '\n' << s.substr(st - 1, p[best]) << '\n';
}
cabbad
4
abba
Độ phức tạp: , với cần khoảng phép tính.
IVXâu đối xứng ở cuối xâu
Một xâu con (hậu tố) đối xứng khi nào? Trong , nó là đoạn kết thúc ở dấu # cuối cùng, và tâm của nó thỏa : đoạn đối xứng tâm chạm tới tận đầu phải của . Ngược lại, tâm nào có cho một hậu tố đối xứng dài . Hậu tố đối xứng dài nhất ứng với tâm nhỏ nhất như vậy. Tương tự cho tiền tố, với điều kiện .
Ví dụNối Đuôi Thành Đối Xứng
Đề. Mỗi dòng input là một xâu gồm chữ cái hoa và thường, dài tới . Với mỗi xâu, chỉ được nối thêm ký tự vào cuối để được xâu đối xứng ngắn nhất, giữ nguyên xâu ban đầu ở đầu kết quả. In xâu đó.
Lời giải. Gọi là độ dài hậu tố đối xứng dài nhất của . Khi nối đảo của ký tự đầu vào cuối, phần hậu tố đối xứng ở giữa vẫn đối xứng, phần đầu và phần nối thành hai nửa đối nhau. Số ký tự phải nối là , nhỏ nhất khi lớn nhất. Ta chạy Manacher, tìm tâm nhỏ nhất có và đặt . Với Abc, hậu tố đối xứng dài nhất là c, ta nối bA để được AbcbA.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll m, p[N];
char u[N];
string s;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
while (cin >> s) {
ll n = s.size();
m = 0;
u[++m] = '#';
for (ll i = 0; i < n; i++) {
u[++m] = s[i];
u[++m] = '#';
}
ll l = 1, r = 0;
for (ll i = 1; i <= m; i++) {
ll k = 0;
if (i <= r) k = min(p[l + r - i], r - i);
while (i - k - 1 >= 1 && i + k + 1 <= m && u[i - k - 1] == u[i + k + 1]) k++;
p[i] = k;
if (i + k > r) l = i - k, r = i + k;
}
ll c = 1;
while (c + p[c] != m) c++;
cout << s;
for (ll j = n - p[c] - 1; j >= 0; j--) cout << s[j];
cout << '\n';
}
}
Abc
racecar
AbcbA
racecar
Độ phức tạp: cho mỗi xâu, với cần khoảng phép tính.
VĐếm xâu con đối xứng
Mỗi xâu con đối xứng có đúng một tâm, và các xâu đối xứng cùng tâm chính là số bán kính, nên tổng số xâu con đối xứng (đếm theo vị trí, hai xâu giống nhau ở hai vị trí khác nhau được đếm riêng) là . Ta lấy được hai mảng này từ : ký tự nằm ở vị trí của và dấu # đứng ngay trước nó ở vị trí , nên
với phép chia nguyên.
Câu hỏi nhanh dành cho tài khoản Pro
Mở khóa bằng tài khoản ProĐếm theo vị trí bắt đầu và kết thúc. Tâm lẻ cho các xâu đối xứng bắt đầu ở mọi vị trí trong và kết thúc ở mọi vị trí trong , mỗi vị trí đúng một xâu. Tâm chẵn cho các xâu bắt đầu trong và kết thúc trong . Cộng 1 vào cả một đoạn, rồi lấy tổng tiền tố là mảng hiệu của bài 0.3. Ta được là số xâu đối xứng bắt đầu tại và là số xâu đối xứng kết thúc tại .
Cặp xâu không giao nhau. Bài toán: cho xâu dài , đếm số cặp của hai xâu con đối xứng sao cho kết thúc trước vị trí bắt đầu (chọn theo vị trí). Với mỗi vị trí bắt đầu của , số hợp lệ là , nên đáp án là .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, m, p[N], d1[N], d2[N], st[N], en[N];
char u[N];
string s;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
n = s.size();
u[++m] = '#';
for (ll i = 0; i < n; i++) {
u[++m] = s[i];
u[++m] = '#';
}
ll l = 1, r = 0;
for (ll i = 1; i <= m; i++) {
ll k = 0;
if (i <= r) k = min(p[l + r - i], r - i);
while (i - k - 1 >= 1 && i + k + 1 <= m && u[i - k - 1] == u[i + k + 1]) k++;
p[i] = k;
if (i + k > r) l = i - k, r = i + k;
}
for (ll i = 1; i <= n; i++) {
d1[i] = (p[2 * i] + 1) / 2;
d2[i] = p[2 * i - 1] / 2;
st[i - d1[i] + 1]++, st[i + 1]--;
en[i]++, en[i + d1[i]]--;
st[i - d2[i]]++, st[i]--;
en[i]++, en[i + d2[i]]--;
}
for (ll i = 1; i <= n; i++) st[i] += st[i - 1], en[i] += en[i - 1];
ll ans = 0, pre = 0;
for (ll b = 1; b <= n; b++) {
ans += st[b] * pre;
pre += en[b];
}
cout << ans << '\n';
}
abaab
16
Độ phức tạp: , với cần khoảng phép tính.
Bài nâng cao
Xem 1 bài nâng cao
Ví dụTổng Bậc Tiền Tố
Đề. Bậc của một xâu là nếu nó không đối xứng; nếu đối xứng thì bậc bằng cộng bậc của nửa đầu có độ dài (xâu rỗng có bậc ). Cho xâu gồm chữ Latin và chữ số, . In tổng bậc của mọi tiền tố không rỗng của .
Lời giải. Gọi là bậc của tiền tố dài . Theo định nghĩa, nếu tiền tố đó đối xứng, và nếu không; nửa đầu ngắn hơn nên đã tính xong. Còn lại là biết tiền tố dài có đối xứng không. Trong , tiền tố này là với tâm , nên nó đối xứng khi và chỉ khi . Vậy Manacher cho mọi trong và một vòng cho đáp án. Với , mảng có phần tử nên ta dùng int cho mảng này để tiết kiệm bộ nhớ.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e7 + 5;
ll n, m;
int p[N];
unsigned char deg[N / 2];
char u[N];
string s;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
n = s.size();
u[++m] = '#';
for (ll i = 0; i < n; i++) {
u[++m] = s[i];
u[++m] = '#';
}
ll l = 1, r = 0;
for (ll i = 1; i <= m; i++) {
ll k = 0;
if (i <= r) k = min((ll)p[l + r - i], r - i);
while (i - k - 1 >= 1 && i + k + 1 <= m && u[i - k - 1] == u[i + k + 1]) k++;
p[i] = k;
if (i + k > r) l = i - k, r = i + k;
}
ll ans = 0;
for (ll len = 1; len <= n; len++) {
if (p[len + 1] == len) deg[len] = deg[len / 2] + 1;
ans += deg[len];
}
cout << ans << '\n';
}
aaaa
8
Độ phức tạp: , với cần khoảng phép tính.
Khi đi thi
- Hash cộng chặt nhị phân thường đủ khi ; Manacher cần khi cỡ trở lên, hoặc khi không muốn lo va chạm hash.
- Chèn
#giữa các ký tự để chỉ còn một loại tâm; nhớ rằng trên xâu mới đúng bằng độ dài xâu đối xứng trên xâu gốc. - Bài đếm cặp xâu đối xứng không giao nhau: đếm số xâu bắt đầu và kết thúc tại từng vị trí bằng mảng hiệu rồi nhân với tổng tiền tố, và dùng
long long.
Tóm tắt
- Đoạn đối xứng khi hash của nó bằng hash của đoạn trên xâu đảo ; kiểm trong .
- Các xâu đối xứng cùng tâm lồng vào nhau, nên mỗi tâm chỉ cần một bán kính; bán kính có tính đơn điệu nên chặt nhị phân được trong cho mọi tâm.
- Manacher giữ xâu đối xứng có đầu phải xa nhất và khởi đầu bằng ; đầu phải chỉ tăng nên tổng cộng .
- Chèn
#biến mọi tâm thành ký tự; bằng độ dài xâu đối xứng dài nhất có tâm trong xâu gốc. - Số xâu đối xứng bằng tổng các bán kính; hậu tố đối xứng ứng với và tiền tố đối xứng ứng với .
Bài tập
Đọc thêm
- Thuật toán ManacherVNOI Wiki
- Manacher's Algorithmcp-algorithms
- Manacher 算法OI Wiki