Skip to content

Palindrome Tree (Eertree) - Cây Xâu Đối Xứng

Tác giả: FPTOJ Team
Nội dung tham khảo từ: CP-Algorithms - Palindrome Tree


1. Bản chất vấn đề

Bài toán: Đếm số palindrome con phân biệt

Cho xâu \(S\) độ dài \(N\). Đếm số xâu con palindrome phân biệt trong \(S\).

Cách thường: Duyệt tất cả \(O(N^2)\) xâu con, kiểm tra palindrome \(O(N)\) mỗi xâu \(\Rightarrow O(N^3)\).

Palindrome Tree (Eertree): Xây dựng trong \(O(N)\), mỗi palindrome là 1 nút trong cây.

So sánh

Phương pháp Thời gian Không gian
Duyệt + Manacher \(O(N^2)\) \(O(N)\)
Hashing \(O(N^2)\) \(O(N^2)\)
Palindrome Tree \(O(N)\) \(O(N)\)

2. Tư duy cốt lõi

Cấu trúc cây

Palindrome Tree có 2 gốc:

  • Node \(-1\): Gốc ảo (độ dài \(-1\)),方便处理 lẻ palindrome.
  • Node \(0\): Gốc cho chẵn palindrome (độ dài \(0\)).

Mỗi nút đại diện cho 1 palindrome duy nhất. Cạnh \(c\) từ node \(u\) đến node \(v\) nghĩa là: palindrome \(v\) = \(c\) + palindrome \(u\) + \(c\).

Mỗi nút lưu:

  • len: độ dài palindrome
  • link: suffix link (palindrome đối xứng dài nhất là hậu tố đúng của palindrome hiện tại)
  • next[c]: palindrome con khi thêm ký tự \(c\) vào 2 đầu

Trace chi tiết

Xâu: \(S = \text{"abacaba"}\)

Bước Ký tự Palindrome mới tạo Độ dài Suffix link
0 \(a\) \(a\) 1 → node 0 (rỗng)
1 \(b\) \(b\) 1 → node 0
2 \(a\) \(aba\) 3 → node \(a\)
3 \(c\) \(c\) 1 → node 0
4 \(a\) \(aca\) 3 → node \(a\)
5 \(b\) \(bacab\) 5 → node \(b\)
6 \(a\) \(abacaba\) 7 → node \(aba\)

Tổng palindrome phân biệt: \(\{a, b, c, aba, aca, bacab, abacaba\}\) = 7.

Suffix link của node \(u\) là palindrome đối xứng dài nhất đúng là hậu tố của palindrome \(u\).

Ví dụ: suffix link của \(abacaba\)\(aba\) (vì \(aba\) là hậu tố đúng của \(abacaba\) và là palindrome).

flowchart LR N1["a (len=1)"] --> R0["rỗng (len=0)"] N2["b (len=1)"] --> R0 N3["aba (len=3)"] --> N1 N4["c (len=1)"] --> R0 N5["aca (len=3)"] --> N1 N6["bacab (len=5)"] --> N2 N7["abacaba (len=7)"] --> N3

3. Phân tích tính đúng đắn

Tại sao mỗi nút là palindrome duy nhất?

Khi thêm ký tự \(c\) vào 2 đầu palindrome \(P\), ta được palindrome \(cPc\). Nếu \(cPc\) chưa tồn tại trong cây, tạo nút mới.

Suffix link đảm bảo mỗi palindrome chỉ được tạo đúng 1 lần.

Suffix link của palindrome \(P\) luôn ngắn hơn \(P\) (trừ node gốc). Do đó, suffix link tạo thành cây có gốc là node \(0\) (palindrome rỗng).


4. Đánh giá độ phức tạp

Thao tác Thời gian Không gian
Xây Palindrome Tree \(O(N)\) $O(N \cdot
Đếm palindrome phân biệt \(O(N)\) \(O(1)\)
Số palindrome kết thúc tại vị trí \(i\) \(O(1)\) mỗi vị trí \(O(1)\)

\(|\Sigma|\) = kích thước bảng chữ cái.


Code minh họa

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

struct PalindromeTree {
    struct Node {
        int len, link;
        int next[26];
        Node() : len(0), link(0) { memset(next, 0, sizeof(next)); }
    };

    string s;
    vector<Node> tree;
    int last, sz;

    PalindromeTree() {
        tree.resize(2);
        tree[0].len = -1; tree[0].link = 0;
        tree[1].len = 0;  tree[1].link = 0;
        last = 1; sz = 2;
    }

    void extend(int pos) {
        int cur = last;
        int c = s[pos] - 'a';

        while (true) {
            int curlen = tree[cur].len;
            if (pos - 1 - curlen >= 0 && s[pos - 1 - curlen] == s[pos])
                break;
            cur = tree[cur].link;
        }

        if (tree[cur].next[c]) {
            last = tree[cur].next[c];
            return;
        }

        last = sz++;
        tree.push_back(Node());
        tree[last].len = tree[cur].len + 2;
        tree[cur].next[c] = last;

        if (tree[last].len == 1) {
            tree[last].link = 1;
            return;
        }

        cur = tree[cur].link;
        while (true) {
            int curlen = tree[cur].len;
            if (pos - 1 - curlen >= 0 && s[pos - 1 - curlen] == s[pos])
                break;
            cur = tree[cur].link;
        }
        tree[last].link = tree[cur].next[c];
    }

    int countDistinct() {
        return sz - 2; // trừ 2 node gốc
    }
};

int main() {
    string s;
    cin >> s;

    PalindromeTree pt;
    pt.s = s;

    for (int i = 0; i < (int)s.size(); i++) {
        pt.extend(i);
    }

    cout << pt.countDistinct() << "\n";
    return 0;
}
class PalindromeTree:
    def __init__(self):
        self.tree = [{'len': -1, 'link': 0, 'next': {}},
                     {'len': 0, 'link': 0, 'next': {}}]
        self.last = 1
        self.s = ''

    def extend(self, pos):
        c = self.s[pos]
        cur = self.last
        while True:
            curlen = self.tree[cur]['len']
            if pos - 1 - curlen >= 0 and self.s[pos - 1 - curlen] == c:
                break
            cur = self.tree[cur]['link']

        if c in self.tree[cur]['next']:
            self.last = self.tree[cur]['next'][c]
            return

        new_node = len(self.tree)
        self.tree.append({'len': self.tree[cur]['len'] + 2, 'link': 0, 'next': {}})
        self.tree[cur]['next'][c] = new_node
        self.last = new_node

        if self.tree[new_node]['len'] == 1:
            self.tree[new_node]['link'] = 1
            return

        cur = self.tree[cur]['link']
        while True:
            curlen = self.tree[cur]['len']
            if pos - 1 - curlen >= 0 and self.s[pos - 1 - curlen] == c:
                break
            cur = self.tree[cur]['link']
        self.tree[new_node]['link'] = self.tree[cur]['next'][c]

    def count_distinct(self):
        return len(self.tree) - 2

s = input().strip()
pt = PalindromeTree()
pt.s = s
for i in range(len(s)):
    pt.extend(i)
print(pt.count_distinct())

💬 Bình luận