P3804 【模板】后缀自动机(SAM)
P3804 【模板】后缀自动机(SAM)
题目描述:
给定一个只包含小写字母的字符串
请你求出
对于
前言:
后缀自动机训练偶遇 你的名字 强如怪物,拼尽全力无法战胜,见总结SAM未写,故水之.
Solution:
后缀自动机,故名思意就是一台包含了以这个字符串的每个节点为结尾的所有后缀的机器。(所以为什么不叫字串自动机)?
反正就是非常强力的自动机就对了,这里并不介绍理论,只说应用。(主播是实用主义者)(其实是不会,防止把自己绕晕)。
首先我们有一些概念:
在后缀自动机中,一个节点并无实际意义,有意义的是rt->x 这条路径,这条路径上的边是有
endpos: 某个后缀在原串上的结束位置(显然是一个集合)
link/fa: 这个节点在 parent 树上的父亲
len: 这个 endpos 集合下所对应的所有字符串中最长的字符串长度。
首先呢,后缀自动机是一个 DAG. 每个点可以有很多个ch[x][c] 表示的是下一个字符串接 c 应该往哪跳。(很像AC自动机对不对)。
但是每个点对应的的字符串有很多个(因为到达该点的路径可能有多条)。所以一个点的含义其实是 endpos 集合相等 的一些字符串。并且显然这些字符串是存在包含关系的,长度为 len 的字符串包含了该点下所有字符串。
然后对于 parent 树,有一些显然的性质:
父亲 fa 的 endpos 集合为其所有儿子 x 的并集,且父亲节点代表的所有字符串中的最长长度等于儿子代表的所有字符串中的最短长度+1.并且父亲的每个字符串都是儿子的字串。
这些性质只要记下来并且理解就很足够了,比如说这题,我们建完 SAM 之后,不断向上合并其 siz,就可以得到一个点内最长的字符串的出现次数,然后乘上其长度就能得到答案了。
Code:
#include<bits/stdc++.h> const int N=3e6+6; using namespace std; long long ans; vector<int> E[N]; struct SAM{ int ch[N][26],siz[N],fa[N],len[N]; int last=1,cnt=1; void build(){for(int u=2;u<=cnt;u++){E[fa[u]].push_back(u);}} void insert(int c) { int p=last,q=++cnt;last=q; siz[q]=1;len[q]=len[p]+1; for(;p&&!ch[p][c];p=fa[p]){ch[p][c]=q;} if(!p){fa[q]=1;return;}int x=ch[p][c]; if(len[x]==len[p]+1){fa[q]=x;return;} int y=++cnt;fa[y]=fa[x];fa[x]=fa[q]=y;len[y]=len[p]+1; for(int i=0;i<26;i++)ch[y][i]=ch[x][i]; for(;p&&ch[p][c]==x;p=fa[p]){ch[p][c]=y;} } void dfs(int x) { for(auto y : E[x]) { dfs(y);siz[x]+=siz[y]; } if(siz[x]!=1) ans=max(ans,1ll*siz[x]*len[x]); } }sam; char s[N]; void work() { scanf("%s",s+1); int n=strlen(s+1); for(int i=1;i<=n;i++) { sam.insert(s[i]-'a'); } sam.build(); sam.dfs(1); printf("%lld",ans); } int main() { //freopen("sam.in","r",stdin);freopen("sam.out","w",stdout); work(); return 0; }