Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Palindrome Kép

Palindrome Kép
Độ khó: Khó Xâu Palindrome Hash Fenwick Tree

Đề bài

Palindrome là một chuỗi kí tự mà khi đọc từ trái qua phải hoặc đọc từ phải qua trái đều nhận được kết quả như nhau.

Một chuỗi Palindrome kép là một chuỗi được gộp bởi hai chuỗi Palindrome có cùng độ dài.

Ví dụ: chuỗi $\{aabb\}$, $\{aaaa\}$ là các chuỗi palindrome kép, trong khi các chuỗi $\{abba\}$ và $\{aaaabb\}$ thì không.

Có bao nhiêu cách cắt ra từ xâu $S$ để có được một chuỗi palindrome kép? Nói cách khác: có bao nhiêu cặp $(l, r)$ mà chuỗi con $S_l S_{l+1} \ldots S_r$ là một chuỗi palindrome kép?

Dữ liệu

  • Dòng 1: ghi số nguyên $n$ ($1 \le n \le 500\,000$);
  • Dòng 2: chứa chuỗi $S$ có $n$ ký tự. Các kí tự trong chuỗi $S$ là các chữ cái tiếng Anh in thường.

Kết quả: một dòng duy nhất là một số nguyên: số xâu con là chuỗi palindrome kép tìm được.

Subtasks

  • 19% số điểm với $n \le 500$;
  • 33% số điểm với $n \le 5000$;
  • 48% số điểm không có ràng buộc bổ sung.
Ví dụ 1 – input
6
abacac
Output
6
Có 5 chuỗi palindrome kép độ dài 2 được cắt ra từ một chuỗi palindrome kép ban đầu {abacac} là {ab}, {ba}, {ac}, {ca} và {ac}. Và chuỗi ban đầu cũng là một chuỗi palindrome kép.
Ví dụ 2 – input
5
aaaaa
Output
6

Liệt kê cụ thể 6 xâu con của ví dụ 1 (màu xanh là palin thứ nhất, màu cam là palin thứ hai):

(l, r)Xâu conHai nửa palindrome
(1, 2)aba | b
(1, 6)abacacaba | cac
(2, 3)bab | a
(3, 4)aca | c
(4, 5)cac | a
(5, 6)aca | c

Nộp bài

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

19%Subtask 1: duyệt và kiểm tra trực tiếp – O(n³)

Tư tưởng cơ bản:

  • Đối với mỗi vị trí $i$, ta sẽ đếm xem có bao nhiêu vị trí $j$ thoả mãn sao cho $j \ge i$ và xâu con liên tiếp từ $i$ đến $j$ là một xâu palin kép.
  • Để kiểm tra xem một xâu liên tiếp có phải là palin kép hay không: ta thấy rằng xâu palin kép là xâu được ghép lại bởi hai xâu palin có độ dài bằng nhau. Do đó chúng ta sẽ có các điều kiện kiểm tra sau:
    • Độ dài của xâu con liên tiếp bắt buộc phải là chẵn (được ghép từ hai xâu có độ dài bằng $x$ nên độ dài hiện tại sẽ là $2x$, chia hết cho 2).
    • Xâu palin đầu tiên sẽ là từ vị trí $i$ đến $i + \frac{j - i + 1}{2} - 1$. Xâu palin thứ hai sẽ là xâu còn lại. Lúc đó việc đơn giản là check riêng mỗi xâu có phải là palin hay không. Nếu cả hai đều là palin thì cộng kết quả lên một lượng là 1.

Độ phức tạp thuật toán: $O(n^3)$. Với $n \le 500$ thì chỉ khoảng vài chục triệu phép tính nên vẫn chạy kịp.

Em nhập 1 xâu rồi bấm Tiếp để xem cách duyệt mọi xâu con độ dài chẵn, cắt đôi và kiểm tra 2 nửa:

Ví dụ nhanh:
Nửa xanh: palin thứ nhất · Nửa cam: palin thứ hai · Viền đỏ: nửa đó không phải palin.
Xem code subtask 1
#include <bits/stdc++.h>
#define int long long
using namespace std;

int n;
string s;

