Chương 1·Bài 1.10.2

Manacher Algorithm

Cho một xâu ss gồm n≤106n \le 10^6 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 O(n)O(n).

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 O(n2)O(n^2). Hash (bài 1.10.1) cho phép chặt nhị phân độ dài mở rộng ở mỗi tâm, được O(nlog⁡n)O(n \log n). 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ỉ O(n)O(n) 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 ss từ 11 tới nn và viết s[l..r]s[l..r] cho đoạn từ ký tự thứ ll tới ký tự thứ rr. Gọi tt là xâu đảo của ss, tức ti=sn+1−it_i = s_{n+1-i}; với ss = dabacab thì tt = bacabad. Đoạn s[l..r]s[l..r] viết ngược chính là t[n+1−r..n+1−l]t[n+1-r..n+1-l], nên s[l..r]s[l..r] đối xứng khi và chỉ khi hai đoạn này bằng nhau. Có hash tiền tố của cả ss và tt (bài 1.10.1), việc đó là một phép so sánh hai số, tốn O(1)O(1) sau O(n)O(n) chuẩn bị.

Đoạn và xâu đảo.

Hỏi nhanh

Xâu ss dài n=10n = 10. Để biết đoạn s[3..6]s[3..6] có đối xứng không, ta so hash của nó với hash đoạn nào của xâu đảo tt?

Ví dụCửa Sổ Đối Xứng

Đề. Cho số nguyên kk và xâu ss gồm chữ thường, 2≤k≤∣s∣≤3⋅1042 \le k \le |s| \le 3 \cdot 10^4. Đếm số cửa sổ gồm đúng kk ký tự liên tiếp của ss 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ó n−k+1n - k + 1 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 O(k)O(k), cả bài O(nk)O(nk). Với hash của ss và của tt, mỗi cửa sổ chỉ còn một phép so sánh. Ta dùng modulo 261−12^{61} - 1 với cơ số ngẫu nhiên và hàm mul như ở bài 1.10.1. Hash của tt dựng bằng cách đọc ss từ cuối về đầu.

Code đầy đủ
#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

Độ phức tạp: O(n)O(n), với n=3⋅104n = 3 \cdot 10^4 cần khoảng 10510^5 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í ii, ta đặt hai đại lượng:

  • d1[i]d_1[i] là số xâu đối xứng có độ dài lẻ với ký tự giữa là sis_i. Xâu dài nhất trong số đó có độ dài 2d1[i]−12 d_1[i] - 1, và d1[i]≥1d_1[i] \ge 1 vì một ký tự luôn đối xứng.
  • d2[i]d_2[i] là số xâu đối xứng có độ dài chẵn với hai ký tự giữa là si−1s_{i-1} và sis_i. Xâu dài nhất có độ dài 2d2[i]2 d_2[i], và d2[i]d_2[i] có thể bằng 00.

Ta gọi chung d1[i]d_1[i], d2[i]d_2[i] là bán kính của tâm tương ứng. Với ss = abaaba thì d1=1,2,1,1,2,1d_1 = 1, 2, 1, 1, 2, 1 và d2=0,0,0,3,0,0d_2 = 0, 0, 0, 3, 0, 0.

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ách dễ nghĩ ra nhất là mở rộng từng ký tự một cho mọi tâm:

C++
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: O(n2)O(n^2) khi xâu gồm toàn một chữ cái, với n=106n = 10^6 cần khoảng 5⋅10115 \cdot 10^{11} 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 kk mà đúng thì bán kính k−1k - 1 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 O(1)O(1) bằng hash ở mục trước.

Ví dụĐối Xứng Dài Nhất

