后缀自动机模板题详解:从算法思路到代码实现(以 P3804 为例)

后缀自动机

下面是笔者总结的知识点,仅供参考~
在这里插入图片描述
在这里插入图片描述

例题P3804【模板】后缀自动机

在这里插入图片描述

输入样例

abab

输出样例

4
算法思路

问题分析
题目要求找出字符串所有出现次数大于1的子串中,出现次数与子串长度的乘积的最大值。
关键挑战:高效处理大字符串(长度达1e6),需线性或近似线性复杂度。

后缀自动机(SAM) 是解决此类问题的理想数据结构:

  1. SAM特性
    • 每个状态代表一组endpos相同的子串。
    • 状态间通过link指针形成树结构(parent树)。
  2. 核心步骤
    • 构建SAM,记录每个状态的最长子串长度(len)和出现次数(size)。
    • 利用拓扑排序累加出现次数。
    • 遍历所有状态计算最大值。
代码示例
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;

// 定义状态结构体,用于存储后缀自动机的每个状态信息
struct State {
    int len;    // 该状态对应的最长子串长度
    int link;   // 后缀链接指针,指向另一个状态,该状态代表的子串是当前状态所代表子串的后缀
    int trans[26]; // 字符转移表,存储从当前状态通过每个小写字母可以转移到的下一个状态
    int size;   // 该状态所代表的子串集合在原字符串中出现的次数
};

const int MAXN = 2e6 + 10; // 根据题目规模预设足够大的数组,用于存储后缀自动机的状态
State sam[MAXN];
int sz, last; // sz为状态总数,last为当前最后状态

// 后缀自动机初始化函数
void sam_init() {
    // 初始化起始状态
    sam[0].len = 0; // 起始状态对应的最长子串长度为 0,即空串
    sam[0].link = -1; // 起始状态没有后缀链接,用 -1 表示
    memset(sam[0].trans, 0, sizeof(sam[0].trans)); // 起始状态的转移表初始化为 0,表示没有转移
    sam[0].size = 0; // 起始状态对应的子串(空串)出现次数为 0
    sz = 1; // 初始时只有起始状态,状态总数为 1
    last = 0; // 当前最后状态为起始状态
}

// 后缀自动机扩展函数,用于添加一个新字符
void sam_extend(int c) {
    int cur = sz++; // 创建一个新状态,状态总数加 1
    sam[cur].len = sam[last].len + 1; // 新状态对应的最长子串长度为上一个状态的长度加 1
    sam[cur].size = 1; // 新状态对应的子串至少出现一次,初始出现次数为 1
    memset(sam[cur].trans, 0, sizeof(sam[cur].trans)); // 新状态的转移表初始化为 0

    int p = last; // 从当前最后状态开始
    // 沿着后缀链接向上查找,若某个状态没有通过字符 c 的转移,则添加转移到 cur
    while (p != -1 && !sam[p].trans[c]) {
        sam[p].trans[c] = cur;
        p = sam[p].link;
    }

    if (p == -1) {
        // 若没有找到合适的状态,新状态的后缀链接指向起始状态 0
        sam[cur].link = 0;
    } else {
        int q = sam[p].trans[c];
        if (sam[p].len + 1 == sam[q].len) {
            // 若满足条件,新状态的后缀链接指向 q
            sam[cur].link = q;
        } else {
            int clone = sz++; // 克隆一个新状态
            sam[clone] = sam[q]; // 复制 q 的信息到克隆状态
            sam[clone].len = sam[p].len + 1; // 调整克隆状态的最长子串长度
            sam[clone].size = 0; // 克隆状态的初始出现次数为 0

            // 更新相关状态的转移,将通过字符 c 转移到 q 的状态改为转移到克隆状态
            while (p != -1 && sam[p].trans[c] == q) {
                sam[p].trans[c] = clone;
                p = sam[p].link;
            }
            // 更新 q 和 cur 的后缀链接指向克隆状态
            sam[q].link = clone;
            sam[cur].link = clone;
        }
    }
    last = cur; // 更新当前最后状态为新状态
}

int main() {
    ios::sync_with_stdio(false); // 关闭输入输出流的同步,提高输入输出效率
    cin.tie(nullptr);

    string s;
    cin >> s; // 输入字符串
    sam_init(); // 初始化后缀自动机

    // 遍历字符串的每个字符,调用扩展函数构建后缀自动机
    for (char ch : s) {
        sam_extend(ch - 'a');
    }

    // 按 len 排序,用于拓扑排序
    vector<int> order(sz);
    for (int i = 0; i < sz; ++i) order[i] = i;
    // 按状态的 len 从大到小对状态编号进行排序
    sort(order.begin(), order.end(), [&](int a, int b) {
        return sam[a].len > sam[b].len;
    });

    // 累加出现次数
    // 按照拓扑排序的顺序,将每个状态的出现次数累加到其后缀链接指向的状态上
    for (int u : order) {
        if (sam[u].link != -1) {
            sam[sam[u].link].size += sam[u].size;
        }
    }

    long long ans = 0;
    // 遍历所有状态(跳过起始状态),计算满足条件的最大结果
    for (int i = 1; i < sz; ++i) { 
        if (sam[i].size > 1) {
            // 若状态的出现次数大于 1,计算其 len 乘以 size 的结果,并取最大值
            ans = max(ans, (long long)sam[i].len * sam[i].size);
        }
    }

    cout << ans << endl; // 输出最终结果

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值