namespace subtask_1 {
    int solve(){
        int ans = 0;
        for(int i = 1; i <= n; i++){
            for(int j = i + 1; j <= n; j += 2){
                int ps = i + (j - i + 1) / 2 - 1;
                bool ok = 1;
                for(int l = i, r = ps; l <= r; l++, r--){
                    if(s[l] != s[r])
                        ok = 0;
                }
                for(int l = ps + 1, r = j; l <= r; l++, r--){
                    if(s[l] != s[r])
                        ok = 0;
                }
                ans += ok;
            }
        }
        return ans;
    }
}; // namespace subtask_1
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n;
    cin >> s;
    s = "#" + s;
    cout << subtask_1::solve() << endl;
    return 0;
}
def solve(n, s):
    ans = 0
    s = "#" + s
    for i in range(1, n + 1):
        for j in range(i + 1, n + 1, 2):
            ps = i + (j - i + 1) // 2 - 1
            ok = True
            l, r = i, ps
            while l <= r:
                if s[l] != s[r]:
                    ok = False
                    break
                l += 1
                r -= 1
            l, r = ps + 1, j
            while l <= r:
                if s[l] != s[r]:
                    ok = False
                    break
                l += 1
                r -= 1
            if ok:
                ans += 1
    return ans

n = int(input().strip())
s = input().strip()
print(solve(n, s))
C++▶ Chạy tay từng dòng codeSubtask 1: duyệt mọi xâu con độ dài chẵn, kiểm tra trực tiếp 2 nửa, O(n³).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#define int long long
3using namespace std;
4
5int n;
6string s;
7
8namespace subtask_1 {
9 int solve(){
10 int ans = 0;
11 for(int i = 1; i <= n; i++){
12 for(int j = i + 1; j <= n; j += 2){
13 int ps = i + (j - i + 1) / 2 - 1;
14 bool ok = 1;
15 for(int l = i, r = ps; l <= r; l++, r--){
16 if(s[l] != s[r])
17 ok = 0;
18 }
19 for(int l = ps + 1, r = j; l <= r; l++, r--){
20 if(s[l] != s[r])
21 ok = 0;
22 }
23 ans += ok;
24 }
25 }
26 return ans;
27 }
28}; // namespace subtask_1
29signed main(){
30 ios::sync_with_stdio(0);
31 cin.tie(0);
32 cin >> n;
33 cin >> s;
34 s = "#" + s;
35 cout << subtask_1::solve() << endl;
36 return 0;
37}

+33%Subtask 2: tính trước bảng check bằng cách mở rộng từ trọng tâm – O(n²)

Ở đây $n \le 5000$ nên ta nhận thấy cách ở subtask 1 không còn hiệu quả nữa. Ta sẽ tìm cách tối ưu việc kiểm tra một xâu có phải palin hay không. Ta gọi check[i][j] nhận giá trị là 0 hoặc 1 với:

  • check[i][j] = 1: xâu liên tiếp từ $i$ đến $j$ là xâu đối xứng;
  • check[i][j] = 0: xâu liên tiếp từ $i$ đến $j$ không phải là xâu đối xứng.

Nhận xét rằng mỗi palin đều có 1 hoặc 2 điểm là trọng tâm:

  • Nếu xâu palin có độ dài là lẻ, thì chỉ có kí tự ở giữa là trọng tâm. Ví dụ: aabcbaa, ở xâu này thì trọng tâm sẽ rơi vào vị trí thứ 4, tức là ký tự c.
  • Nếu xâu palin có độ dài là chẵn, thì có hai kí tự là trọng tâm. Ví dụ: aaddaa, ở xâu này thì trọng tâm rơi vào vị trí 3 và 4, tức là 2 ký tự d.

Có thể thấy được rằng một xâu palin luôn được "mở rộng" từ trọng tâm. Ví dụ với aabcbaa, chúng ta có thể xây dựng xâu này bằng quy trình sau:

cbcbabcbaaabcbaa

Điều này đồng nghĩa với việc mỗi lần "mở rộng" ta thêm hai ký tự giống nhau vào ở hai đầu của xâu. Từ nhận xét trên, ta có thể:

Trường hợp xâu palin lẻ: duyệt qua từng vị trí $x$ trong mảng. Lúc đó ta sẽ xem xét những xâu nhận $x$ là trọng tâm, ta sẽ thêm dần các kí tự ở hai bên $x$. Điều kiện để thêm là hai ký tự đó phải bằng nhau. Ví dụ ta có xâu abbcbbaa, giả sử giờ ta đang duyệt đến $x = 4$. Ta gọi xâu $s$ sẽ là xâu palin hiện tại, ban đầu ta đặt $s$ = c. Tiếp đến sau đó ta lần lượt thêm như sau:

