后缀自动机
题目描述
核心思路
考虑如何求出 “子串的出现数量”
结论:子串substr出现的数量=|endpos(substr)|
也就是说,子串substr出现的数量,其实就是endpos(substr)这个集合中的元素的个数
如下图感性理解一下:
如上图所示,设原串为 a b c a abca abca,我们想要求出子串a的出现数量,由原串一眼就看出子串a出现了两次。但是我们观察endpos发现, e n d p o s ( " a " ) = [ 1 , 4 ] endpos("a")=[1,4] endpos("a")=[1,4],这个集合中的元素个数为2,这刚好就说明了子串a出现了两次。
接下来就要考虑怎么才能求出某个endpos集合的大小呢?
这里要注意,其实endpos集合只与后缀自动机中的后缀链接link有关,因此我们建图时,就只需要把含有link的边建立出来就好了。
如上图所示,我们发现一个性质:叶子节点endpos的并集就构成了它们父节点的endpos中的元素。但是要注意有可能父节点是有属于自己特有的元素的。因此我们要计算endpos[u]
的大小时,就先计算自己特有的元素的大小endpos[u]
,然后再计算它的子节点的元素大小endpos[v]
,那么相加,就会得到状态节点
u
u
u的endpos集合的大小
∣
e
n
d
p
o
s
(
u
)
∣
|endpos(u)|
∣endpos(u)∣
虽然再后缀自动机中,我们是从子节点向父节点引了后缀链接边,但是在这题中,我们建图时,我们是从父节点向子节点建立一条有向边。为什么呢?因为这样当我们进行dfs时,从根节点开始,一直递归到叶子节点,然后统计出了叶子节点 v v v的endpos集合大小 e n d p o s ( v ) endpos(v) endpos(v),那么在回溯时就会累加到其父节点 u u u,那么就可以算出父节点 u u u的endpos集合大小 e n d p o s ( u ) endpos(u) endpos(u)了
代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
//N是后缀自动机中的状态节点个数 要开2倍 M是建立出只含link后缀链接时图中边的总数
const int N = 2e6+10,M = N;
//tot记录的是后缀自动机中的状态节点 初始化为根节点1
//last记录的是上一个状态节点
int tot = 1, last = 1;
struct Node
{
int len; //记录这个状态节点所形成的串中最大的那个串的长度
int link; //后缀链接
int ch[26]; //类似于trie树中的孩子
}node[N];
char str[N];
//endpos集合 ans是答案
LL endpos[N], ans;
int h[N], e[M], ne[M], idx;
//后缀自动机模板
void extend(int c)
{
//先用p来记录上一个状态节点
//然后由于来了一个字符c,需要进行状态转移了
// 因此给转移后得到的节点np分配一个编号tot
int p = last, np = last = ++ tot;
//以tot这个位置结尾形成串它自己就是一个前缀
endpos[tot] = 1;
//由于np是从p通过新添一个字符c转移过去的 因此长度+1
node[np].len = node[p].len + 1;
//沿着p的后缀链接遍历节点 如果遍历到的节点p它没有孩子c
//则要创建出来 让np连一条后缀链接边到node[p].ch[c]就相当于
//该节点p通过字符c转移到了np
for (; p && !node[p].ch[c];p = node[p].link)
node[p].ch[c] = np;
//沿着后缀链接一直来到了根节点 仍然没有发现
//那么np的后缀链接就是根节点
if (!p)
node[np].link = 1;
else
{
int q = node[p].ch[c]; //找到状态节点p的c孩子节点 q
//np沿着后缀链接找到了q 发现q中有np想要的串 能够进行后缀邻接
//那么此时nq就可以引出一条后缀链接边到q
if (node[q].len == node[p].len + 1)
node[np].link = q;
else
{
//将q一分为二 变成q 和 nq
int nq = ++ tot;
//nq是q克隆出来了 但是nq包含了从q中抽离出来的能够让np进行后缀链接的串
node[nq] = node[q], node[nq].len = node[p].len + 1;
//分裂前的q引一条后缀链接边到nq
//从新开的状态节点np引一条后缀链接边到nq
node[q].link = node[np].link = nq;
//沿着p的后缀链接一直往回走 将所遍历到的节点 都 引一条后缀链接边到nq
for (; p && node[p].ch[c] == q; p = node[p].link)
node[p].ch[c] = nq;
}
}
}
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
for (int i = h[u]; ~i; i = ne[i])
{
int v=e[i];
dfs(v);
//当前状态节点u的endpos就等于它自身特有的endpos[u]加上它的孩子节点的endpos[v]
//比如u={1,2,3,4,5} v1={1,2},v2={3,4} 那么它特有的就是{5}
//所以u的endpos集合中元素的个数 就是 它自己特有的{5} 加上 |v1|+|v2|
//一共就是5个 即endpos[u]的元素个数为5
endpos[u] += endpos[v];
}
//出现次数不为 1
if (endpos[u] > 1)
ans = max(ans, endpos[u] * node[u].len);
}
int main()
{
scanf("%s", str+1);
for (int i = 1; str[i]; i ++ )
extend(str[i] - 'a');
memset(h, -1, sizeof h);
//虽然后缀自动机中后缀链接是从子节点引向父节点
//但是我们这里是建边时是从父节点引向子节点 因为这样的话
//我们在dfs时是从父节点递归到子节点 然后统计子节点的endpos值
//回溯时累加到其父节点的endpos上 这样就刚好满足 "父节点是它子节点的并集" 的性质
for (int i = 2; i <= tot; i ++ )
add(node[i].link, i);
dfs(1); //从后缀状态机的根节点开始深搜
printf("%lld\n", ans);
return 0;
}