2766. 后缀自动机
题目链接
2766. 后缀自动机
给定一个长度为 \(n\) 的只包含小写字母的字符串 \(S\)。
对于所有 \(S\) 的出现次数不为 \(1\) 的子串,设其 \(value\) 值为该子串出现的次数 \(×\) 该子串的长度。
请计算,\(value\) 的最大值是多少。
输入格式
共一行,包含一个由 \(n\) 个小写字母构成的字符串。
输出格式
共一行,输出一个整数,表示答案。
数据范围
\(1≤n≤10^6,\)
保证至少存在一个子串出现次数大于 \(1\)。
输入样例:
aabab
输出样例:
4
解题思路
后缀自动机(SAM)
后缀自动机是一种能够恰好将一个字符串的所有子串存储下来的数据结构,且其中含有的节点数量为 \(2n-1\) 个,含有的边数为 \(3n-4\) 条,其中关键在于 \(endpos(s)\),其表示子串 \(s\) 的尾字符出现的所有位置,而后缀自动机中的每一个节点状态表示所有等于 \(endpos(s)\) 的子串,即 \(endpos\) 的等价类,其中最为关键的性质:后缀自动机的每个节点状态包含一段连续的子串,即其他串都是最长串的连续后缀,即串的长度依次递减
另外,其关键还有两条边:
后缀自动机有一个源点(表示空串)和一个汇点(表示到这个点的所有字符形成的后缀),其中,蓝边表示的是一个有向无环图,绿边表示的是一棵树,蓝边的含义类似于字典树,即新状态是通过旧状态通过字母连边形成的,绿边的含义为当前节点中的最短子串去掉第一个字母后形成的某个状态的最长子串,可以看出,所有绿边连向的所有节点形成的状态都是连续的,即某一个状态表示一段的子串,则其连向的状态表示上一个状态首字母的又一连续子串,即连向的状态中的子串长度在递减
构造过程:采用增量的方式,即一个一个字符插入,后缀自动机中维护三个信息:绿边的父节点,其最长子串的长度,其通过字母连向的其他状态节点(蓝边)。每次有新字母加进来时,设置一个新状态,其最长长度为上一个状态最长长度加一(因为有新字母加入,旧状态向新状态转移),同时由于加入新字母,对于前一个状态绿边连向的节点,由于绿边连向的节点每次变化的都是前面的字符,有新字符加入,这些节点连向新状态节点,表示新的状态节点包含到达该节点的所有子串,如果通过绿边到了空串表示的位置,则当前节点能表示的最小子串为新加入的字符,则其绿边应该指向空串表示的位置,否则,分为两种情况:(图片来源于网路)
1.
对于新加入的字符 \(A\),遍历到 \(A\) 时由于 \(AA\) 已经存在,故为了满足后缀自动机不重的性质,新状态不应该包含 \(AA\),此时当前状态节点的绿边应该直接连向 \(AA\) 表示的状态节点,\(\color{red}{什么时候当前状态节点的绿边直接连向某一个状态节点?}\)
当前状态按绿边往前遍历节点如果发现某个状态节点通过当前字符连向的状态节点存在,由于前面遍历过的节点能表示的状态都加入新的状态中去了,而在某个状态节点由于该后缀已经存在,为了满足后缀自动机不重的性质,此状态节点通过当前字符连向的状态节点表示的子串显然不能加入新状态中,由绿边的定义,绿边连向的点为当前状态节点最小子串去掉第一个字符后所在的状态节点,如果前面的某个后缀已经存在,当无缝衔接时,即当前状态节点正好需要停下的状态节点加上当前字符,当前状态节点的绿边直接连向某一个状态节点当且仅当停下的状态节点的最长子串加上当前字符等于通过当前字符连向的状态节点表示的最长子串,即判断停下的状态节点表示的最长子串长度是否为其通过当前字符连向的状态节点表示的最长子串的长度减一
此时当前的状态节点需要的子串所在的状态节点中不是最长的,而是最短的,故其绿边不能直接指向该状态节点,而为了满足后缀自动机不漏的性质,此时必须将该状态节点拆分,即将最短子串从原状态中分离出去,新建状态节点表示该最短子串,则相应的原状态节点和当前状态节点的绿边应该指向该状态节点,同时原来蓝边通过当前字符指向原状态节点应该换成新的状态节点
后缀自动机的一些典型应用:
- 匹配子串(取代 KMP)
所有蓝边构成的节点集合组成的子串包含了该字符串的所有子串,直接根据蓝边走,如果能到达最后一个字符说明匹配否则不匹配
- 不同子串数量
由于每个状态节点中的子串是连续的,所以其状态节点中包含的本质不同的子串数量为最长的子串的长度减最短的字串的长度加一
- 每个子串出现的次数
即 \(|endpos(s)|\)
- 最长公共子串
建出某一个字符串的后缀自动机拿另外一个字符串去遍历,如果走不动回溯绿边,过程中更新答案
- 时间复杂度:\(O(n)\)
代码
// Problem: 后缀自动机
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/description/2768/
// Memory Limit: 512 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N=2e6+5;
int h[N],ne[N],e[N],idx,lst=1,cnt=1;
char s[N];
LL f[N],res;
struct Node
{
int fa,len;
int ch[26];
}node[N];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void extend(int c)
{
int p=lst,np=lst=++cnt;
node[np].len=node[p].len+1;
f[np]=1;
for(;p&&!node[p].ch[c];p=node[p].fa)node[p].ch[c]=np;
if(!p)node[np].fa=1;
else
{
int q=node[p].ch[c];
if(node[q].len==node[p].len+1)node[np].fa=q;
else
{
int nq=++cnt;
node[nq]=node[q];
node[nq].len=node[p].len+1;
node[np].fa=node[q].fa=nq;
for(;p&&node[p].ch[c]==q;p=node[p].fa)node[p].ch[c]=nq;
}
}
}
void dfs(int u)
{
for(int i=h[u];~i;i=ne[i])
{
dfs(e[i]);
f[u]+=f[e[i]];
}
if(f[u]>1)res=max(res,f[u]*node[u].len);
}
int main()
{
scanf("%s",s);
for(int i=0;s[i];i++)extend(s[i]-'a');
memset(h,-1,sizeof h);
for(int i=2;i<=cnt;i++)add(node[i].fa,i);
dfs(1);
printf("%lld",res);
return 0;
}