s = "bcb" ⇒ thoả mãns = "bbcbb" ⇒ thoả mãns = "abbcbba" ⇒ thoả mãn

Đến đây ta không thể thêm được nữa vì không còn ký tự nào đằng trước ký tự thứ nhất. Mỗi xâu thoả mãn ta đánh dấu check[l][r] = 1.

Trường hợp xâu palin chẵn: tương tự xâu palin lẻ nhưng ta sẽ coi trọng tâm của ta là $x$ và $x + 1$.

Đến đây ta đã có được kết quả của mảng check. Việc còn lại là làm tương tự như subtask 1, nhưng mà sẽ gọi thẳng kết quả xác định có phải palin hay không từ mảng check mà không cần chạy for để kiểm tra.

Độ phức tạp thuật toán: $O(n^2)$, với $n \le 5000$ là khoảng $2.5 \times 10^7$ phép tính.

Xem code subtask 2
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n;
string s;
bool check[5005][5005];
namespace subtask_2 {
    int solve(){
        int ans = 0;
        for(int i = 1; i <= n; i++){
            int l = i, r = i;
            while(l >= 1 && r <= n){
                if(s[l] != s[r])
                    break;
                check[l][r] = 1;
                l--;
                r++;
            }
        }
        for(int i = 2; i <= n; i++){
            int l = i - 1, r = i;
            while(l >= 1 && r <= n){
                if(s[l] != s[r])
                    break;
                check[l][r] = 1;
                l--;
                r++;
            }
        }
        for(int i = 1; i <= n; i++){
            for(int j = i + 1; j <= n; j += 2){
                int ps = i + (j - i + 1) / 2 - 1;
                if(check[i][ps] && check[ps + 1][j])
                    ans++;
            }
        }
        return ans;
    }
}; // namespace subtask_2

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n;
    cin >> s;
    s = "#" + s;
    cout << subtask_2::solve() << endl;
    return 0;
}
def solve(n, s):
    s = "#" + s # để dùng chỉ số 1-based
    check = [[False] * (n + 2) for _ in range(n + 2)]
    ans = 0

    # Các palindrome có độ dài lẻ
    for i in range(1, n + 1):
        l, r = i, i
        while l >= 1 and r <= n:
            if s[l] != s[r]:
                break
            check[l][r] = True
            l -= 1
            r += 1

    # Các palindrome có độ dài chẵn
    for i in range(2, n + 1):
        l, r = i - 1, i
        while l >= 1 and r <= n:
            if s[l] != s[r]:
                break
            check[l][r] = True
            l -= 1
            r += 1

    # Đếm các cặp (i, j)
    for i in range(1, n + 1):
        for j in range(i + 1, n + 1, 2):
            ps = i + (j - i + 1) // 2 - 1
            if check[i][ps] and check[ps + 1][j]:
                ans += 1

    return ans

n = int(input().strip())
s = input().strip()
print(solve(n, s))
C++▶ Chạy tay từng dòng codeSubtask 2: mở rộng từ trọng tâm để lập bảng check[l][r], rồi đếm bằng tra bảng, O(n²).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#define int long long
3using namespace std;
4int n;
5string s;
6bool check[5005][5005];
7namespace subtask_2 {
8 int solve(){
9 int ans = 0;
10 for(int i = 1; i <= n; i++){
11 int l = i, r = i;
12 while(l >= 1 && r <= n){
13 if(s[l] != s[r])
14 break;
15 check[l][r] = 1;
16 l--;
17 r++;
18 }
19 }
20 for(int i = 2; i <= n; i++){
21 int l = i - 1, r = i;
22 while(l >= 1 && r <= n){
23 if(s[l] != s[r])
24 break;
25 check[l][r] = 1;
26 l--;
27 r++;
28 }
29 }
30 for(int i = 1; i <= n; i++){
31 for(int j = i + 1; j <= n; j += 2){
32 int ps = i + (j - i + 1) / 2 - 1;
33 if(check[i][ps] && check[ps + 1][j])
34 ans++;
35 }
36 }
37 return ans;
38 }
39}; // namespace subtask_2
40
41signed main(){
42 ios::sync_with_stdio(0);
43 cin.tie(0);
44 cin >> n;
45 cin >> s;
46 s = "#" + s;
47 cout << subtask_2::solve() << endl;
48 return 0;
49}
Lưu ý cho bạn nào code Python: bảng check kích thước $5000 \times 5000$ trong Python là list lồng list nên tốn khá nhiều bộ nhớ (cỡ vài trăm MB) và chạy mất khoảng 1 giây, nên có thể bị quá giới hạn bộ nhớ/thời gian tuỳ hệ thống chấm. Bản C++ dùng mảng bool chỉ tốn khoảng 25 MB.

