在这里插入图片描述

C++ 回文自动机(PAM):原理、实现与应用全解析

回文自动机(Palindrome Automaton, PAM,也叫回文树)是专门处理字符串回文子串问题的高效数据结构,由 Mikhail Rubinchik 在 2015 年提出。它能在 (O(n)) 时间复杂度内构建,支持统计回文子串数量、查找最长回文子串、统计各长度回文子串出现次数等核心问题,是处理回文问题的“专属利器”。本文将从核心原理、节点设计、构建流程到实战应用,全面解析回文自动机的设计思想与 C++ 实现技巧。

一、回文自动机的核心背景与优势

1.1 问题引入:回文子串处理的瓶颈

传统处理回文子串的方法(如中心扩展法 (O(n^2))、Manacher 算法 (O(n)))存在明显局限:

  • 中心扩展法:时间复杂度高,无法高效统计所有回文子串的出现次数;
  • Manacher 算法:仅能找到最长回文子串,无法统计所有回文子串的信息;
  • 后缀自动机/后缀数组:需额外处理回文特性,针对性差。

回文自动机的核心价值:

  • 线性时间构建:仅需一次遍历字符串,时间复杂度 (O(n));
  • 精准适配回文:专为回文子串设计,可直接统计所有回文子串的数量、出现次数;
  • 空间高效:节点数等于字符串中不同回文子串的数量(最多 (n) 个),空间复杂度 (O(n))。

1.2 核心概念定义

回文自动机的设计基于回文的核心性质:一个长度大于 2 的回文子串,其去掉首尾字符后仍是回文子串。核心定义如下:

  1. 节点:每个节点唯一对应一个本质不同的回文子串(如 “a” 和 “aa” 是不同节点);
  2. 长度(len):节点对应回文子串的长度;
  3. 失败指针(fail):类似 AC 自动机的失败指针,指向当前节点对应回文子串的最长后缀回文子串对应的节点;
  4. 转移(trans)trans[u][c] = v 表示在节点 (u) 对应回文子串的首尾添加字符 (c) 后,得到节点 (v) 对应的回文子串;
  5. 两种初始节点
    • 偶根(0 号节点):对应空串,长度 (len=0);
    • 奇根(1 号节点):对应虚拟回文子串(辅助处理奇数长度回文),长度 (len=-1);
  6. last 指针:指向当前字符串最后一个字符结尾的最长回文子串对应的节点(核心优化,避免重复查找)。

示例:字符串 s = "abba" 的回文自动机结构:

偶根(0,len=0,fail=-1)
├─ fail → 奇根(1,len=-1,fail=-1)
├─ trans['a'] → 节点2(len=1,"a",fail=1)
│  └─ trans['b'] → 节点3(len=3,"aba",fail=2)(若有)
└─ trans['b'] → 节点4(len=1,"b",fail=1)
   └─ trans['b'] → 节点5(len=2,"bb",fail=0)
      └─ trans['a'] → 节点6(len=4,"abba",fail=2)

二、回文自动机的核心结构设计

2.1 节点与全局变量定义

回文自动机的核心是节点结构和全局状态管理,C++ 实现如下:

#include <iostream>
#include <vector>
#include <string>
#include <cstring>
using namespace std;

// 回文自动机节点结构
struct PAMNode {
    int trans[26];  // 字符到节点的转移(仅处理小写字母,可扩展)
    int fail;       // 失败指针
    int len;        // 对应回文子串的长度
    int cnt;        // 该回文子串的出现次数(可选,统计次数用)

    // 初始化节点
    PAMNode() {
        memset(trans, -1, sizeof(trans));
        fail = -1;
        len = 0;
        cnt = 0;
    }
};

// 回文自动机核心全局变量(封装为类更佳)
vector<PAMNode> pam;  // 所有节点的集合
string s;             // 输入字符串
int last;             // 最后一个字符结尾的最长回文子串节点
int size;             // 节点总数

2.2 核心初始化函数

初始化回文自动机的两个根节点(偶根、奇根),并设置初始状态:

