入门博客
- hihocoder - #1441 : 后缀自动机一·基本概念 (很适合入门 · 概念篇)
- 后缀自动机学习笔记 (入门 · 代码详解)
- oiwik - 后缀自动机 (SAM) (对新手不是很友好)
sam线性解决:
在另一个字符串中搜索一个字符串的所有出现位置。
计算给定的字符串中有多少个不同的子串
后缀自动机 (suffix automaton, SAM) 是用于处理单个字符串的子串问题的强力工具。
而广义后缀自动机 (General Suffix Automaton) 则是将后缀自动机整合到字典树中来解决对于多个字符串的子串问题
如何在一个DAG上表示出一个字符串的所有子串?
建立字典树,将字符串的所有后缀加入字典树
这个也不错→ 史上最通俗的后缀自动机详解
自动机上插入 aabab
同时蕴含了前缀(下面最长的那个)和后缀(各种到达节点6、7的路径)
题目
P3804 【模板】后缀自动机 (SAM)
P6139 【模板】广义后缀自动机(广义 SAM)
后缀自动机板子
namespace Sam { // 后缀自动机 板子
/* 注意:
* 当空间限制大小为32768K时 在保存string s[]后执行sam 有MLE的风险 (hdu6208多次测试均无法避免)
* 此时应该用set<string>进行去重
* */
const int M = 1e5 + 10;// 刚好符合字符串大小即可
const int CHAR_NUM = 26;
struct SAM {
struct state {
int len;
int link; // 等同于 father
int cnt; //统计子串出现的次数 不用时可以关闭以 便于节省空间
vector<int> nxt;
state() { nxt = vector<int>(CHAR_NUM, 0); }
} st[M * 2];
int sz;//自动机的大小 同时也是当前状态的编号 终止状态为-1
int last;// 未加入下一个字符前最长的前缀(整个串)所属的节点的编号
/*
* 一些我自己对于sam的理解:
* 从根节点 沿nxt数组 到编号为 i 的节点 一条路径 形成原字符串里的 一个后缀
* 一条字符串 root -> link[i] 是字符串 root -> i 的 一个后缀
*
* len[] 这个东西 比较玄学
* len[i]表示 第i个状态 能表示的一些子串里子串的最长长度
*
* 而 第i个状态 能表示的子串 和 从根节点到第i个状态 的走法相关
* 一个走法 就是一个子串
* 其中走法最远的 就是 len[i] 的值
* 但是走法的个数 不等于 状态i能表示子串的个数
*
* 并非每一个字母都对应一个状态点 它会被拆成多个状态
* 比如 当我插入 aabab 这么一个字符串 (就上面那张图)
* 插入最后一个b 会产生两个状态 编号6 编号7
* 打表确认 len[6]=5 (通过图可以发现 这个状态对应的子串有:aabab、bab、abab)
* len[7]=2 (这个状态对应的子串有:b、ab)
*
* len[i] - len[link[i]]
* 每个状态i都对应一些子串 之前提到过 link[i] 对应的字符串 是 i 对应的字符串的后缀
* len[i]表示i对应的子串的最长长度 (一个状态里不可能存在相同长度的后缀)
* 显然 len[i] - len[link[i]] = i对应的所有后缀子串 - i里被link[i]占据的那些后缀部分子串
* = 状态i相对于状态link[i] 新增加的后缀子串的个数
* */
void clear(vector<int> &a) {
for (int i = 0; i < CHAR_NUM; i++) {
a[i] = 0;
}
}
void init() {
// 创建只有一个初始状态的sam
// 编号为0 对应字符串="" (空串)
st[0].len = 0;
st[0].link = -1;
clear(st[0].nxt);
sz = 1;
last = 0;
}
// 普通插入
void insert(int c) {
int cur = sz++;
st[cur].len = st[last].len + 1;
//clear(st[cur].nxt);
for (int i = 0; i < CHAR_NUM; i++) {
st[cur].nxt[i] = 0;
}
st[cur].link = -1;
int p = last;
while (p != -1 && !st[p].nxt[c]) {
st[p].nxt[c] = cur;
p = st[p].link;
}
if (p == -1) { // 如果到达虚拟状态
st[cur].link = 0; // 连接到初始状态
} else {
int q = st[p].nxt[c];
if (st[p].len + 1 == st[q].len) {
st[cur].link = q;
} else {
int clone = sz++;//复制
st[clone].len = st[p].len + 1;
st[clone].nxt = st[q].nxt;
st[clone].link = st[q].link;
while (p != -1 && st[p].nxt[c] == q) {
st[p].nxt[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}
/*
* sam的常见应用
* 1. 检查字符串是否出现过
* 2. 不同子串个数
* 3. 所有不同子串的总长度
* 4. 字典序第k大子串
* 5. 字符串s的最小循环位移
* 6. 出现次数
* 7. 第一次出现的位置
* 8. 所有出现的位置
* 9. 最短的没有出现的字符串
* 10. 两个字符串的最长公共子串
* 11. 多个字符串间的最长公共子串
* */
// 1.判断子串s是否在文本串里出现过
bool find(string s) {
int len = s.length();
int now = 0;
for (int i = 0; i < len; i++) {
if (!st[now].nxt[s[i] - 'a']) return false;
now = st[now].nxt[s[i] - 'a'];
}
return true;
}
// 统计 至少出现了k次的本质不同的子串的个数
// 统计 恰好出现了k次的本质不同的子串的个数
// = 至少出现了k次的本质不同的子串的个数 - 至少出现了k+1次的本质不同的子串的个数
ll insert(int c, int k) {
int cur = sz++;
st[cur].cnt = 0;
st[cur].len = st[last].len + 1;
clear(st[cur].nxt);
st[cur].link = -1;
int p = last;
while (p != -1 && !st[p].nxt[c]) {
st[p].nxt[c] = cur;
p = st[p].link;
}
if (p == -1) {
st[cur].link = 0;
} else {
int q = st[p].nxt[c];
if (st[p].len + 1 == st[q].len) {
st[cur].link = q;
} else {
int clone = sz++;//复制
st[clone].len = st[p].len + 1;
st[clone].nxt = st[q].nxt;
st[clone].link = st[q].link;
st[clone].cnt = st[q].cnt;
while (p != -1 && st[p].nxt[c] == q) {
st[p].nxt[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
// 此时 从last一路沿着link走 走到的所有点都是某些后缀的终止状态
int now = last;
ll res = 0;
while (now != -1) {
if (st[now].cnt >= k) { // 之前统计过了 就没必要被统计
break;
}
st[now].cnt++;
if (st[now].cnt == k) {
res += st[now].len - st[st[now].link].len;
}
now = st[now].link;
}
return res;
}
} sam;
}
using namespace Sam;
广义后缀自动机板子
namespace Gsa { // 广义后缀自动机 板子
typedef long long ll;
typedef pair<int, int> pii;
const int M = 2e6 + 10;
const int CHAR_NUM = 26 + 10;
// 广义后缀自动机
struct GSA {
int len[M];//节点长度
int link[M];// 后缀连接
int nxt[M][CHAR_NUM];
int tot;//节点总数
public:
//初始化
void init() {
tot = 1;
link[0] = -1;
}
// 插入文本
void insert(const string &s) {
int root = 0;
for (auto ch:s) {
root = insertTrie(root, ch - 'a');
}
}
void insert(const char *s, int n) {
int root = 0;
for (int i = 0; i < n; i++) {
root = insertTrie(root, s[i] - 'a');
}
}
// 根据建立的字典树 构建广义后缀自动机
// 这样建立广义后缀自动机会破坏原来的字典树
// 可以考虑 将原来的字典树备份到另一颗树上
void build() {
queue<pii> q;
for (int i = 0; i < CHAR_NUM; i++) {
if (nxt[0][i]) q.push({i, 0});
}
while (!q.empty()) {
auto item = q.front();
q.pop();
auto last = insertSAM(item.second, item.first);
for (int i = 0; i < CHAR_NUM; i++) {
if (nxt[last][i])
q.push({i, last});
}
}
}
private:
int insertTrie(int cur, int c) {
if (nxt[cur][c]) return nxt[cur][c];
return nxt[cur][c] = tot++;
}
int insertSAM(int last, int c) {
int cur = nxt[last][c];
len[cur] = len[last] + 1;
int p = link[last];
while (p != -1) {
if (!nxt[p][c]) {
nxt[p][c] = cur;
} else {
break;
}
p = link[p];
}
if (p == -1) {
link[cur] = 0;
return cur;
}
int q = nxt[p][c];
if (len[p] + 1 == len[q]) {
link[cur] = q;
return cur;
}
int clone = tot++;
for (int i = 0; i < CHAR_NUM; i++) {
nxt[clone][i] = (len[nxt[q][i]] != 0 ? nxt[q][i] : 0);
}
len[clone] = len[p] + 1;
while (p != -1 && nxt[p][c] == q) {
nxt[p][c] = clone;
p = link[p];
}
link[clone] = link[q];
link[cur] = clone;
link[q] = clone;
return cur;
}
} gsa;
string s;
//n个由小写字母组成的字符串里出现的本质不同的子串个数 不包含空串
void solve_P6139() {
int n;
cin >> n;
gsa.init();
for (int i = 1; i <= n; i++) {
cin >> s;
gsa.insert(s);
}
gsa.build();
// 根据后缀自动机的性质 以点i为节点的子串的个数 = len[i]-len[link[i]]
ll ans = 0;
for (int i = 1; i < gsa.tot; i++) {
ans += gsa.len[i] - gsa.len[gsa.link[i]];
}
cout << ans << endl;
}
}
using namespace Gsa;