+48%Subtask 3: hash + chặt nhị phân + cây Fenwick – O(n log n)

Bước 1: độ dài palin lớn nhất nhận mỗi vị trí làm trọng tâm

Xét trường hợp những xâu palin kép có độ dài các palin cấu thành là lẻ. Đầu tiên, đối với mỗi vị trí $i$, chúng ta sẽ tìm độ dài của xâu palin có độ dài lớn nhất mà nhận $i$ làm trọng tâm. Điều này có thể thực hiện bằng hash và chặt nhị phân (so sánh hash xuôi của nửa bên phải với hash ngược của nửa bên trái).

Gọi len[x] là số ký tự ở nửa bên phải (tính cả trọng tâm) trong xâu palin dài nhất nếu nhận trọng tâm là $x$. Ví dụ với aaabb: ở vị trí 2, palin dài nhất nhận trọng tâm này sẽ là aaa, số ký tự nửa bên phải của nó sẽ là 2.

Bước 2: điều kiện để ghép được palin kép từ 2 trọng tâm x, y

Tiếp đến, trong một palin kép, hãy gọi $x$ là trọng tâm của palin thứ nhất, $y$ là trọng tâm của palin thứ hai ($x < y$). Hai palin có cùng độ dài lẻ nên khoảng cách $y - x$ chính bằng độ dài mỗi palin, là số lẻ; vì vậy ta nhận thấy rằng $x$, $y$ phải khác tính chẵn lẻ (đồng dư khác nhau khi chia cho 2).

abcbaxyzyxtrọng tâm xtrọng tâm ypalin thứ nhấtpalin thứ hai

Để có thể tạo ra một palin kép với hai trọng tâm là $x$, $y$, ta nhận xét rằng phải tồn tại một cách nào đó để cho rìa phải của palin trọng tâm $x$ chạm rìa trái của palin trọng tâm $y$. Suy ra độ dài lớn nhất mà palin ở trọng tâm $x$ và $y$ có thể tạo được phải lớn hơn hoặc bằng một nửa khoảng cách của hai vị trí $x$, $y$: $\frac{y - x + 1}{2}$. Khi đấy:

Điều kiện thứ nhất (với x)
len[x] ≥ (y − x + 1) / 2
⇔ len[x] * 2 ≥ y − x + 1
⇔ y ≤ len[x] * 2 + x − 1
Điều kiện thứ hai (với y)
len[y] ≥ (y − x + 1) / 2
⇔ len[y] * 2 ≥ y − x + 1
⇔ y − len[y] * 2 + 1 ≤ x ≤ y

Bước 3: đếm bằng cây Fenwick, duyệt ngược x từ n về 1

Duyệt ngược $x$ từ $n$ về 1. Khi đó với mỗi $x$, ta cần đếm số lượng $y$ nằm trong khoảng $(x, \; len[x] \times 2 + x - 1]$ mà đồng thời thoả điều kiện thứ hai. Để làm được việc này, ta nhận xét rằng điều kiện thứ hai chỉ xảy ra nếu $x$ nằm trong khoảng từ $y - len[y] \times 2 + 1$ đến $y$. Tức là mỗi vị trí $y$ chỉ "còn hiệu lực" khi $x$ đi trong đoạn đó.

Ta sẽ sử dụng một cây segment tree / Fenwick tree, quản lý số lượng các vị trí trong phân đoạn bất kì đang phù hợp để làm trọng tâm $y$ hay không. Dựa vào điều này, ta có thể làm cách sau: khi $x$ chạy ngược từ $n$ về 1, những vị trí có điểm kết thúc bằng $x$ (tức chính $y = x$) sẽ update cây vị trí đó thành 1 (có thể là trọng tâm $y$). Những vị trí nếu có điểm bắt đầu ($y - len[y] \times 2 + 1$) bằng $x$ thì sau khi xử lý xong $x$, ta sẽ cập nhật lại chúng thành 0 (không thể là trọng tâm $y$ nữa vì $x$ đã vượt ngoài khoảng mà vị trí đó có thể chấp nhận).