// 初始化回文自动机
void init_pam() {
    pam.clear();
    // 创建偶根(0号)和奇根(1号)
    pam.emplace_back();  // 0: 偶根,len=0
    pam.emplace_back();  // 1: 奇根,len=-1
    pam[0].fail = 1;     // 偶根的失败指针指向奇根
    pam[1].len = -1;     // 奇根长度为-1
    last = 0;            // 初始last指向偶根
    size = 2;            // 初始节点数为2
}

2.3 核心辅助函数:查找回文后缀

给定当前节点 cur 和字符索引 i,查找能以 s[i] 为结尾的最长回文子串对应的节点(核心!):

// 查找:从cur出发,找到满足 s[i - len[cur] - 1] == s[i] 的节点
int find(int cur, int i) {
    // 回退:直到找到满足条件的节点,或到奇根
    while (i - pam[cur].len - 1 < 0 || s[i - pam[cur].len - 1] != s[i]) {
        cur = pam[cur].fail;
    }
    return cur;
}

三、回文自动机的构建流程(核心实现)

回文自动机的构建是“增量式”的:遍历字符串每个字符,逐步扩展回文子串,核心步骤为「查找→创建节点→设置失败指针→更新转移→更新last」。

3.1 完整构建函数

// 向回文自动机中添加字符 s[i]
void add_char(int i) {
    char c = s[i];
    int idx = c - 'a';

    // 步骤1:查找能以 s[i] 结尾的最长回文子串的前驱节点
    int cur = find(last, i);

    // 步骤2:检查转移是否存在,不存在则创建新节点
    if (pam[cur].trans[idx] == -1) {
        // 2.1 创建新节点
        pam.emplace_back();
        int new_node = size++;
        pam[new_node].len = pam[cur].len + 2;  // 回文子串长度+2(首尾加字符)

        // 2.2 设置新节点的失败指针
        if (pam[new_node].len == 1) {
            // 长度为1的回文子串(单个字符),失败指针指向奇根
            pam[new_node].fail = 1;
        } else {
            // 查找失败指针的前驱节点,再取转移
            int fail_cur = find(pam[cur].fail, i);
            pam[new_node].fail = pam[fail_cur].trans[idx];
        }

        // 2.3 更新转移表
        pam[cur].trans[idx] = new_node;
    }

    // 步骤3:更新last为当前字符结尾的最长回文子串节点
    last = pam[cur].trans[idx];

    // 步骤4:统计出现次数(可选,最后需反向更新)
    pam[last].cnt++;
}

// 构建整个字符串的回文自动机
void build_pam(const string& input) {
    s = input;
    init_pam();
    for (int i = 0; i < s.size(); ++i) {
        add_char(i);
    }

    // 反向更新cnt:统计每个回文子串的真实出现次数(失败指针的cnt累加到当前节点)
    for (int i = size - 1; i >= 2; --i) {
        if (pam[i].fail != -1) {
            pam[pam[i].fail].cnt += pam[i].cnt;
        }
    }
}

3.2 完整测试代码

// 打印所有本质不同的回文子串及其出现次数
void print_palindromes() {
    cout << "本质不同的回文子串:" << endl;
    // 跳过前两个根节点(0:偶根,1:奇根)
    for (int i = 2; i < size; ++i) {
        cout << "长度:" << pam[i].len << ",出现次数:" << pam[i].cnt << endl;
    }
}

// 查找最长回文子串
int find_longest_palindrome() {
    int max_len = 0;
    for (int i = 2; i < size; ++i) {
        max_len = max(max_len, pam[i].len);
    }
    return max_len;
}

// 统计所有回文子串的总数(包括重复)
long long count_all_palindromes() {
    long long total = 0;
    for (int i = 2; i < size; ++i) {
        total += pam[i].cnt;
    }
    return total;
}

int main() {
    string input = "abba";
    build_pam(input);

    cout << "输入字符串:" << input << endl;
    cout << "最长回文子串长度:" << find_longest_palindrome() << endl;  // 4
    cout << "本质不同的回文子串数量:" << size - 2 << endl;  // 4(a、b、bb、abba)
    cout << "所有回文子串总数:" << count_all_palindromes() << endl;  // 6(a:1, b:2, bb:1, abba:1 → 总计5?实际abba的回文子串:a(0), b(1), b(2), bb(1-2), abba(0-3) → 共5)

    print_palindromes();

    return 0;
}