Đề. Cho xâu ss dài n≤5⋅104n \le 5 \cdot 10^4, 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ẻ ii, bán kính kk nằm trong [1,min⁡(i,n−i+1)][1, \min(i, n - i + 1)] và đoạn cần kiểm là s[i−k+1..i+k−1]s[i-k+1..i+k-1]. Với mỗi tâm chẵn ii, bán kính kk nằm trong [0,min⁡(i−1,n−i+1)][0, \min(i - 1, n - i + 1)] và đoạn cần kiểm là s[i−k..i+k−1]s[i-k..i+k-1]. Hàm pal(l, r) so hash của s[l..r]s[l..r] với hash đoạn tương ứng trên xâu đảo. Đáp án là giá trị lớn nhất của 2d1[i]−12 d_1[i] - 1 và 2d2[i]2 d_2[i] trên mọi ii.

Code đầy đủ
#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

Độ phức tạp: O(nlog⁡n)O(n \log n), với n=5⋅104n = 5 \cdot 10^4 cần khoảng 1,6⋅1061{,}6 \cdot 10^6 lần so hash.

Cách này còn log⁡n\log 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 uu từ ss 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 m=2n+1m = 2n + 1. Trong uu mọi tâm đều là một ký tự: tâm lẻ của ss là ký tự thật, tâm chẵn của ss 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 ss cũng có ký tự #.

Gọi p[i]p[i] là bán kính lớn nhất của xâu đối xứng trong uu có tâm ii, tức số lớn nhất kk để u[i−k..i+k]u[i-k..i+k] đố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 p[i]+1p[i] + 1 dấu # và p[i]p[i] ký tự thật, và p[i]p[i] chính là độ dài xâu đối xứng dài nhất của ss có tâm đó. Với uu = #a#b#a#b# ta có p=0,1,0,3,0,3,0,1,0p = 0, 1, 0, 3, 0, 3, 0, 1, 0.

Hỏi nhanh

Với ss = abab, xâu uu là #a#b#a#b#. Giá trị p[i]p[i] lớn nhất là bao nhiêu, và nó cho biết điều gì?

Dùng lại bán kính của điểm đối xứng. Ta tính p[i]p[i] lần lượt từ i=1i = 1 tới mm, và giữ xâu đối xứng u[l..r]u[l..r] tìm được cho tới lúc này có đầu phải rr xa nhất. Khi tâm ii nằm trong xâu đó (i≤ri \le r), điểm j=l+r−ij = l + r - i đối xứng với ii qua tâm của u[l..r]u[l..r]. Phần u[l..r]u[l..r] đọc xuôi hay ngược như nhau, nên mọi thứ quanh ii, trong phạm vi còn nằm trong [l,r][l, r], giống hệt quanh jj.

Trong xâu đối xứng abacaba (vị trí 2 tới 8), j = 3 và i = 7 đối xứng qua tâm 5, nên xâu aba quanh j lặp lại quanh i.d1a2b3a4c5a6b7a8e9lrcji
Trong xâu đối xứng abacaba (vị trí 2 tới 8), j = 3 và i = 7 đối xứng qua tâm 5, nên xâu aba quanh j lặp lại quanh i.

Do đó p[i]p[i] ít nhất bằng min⁡(p[j],r−i)\min(p[j], r - i), với r−ir - i là khoảng cách từ ii tới đầu phải rr. 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 p[j]<r−ip[j] < r - i thì phép mở rộng dừng ngay ở bước đầu; nếu không, r−ir - i ký tự đã được kiểm và ta chỉ cần xét các ký tự ngoài rr.

Chứng minh

Chặn dưới. Với i≤ri \le r, tâm của u[l..r]u[l..r] không sau ii (nó là một tâm đã xét trước đó), nên r−i≤i−lr - i \le i - l. Lấy k≤min⁡(p[j],r−i)k \le \min(p[j], r - i). Đoạn u[i−k..i+k]u[i-k..i+k] nằm trong [l,r][l, r], và phép đối xứng x↦l+r−xx \mapsto l + r - x biến nó thành u[j−k..j+k]u[j-k..j+k] đọc ngược. Đoạn này đối xứng vì k≤p[j]k \le p[j], nên u[i−k..i+k]u[i-k..i+k] cũng đối xứng. Vậy p[i]≥min⁡(p[j],r−i)p[i] \ge \min(p[j], r - i).