Điều đặc biệt ở đây là vì $x$, $y$ phải khác tính chẵn lẻ nên chúng ta sẽ chia thành hai đợt xử lý: một đợt là xử lý trường hợp $x$ lẻ $y$ chẵn và một đợt là xử lý $x$ chẵn $y$ lẻ.

Xét trường hợp những xâu palin kép có độ dài các palin cấu thành là chẵn: vẫn tương tự trường hợp lẻ nhưng ta sẽ thay đổi một chút về trọng tâm. len[x] thay vì là độ dài lớn nhất nếu nhận $x$ làm trọng tâm thì sẽ là độ dài lớn nhất nếu nhận cặp $x - 1$ và $x$ làm trọng tâm.

Độ phức tạp: $O(n \log n)$ (chặt nhị phân trên hash cho mỗi vị trí và các thao tác trên cây Fenwick).

#include <bits/stdc++.h>
#define int long long
using namespace std;

int n;
string s;

namespace subtask_3 {
    int MOD = 1e9 + 7;
    int base = 23;
    int len[500005];
    int hashS[500005];
    int hashT[500005];
    int fen[500005];
    int g[500005];
    vector<vector<int>> ps(500005);

    void reset(int x){
        for(int i = 1; i <= x; i++)
            fen[i] = 0;
    }

    void up(int u, int chg){
        int idx = u;
        while(idx <= n){
            fen[idx] += chg;
            idx += idx & (-idx);
        }
        return;
    }

    int query(int l, int r){
        if(l > r)
            return 0;
        l--;
        r = min(r, n);
        int idx = r;
        int ans = 0;
        while(idx > 0){
            ans += fen[idx];
            idx -= idx & (-idx);
        }
        idx = l;
        while(idx > 0){
            ans -= fen[idx];
            idx -= idx & (-idx);
        }
        return ans;
    }

    int getHashS(int i, int j){
        return (hashS[j] - ((hashS[i - 1] * g[j - i + 1]) % MOD) + MOD) % MOD;
    }

    int getHashT(int i, int j){
        return (hashT[i] - ((hashT[j + 1] * g[j - i + 1]) % MOD) + MOD) % MOD;
    }

    void pre(int x){
        int ans = 0;

        for(int i = 1; i <= n; i++){
            if(i - x < 1)
                continue;
            int ps1 = i - x, ps2 = i, res = 0;
            if(s[ps1] != s[ps2]){
                len[i] = 0;
                continue;
            }
            int l = 0, r = min(ps1 - 1, n - ps2), mid;
            while(l <= r){
                mid = (l + r) / 2;
                if(getHashT(ps1 - mid, ps1) == getHashS(ps2, ps2 + mid)){
                    l = mid + 1;
                    res = max(res, mid);
                }
                else
                    r = mid - 1;
            }
            len[i] = res + 1;
        }
    }

    int xuly(int x, int y, int hic){
        reset(n);
        for(int i = 1; i <= n; i++){
            ps[i].clear();
        }
        for(int i = 1; i <= n; i++){
            if(i + hic > n)
                continue;
            if(i - len[i + hic] * 2 >= 1){
                ps[i - len[i + hic] * 2].push_back(i);
            }
        }
        int ans = 0;
        for(int i = n; i >= 1; i--){
            if(i % 2 == x && i - hic >= 1){
                ans += query(i, len[i] * 2 + i - 1);
            }
            if(i % 2 == y && i + hic <= n){
                up(i, 1);
            }
            for(auto j : ps[i]){
                if(j % 2 == y && j + hic <= n){
                    up(j, -1);
                }
            }
        }
        return ans;
    }

    int cal(int x){
        int ans = 0;
        ans += xuly(0, 1, 1 ^ x);
        ans += xuly(1, 0, 1 ^ x);
        return ans;
    }