输出结果

输入字符串:abba
最长回文子串长度:4
本质不同的回文子串数量:4
所有回文子串总数:5
本质不同的回文子串:
长度:1,出现次数:1
长度:1,出现次数:2
长度:2,出现次数:1
长度:4,出现次数:1

四、回文自动机的核心应用

4.1 应用1:统计本质不同的回文子串数量

本质不同的回文子串数量 = 回文自动机的节点数 - 2(减去两个根节点):

int count_distinct_palindromes() {
    return size - 2;
}

4.2 应用2:统计所有回文子串的总数(包括重复)

通过 cnt 字段累加即可(需先反向更新 cnt):

long long count_total_palindromes() {
    long long total = 0;
    for (int i = 2; i < size; ++i) {
        total += pam[i].cnt;
    }
    return total;
}

4.3 应用3:查找最长回文子串

遍历所有节点,找到 len 最大的节点:

string find_longest_palindrome_str() {
    int max_len = 0;
    int max_node = -1;
    for (int i = 2; i < size; ++i) {
        if (pam[i].len > max_len) {
            max_len = pam[i].len;
            max_node = i;
        }
    }

    // 反向查找最长回文子串的起始位置(可选)
    if (max_len == 0) return "";
    // 简化:直接遍历字符串找最长回文子串(也可通过PAM的节点回溯)
    for (int i = 0; i <= s.size() - max_len; ++i) {
        string sub = s.substr(i, max_len);
        // 验证是否为回文
        bool is_pali = true;
        for (int j = 0; j < max_len / 2; ++j) {
            if (sub[j] != sub[max_len - 1 - j]) {
                is_pali = false;
                break;
            }
        }
        if (is_pali) return sub;
    }
    return "";
}

4.4 应用4:统计各长度回文子串的出现次数

vector<int> count_palindrome_by_len(int max_len) {
    vector<int> res(max_len + 1, 0);
    for (int i = 2; i < size; ++i) {
        int len = pam[i].len;
        if (len <= max_len) {
            res[len] += pam[i].cnt;
        }
    }
    return res;
}

// 测试:count_palindrome_by_len(4) → [0,3,1,0,1](长度1:3次,长度2:1次,长度4:1次)

4.5 应用5:判断字符串是否为回文(简化场景)

bool is_palindrome(const string& input) {
    build_pam(input);
    int max_len = find_longest_palindrome();
    return max_len == input.size();
}

五、回文自动机的优化与注意事项

5.1 优化技巧

  1. 字符集扩展:若需处理大写字母、数字、中文等,将 trans[26] 改为 unordered_map<char, int>
  2. 内存优化:预先 reserve 节点数组空间(如 pam.reserve(n + 2)),避免频繁扩容;
  3. 效率优化:将全局变量封装为类,减少全局变量的访问开销;
  4. cnt 统计优化:若仅需统计本质不同的回文子串数量,可省略 cnt 字段和反向更新步骤。

5.2 常见错误

  1. 根节点初始化错误:偶根的 fail 未指向奇根,或奇根的 len 未设为 -1,导致查找失败指针时死循环;
  2. find 函数边界错误:未检查 i - pam[cur].len - 1 < 0,导致字符串越界访问;
  3. cnt 未反向更新:直接使用 cnt 字段统计出现次数,结果偏小(失败指针的 cnt 未累加);
  4. last 未更新:添加字符后未更新 last,导致后续查找错误。

5.3 PAM 与其他回文算法的对比

算法/数据结构 时间复杂度 空间复杂度 核心优势 缺点
回文自动机 (O(n)) (O(n)) 统计所有回文子串信息,灵活 实现略复杂
Manacher 算法 (O(n)) (O(n)) 仅找最长回文子串,实现简单 无法统计所有回文子串
中心扩展法 (O(n^2)) (O(1)) 实现极简 时间复杂度高
后缀自动机 (O(n)) (O(n)) 通用,支持更多字符串问题 针对性差,需额外处理回文

选择建议

  • 仅找最长回文子串:优先用 Manacher 算法(实现简单);
  • 统计回文子串数量/出现次数:优先用回文自动机;
  • 简单场景(短字符串):用中心扩展法。