Khi p[j]<r−ip[j] < r - i thì p[i]=p[j]p[i] = p[j]. Hai ký tự u[j−p[j]−1]u[j-p[j]-1] và u[j+p[j]+1]u[j+p[j]+1] khác nhau và cùng nằm trong [l,r][l, r]. Ảnh của chúng qua phép đối xứng là u[i+p[j]+1]u[i+p[j]+1] và u[i−p[j]−1]u[i-p[j]-1], nên hai ký tự này cũng khác nhau và p[i]p[i] không thể lớn hơn p[j]p[j].

Độ phức tạp. Mỗi lần vòng mở rộng thành công tăng kk thêm 1, vị trí i+ki + k vượt qua rr hiện tại. Nếu p[j]<r−ip[j] < r - i vòng không chạy; nếu không, xuất phát của kk là r−ir - i hoặc 00 (khi i>ri > r), nên mỗi lần tăng kk đẩy i+ki + k vượt rr một đơn vị, và sau khi xong ta đặt r=i+kr = i + k. Vậy rr tăng ít nhất bằng số lần mở rộng thành công, mà r≤mr \le m. Tổng số lần mở rộng không quá mm, cả thuật toán O(m)=O(n)O(m) = O(n).

C++
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: O(m)=O(n)O(m) = O(n), với n=106n = 10^6 cần khoảng 4⋅1064 \cdot 10^6 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 p[i]p[i] lớn nhất tại tâm ii thì xâu đó là u[i−p[i]..i+p[i]]u[i-p[i]..i+p[i]], bắt đầu ở ký tự thứ (i−p[i]+1)/2(i - p[i] + 1)/2 của ss.

C++
#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

Độ phức tạp: O(n)O(n), với n=106n = 10^6 cần khoảng 4⋅1064 \cdot 10^6 phép tính.

IVXâu đối xứng ở cuối xâu

Một xâu con s[a..n]s[a..n] (hậu tố) đối xứng khi nào? Trong uu, nó là đoạn u[2a−1..m]u[2a-1..m] kết thúc ở dấu # cuối cùng, và tâm ii của nó thỏa i+p[i]=mi + p[i] = m: đoạn đối xứng tâm ii chạm tới tận đầu phải của uu. Ngược lại, tâm ii nào có i+p[i]=mi + p[i] = m cho một hậu tố đối xứng dài p[i]p[i]. Hậu tố đối xứng dài nhất ứng với tâm ii nhỏ nhất như vậy. Tương tự cho tiền tố, với điều kiện i−p[i]=1i - p[i] = 1.

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 10510^5. 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 ℓ\ell là độ dài hậu tố đối xứng dài nhất của ss. Khi nối đảo của n−ℓn - \ell 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à n−ℓn - \ell, nhỏ nhất khi ℓ\ell lớn nhất. Ta chạy Manacher, tìm tâm ii nhỏ nhất có i+p[i]=mi + p[i] = m và đặt ℓ=p[i]\ell = p[i]. Với Abc, hậu tố đối xứng dài nhất là c, ta nối bA để được AbcbA.

Code đầy đủ
#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

Độ phức tạp: O(n)O(n) cho mỗi xâu, với n=105n = 10^5 cần khoảng 4⋅1054 \cdot 10^5 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à ∑i(d1[i]+d2[i])\sum_i (d_1[i] + d_2[i]). Ta lấy được hai mảng này từ pp: ký tự sis_i nằm ở vị trí 2i2i của uu và dấu # đứng ngay trước nó ở vị trí 2i−12i - 1, nên