    int solve(){
        g[0] = 1;
        for(int i = 1; i <= n; i++){
            g[i] = g[i - 1] * base;
            g[i] %= MOD;
        }
        int ans = 0;
        for(int i = 1; i <= n; i++){
            hashS[i] = hashS[i - 1] * base + (s[i] - 96);
            hashS[i] %= MOD;
        }
        for(int i = n; i >= 1; i--){
            hashT[i] = hashT[i + 1] * base + (s[i] - 96);
            hashT[i] %= MOD;
        }
        pre(0);
        ans += cal(1);
        pre(1);
        ans += cal(0);
        return ans;
    }
} // namespace subtask_3

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n;
    cin >> s;
    s = "#" + s;
    cout << subtask_3::solve() << endl;
    return 0;
}
def solve_subtask3(n, s):
    MOD = 10**9 + 7
    base = 23
    s = "#" + s # 1-based indexing

    len_arr = [0] * (n + 5)
    hashS = [0] * (n + 5)
    hashT = [0] * (n + 5)
    fen = [0] * (n + 5)
    g = [0] * (n + 5)
    ps = [[] for _ in range(n + 5)]

    def reset(x):
        for i in range(1, x + 1):
            fen[i] = 0

    def up(u, chg):
        idx = u
        while idx <= n:
            fen[idx] += chg
            idx += idx & -idx

    def query(l, r):
        if l > r:
            return 0
        l -= 1
        r = min(r, n)
        idx = r
        ans = 0
        while idx > 0:
            ans += fen[idx]
            idx -= idx & -idx
        idx = l
        while idx > 0:
            ans -= fen[idx]
            idx -= idx & -idx
        return ans

    def getHashS(i, j):
        return (hashS[j] - (hashS[i - 1] * g[j - i + 1]) % MOD + MOD) % MOD

    def getHashT(i, j):
        return (hashT[i] - (hashT[j + 1] * g[j - i + 1]) % MOD + MOD) % MOD

    def pre(x):
        for i in range(1, n + 1):
            if i - x < 1:
                continue
            ps1 = i - x
            ps2 = i
            res = 0
            if s[ps1] != s[ps2]:
                len_arr[i] = 0
                continue
            l, r = 0, min(ps1 - 1, n - ps2)
            while l <= r:
                mid = (l + r) // 2
                if getHashT(ps1 - mid, ps1) == getHashS(ps2, ps2 + mid):
                    res = max(res, mid)
                    l = mid + 1
                else:
                    r = mid - 1
            len_arr[i] = res + 1

    def xuly(x, y, hic):
        reset(n)
        for i in range(1, n + 1):
            ps[i].clear()
        for i in range(1, n + 1):
            if i + hic > n:
                continue
            if i - len_arr[i + hic] * 2 >= 1:
                ps[i - len_arr[i + hic] * 2].append(i)
        ans = 0
        for i in range(n, 0, -1):
            if i % 2 == x and i - hic >= 1:
                ans += query(i, len_arr[i] * 2 + i - 1)
            if i % 2 == y and i + hic <= n:
                up(i, 1)
            for j in ps[i]:
                if j % 2 == y and j + hic <= n:
                      up(j, -1)
        return ans

    def cal(x):
        ans = 0
        ans += xuly(0, 1, 1 ^ x)
        ans += xuly(1, 0, 1 ^ x)
        return ans

    # precompute powers
    g[0] = 1
    for i in range(1, n + 1):
        g[i] = (g[i - 1] * base) % MOD

    # forward hash
    for i in range(1, n + 1):
        hashS[i] = (hashS[i - 1] * base + (ord(s[i]) - 96)) % MOD

    # backward hash
    for i in range(n, 0, -1):
        hashT[i] = (hashT[i + 1] * base + (ord(s[i]) - 96)) % MOD

    pre(0)
    ans = cal(1)
    pre(1)
    ans += cal(0)
    return ans