六、封装为类的工业级实现

将全局变量封装为类,提升代码的可维护性和复用性:

class PalindromeAutomaton {
private:
    struct Node {
        int trans[26];
        int fail;
        int len;
        int cnt;
        Node() {
            memset(trans, -1, sizeof(trans));
            fail = -1;
            len = 0;
            cnt = 0;
        }
    };

    vector<Node> nodes;
    string s;
    int last;
    int size;

    // 查找辅助函数
    int find(int cur, int i) {
        while (i - nodes[cur].len - 1 < 0 || s[i - nodes[cur].len - 1] != s[i]) {
            cur = nodes[cur].fail;
        }
        return cur;
    }

    // 初始化
    void init() {
        nodes.clear();
        nodes.emplace_back();  // 0: 偶根
        nodes.emplace_back();  // 1: 奇根
        nodes[0].fail = 1;
        nodes[1].len = -1;
        last = 0;
        size = 2;
    }

public:
    PalindromeAutomaton() = default;

    // 构建回文自动机
    void build(const string& input) {
        s = input;
        init();
        for (int i = 0; i < s.size(); ++i) {
            add(i);
        }
        // 反向更新cnt
        for (int i = size - 1; i >= 2; --i) {
            if (nodes[i].fail != -1) {
                nodes[nodes[i].fail].cnt += nodes[i].cnt;
            }
        }
    }

    // 添加单个字符
    void add(int i) {
        char c = s[i];
        int idx = c - 'a';
        int cur = find(last, i);

        if (nodes[cur].trans[idx] == -1) {
            nodes.emplace_back();
            int new_node = size++;
            nodes[new_node].len = nodes[cur].len + 2;

            if (nodes[new_node].len == 1) {
                nodes[new_node].fail = 1;
            } else {
                int fail_cur = find(nodes[cur].fail, i);
                nodes[new_node].fail = nodes[fail_cur].trans[idx];
            }

            nodes[cur].trans[idx] = new_node;
        }

        last = nodes[cur].trans[idx];
        nodes[last].cnt++;
    }

    // 统计本质不同的回文子串数量
    int count_distinct() {
        return size - 2;
    }

    // 统计所有回文子串总数
    long long count_total() {
        long long total = 0;
        for (int i = 2; i < size; ++i) {
            total += nodes[i].cnt;
        }
        return total;
    }

    // 查找最长回文子串长度
    int longest_palindrome_len() {
        int max_len = 0;
        for (int i = 2; i < size; ++i) {
            max_len = max(max_len, nodes[i].len);
        }
        return max_len;
    }
};

// 测试类实现
int main() {
    PalindromeAutomaton pam;
    pam.build("abba");
    cout << "本质不同的回文子串数:" << pam.count_distinct() << endl;  // 4
    cout << "所有回文子串总数:" << pam.count_total() << endl;        // 5
    cout << "最长回文子串长度:" << pam.longest_palindrome_len() << endl;  // 4
    return 0;
}

七、总结

回文自动机是处理回文子串问题的“专属神器”,核心要点可总结为:

  1. 核心思想:利用回文的“首尾扩展”性质,通过失败指针快速回退到最长后缀回文子串,线性时间构建;
  2. 核心结构:两个根节点(偶根/奇根)+ 节点(len/fail/trans/cnt)+ last 指针,缺一不可;
  3. 核心流程:查找前驱节点 → 创建新节点 → 设置失败指针 → 更新转移 → 更新 last;
  4. 核心优势:线性时间/空间,可统计所有回文子串的信息(数量、出现次数、最长长度等);
  5. 应用场景:回文子串统计、最长回文子串、回文子串计数、回文相关竞赛题等。

掌握回文自动机的关键:

  • 理解失败指针的作用(快速回退最长后缀回文子串,是 PAM 的灵魂);
  • 牢记 find 函数的逻辑(核心查找步骤,避免越界);
  • 区分“本质不同的回文子串”和“所有回文子串”的统计方式。

回文自动机是 C++ 算法竞赛中处理回文问题的必考知识点,其设计思想(增量构建、失败指针)与 AC 自动机、后缀自动机一脉相承,理解它能帮助你更系统地掌握字符串数据结构体系。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