d1[i]=p[2i]+12,d2[i]=p[2i−1]2d_1[i] = \frac{p[2i] + 1}{2}, \qquad d_2[i] = \frac{p[2i-1]}{2}

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ẻ ii cho các xâu đối xứng bắt đầu ở mọi vị trí trong [i−d1[i]+1,i][i - d_1[i] + 1, i] và kết thúc ở mọi vị trí trong [i,i+d1[i]−1][i, i + d_1[i] - 1], mỗi vị trí đúng một xâu. Tâm chẵn ii cho các xâu bắt đầu trong [i−d2[i],i−1][i - d_2[i], i - 1] và kết thúc trong [i,i+d2[i]−1][i, i + d_2[i] - 1]. 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 st[a]st[a] là số xâu đối xứng bắt đầu tại aa và en[b]en[b] là số xâu đối xứng kết thúc tại bb.

Cặp xâu không giao nhau. Bài toán: cho xâu ss dài n≤105n \le 10^5, đếm số cặp (A,B)(A, B) của hai xâu con đối xứng sao cho AA kết thúc trước vị trí BB bắt đầu (chọn theo vị trí). Với mỗi vị trí bắt đầu bb của BB, số AA hợp lệ là en[1]+⋯+en[b−1]en[1] + \dots + en[b-1], nên đáp án là ∑bst[b]⋅(en[1]+⋯+en[b−1])\sum_b st[b] \cdot (en[1] + \dots + en[b-1]).

C++
#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

Độ phức tạp: O(n)O(n), với n=105n = 10^5 cần khoảng 10610^6 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à 00 nếu nó không đối xứng; nếu đối xứng thì bậc bằng 11 cộng bậc của nửa đầu có độ dài ⌊∣x∣/2⌋\lfloor |x|/2 \rfloor (xâu rỗng có bậc 00). Cho xâu ss gồm chữ Latin và chữ số, ∣s∣≤5⋅106|s| \le 5 \cdot 10^6. In tổng bậc của mọi tiền tố không rỗng của ss.

Lời giải. Gọi deg⁡[L]\deg[L] là bậc của tiền tố dài LL. Theo định nghĩa, deg⁡[L]=deg⁡[⌊L/2⌋]+1\deg[L] = \deg[\lfloor L/2 \rfloor] + 1 nếu tiền tố đó đối xứng, và 00 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 LL có đối xứng không. Trong uu, tiền tố này là u[1..2L+1]u[1..2L+1] với tâm L+1L + 1, nên nó đối xứng khi và chỉ khi p[L+1]=Lp[L+1] = L. Vậy Manacher cho mọi pp trong O(n)O(n) và một vòng L=1..nL = 1..n cho đáp án. Với ∣s∣=5⋅106|s| = 5 \cdot 10^6, mảng pp có 10710^7 phần tử nên ta dùng int cho mảng này để tiết kiệm bộ nhớ.

Code đầy đủ
#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

Độ phức tạp: O(n)O(n), với n=5⋅106n = 5 \cdot 10^6 cần khoảng 3⋅1073 \cdot 10^7 phép tính.

Khi đi thi

  • Hash cộng chặt nhị phân thường đủ khi n≤105n \le 10^5; Manacher cần khi nn cỡ 10610^6 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 p[i]p[i] 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 s[l..r]s[l..r] đối xứng khi hash của nó bằng hash của đoạn t[n+1−r..n+1−l]t[n+1-r..n+1-l] trên xâu đảo tt; kiểm trong O(1)O(1).
  • 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 O(nlog⁡n)O(n \log n) cho mọi tâm.
  • Manacher giữ xâu đối xứng có đầu phải xa nhất [l,r][l, r] và khởi đầu p[i]p[i] bằng min⁡(p[l+r−i],r−i)\min(p[l+r-i], r-i); đầu phải chỉ tăng nên tổng cộng O(n)O(n).
  • Chèn # biến mọi tâm thành ký tự; p[i]p[i] bằng độ dài xâu đối xứng dài nhất có tâm ii 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 i+p[i]=mi + p[i] = m và tiền tố đối xứng ứng với i−p[i]=1i - p[i] = 1.

Bài tập

Đọc thêm