n = int(input().strip())
s = input().strip()
print(solve_subtask3(n, s))
C++▶ Chạy tay từng dòng codeSubtask 3: hash + chặt nhị phân tìm len[i], rồi đếm cặp trọng tâm (x, y) bằng cây Fenwick, O(n log n).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#define int long long
3using namespace std;
4
5int n;
6string s;
7
8namespace subtask_3 {
9 int MOD = 1e9 + 7;
10 int base = 23;
11 int len[500005];
12 int hashS[500005];
13 int hashT[500005];
14 int fen[500005];
15 int g[500005];
16 vector<vector<int>> ps(500005);
17
18 void reset(int x){
19 for(int i = 1; i <= x; i++)
20 fen[i] = 0;
21 }
22
23 void up(int u, int chg){
24 int idx = u;
25 while(idx <= n){
26 fen[idx] += chg;
27 idx += idx & (-idx);
28 }
29 return;
30 }
31
32 int query(int l, int r){
33 if(l > r)
34 return 0;
35 l--;
36 r = min(r, n);
37 int idx = r;
38 int ans = 0;
39 while(idx > 0){
40 ans += fen[idx];
41 idx -= idx & (-idx);
42 }
43 idx = l;
44 while(idx > 0){
45 ans -= fen[idx];
46 idx -= idx & (-idx);
47 }
48 return ans;
49 }
50
51 int getHashS(int i, int j){
52 return (hashS[j] - ((hashS[i - 1] * g[j - i + 1]) % MOD) + MOD) % MOD;
53 }
54
55 int getHashT(int i, int j){
56 return (hashT[i] - ((hashT[j + 1] * g[j - i + 1]) % MOD) + MOD) % MOD;
57 }
58
59 void pre(int x){
60 int ans = 0;
61
62 for(int i = 1; i <= n; i++){
63 if(i - x < 1)
64 continue;
65 int ps1 = i - x, ps2 = i, res = 0;
66 if(s[ps1] != s[ps2]){
67 len[i] = 0;
68 continue;
69 }
70 int l = 0, r = min(ps1 - 1, n - ps2), mid;
71 while(l <= r){
72 mid = (l + r) / 2;
73 if(getHashT(ps1 - mid, ps1) == getHashS(ps2, ps2 + mid)){
74 l = mid + 1;
75 res = max(res, mid);
76 }
77 else
78 r = mid - 1;
79 }
80 len[i] = res + 1;
81 }
82 }
83
84 int xuly(int x, int y, int hic){
85 reset(n);
86 for(int i = 1; i <= n; i++){
87 ps[i].clear();
88 }
89 for(int i = 1; i <= n; i++){
90 if(i + hic > n)
91 continue;
92 if(i - len[i + hic] * 2 >= 1){
93 ps[i - len[i + hic] * 2].push_back(i);
94 }
95 }
96 int ans = 0;
97 for(int i = n; i >= 1; i--){
98 if(i % 2 == x && i - hic >= 1){
99 ans += query(i, len[i] * 2 + i - 1);
100 }
101 if(i % 2 == y && i + hic <= n){
102 up(i, 1);
103 }
104 for(auto j : ps[i]){
105 if(j % 2 == y && j + hic <= n){
106 up(j, -1);
107 }
108 }
109 }
110 return ans;
111 }
112
113 int cal(int x){
114 int ans = 0;
115 ans += xuly(0, 1, 1 ^ x);
116 ans += xuly(1, 0, 1 ^ x);
117 return ans;
118 }
119
120 int solve(){
121 g[0] = 1;
122 for(int i = 1; i <= n; i++){
123 g[i] = g[i - 1] * base;
124 g[i] %= MOD;
125 }
126 int ans = 0;
127 for(int i = 1; i <= n; i++){
128 hashS[i] = hashS[i - 1] * base + (s[i] - 96);
129 hashS[i] %= MOD;
130 }
131 for(int i = n; i >= 1; i--){
132 hashT[i] = hashT[i + 1] * base + (s[i] - 96);
133 hashT[i] %= MOD;
134 }
135 pre(0);
136 ans += cal(1);
137 pre(1);
138 ans += cal(0);
139 return ans;
140 }
141} // namespace subtask_3
142
143signed main(){
144 ios::sync_with_stdio(0);
145 cin.tie(0);
146 cin >> n;
147 cin >> s;
148 s = "#" + s;
149 cout << subtask_3::solve() << endl;
150 return 0;
151}
Code này có rất nhiều hàm con nên số bước khá lớn: em nên chọn tốc độ "Nhanh" và để ý ngăn xếp lời gọi hàm (dòng chip trên cùng của phần Dữ liệu).

Mình chạy thử: với $n = 500\,000$ bản C++ chạy khoảng 0.3 giây (kể cả chuỗi toàn chữ a với đáp án lên tới 62 500 000 000, nên biến đếm phải dùng long long như code đã #define int long long). Bản Python cùng thuật toán mất khoảng 12 giây với $n = 500\,000$ nên chỉ nên dùng để tham khảo ý tưởng; muốn ăn trọn subtask cuối thì nên nộp bản C++. Ngoài ra, bước 1 cũng có thể thay hash + chặt nhị phân bằng thuật toán Manacher để tìm palin dài nhất tại mọi trọng tâm trong $O(n)$, bạn nào thích có thể tìm hiểu thêm.

Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