后缀自动机
下面是笔者总结的知识点,仅供参考~
例题P3804【模板】后缀自动机
输入样例
abab
输出样例
4
算法思路
问题分析:
题目要求找出字符串所有出现次数大于1的子串中,出现次数与子串长度的乘积的最大值。
关键挑战:高效处理大字符串(长度达1e6),需线性或近似线性复杂度。
后缀自动机(SAM) 是解决此类问题的理想数据结构:
- SAM特性:
- 每个状态代表一组endpos相同的子串。
- 状态间通过link指针形成树结构(parent树)。
- 核心步骤:
- 构建SAM,记录每个状态的最长子串长度(
len
)和出现次数(size
)。 - 利用拓扑排序累加出现次数。
- 遍历所有状态计算最大值。
- 构建SAM,记录每个状态的最长子串长度(
代码示例
#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;
}