SAM复习
作者:互联网
定义
SAM的定义
字符串\(s\)的SAM是一个可接受\(s\)的所有后缀的最小\(DFA\)(确定的有穷自动机),可以参考编译原理的龙书(强烈推荐)
- SAM是一张有向无环图。节点被称作状态,边被称作状态之间的转移
- 存在一个初始状态\(t_0\),其他各结点都可以从\(t_0\)出发到达
- 没有输出\(\epsilon\)之上的转移,即没有边标号为\(\epsilon\)(空字符串)的转移
- 对每个状态\(s\)和每个输入符号\(a\),有且仅有一条标号为\(a\)的边离开
- 存在一个或多个终止状态。从\(t_0\)出发转移到一个终止状态,则路径上的所有转移连接起来构成了\(s\)的一个后缀。从\(t_0\)出发到达的每一个结点上路径的转移是\(s\)的一个子串。
- 最小的意思是\(DFA\)中的状态数目最少,但又可以表示所有状态。(结点最少)
重要概念
结束为止endpos
- 定义:对于\(s\)的任意非空子串\(t\),即\(endpos(t)\)为子串\(t\)在\(s\)中的所有结束位置
- 等价类:两个子串\(t_1\)和\(t_2\)的\(endpos\)集合可能相同,即\(endpos(t_1)=endpos(t_2)\)。这样所有字符串\(s\)的非空子串都可以根据他们的\(endpos\)集合划分成若干个等价类
- SAM中的每个状态对应一个或多个\(endpos\)相同的子串。SAM的状态个数=等价类个数+1,每个状态是一个等价类
- 引理1:字符串\(s\)的两个非空子串\(u\)和\(w\)\((|u|\le |w|)\)的\(endpos\)相同,当且仅当字符串\(u\)的每次出现,都是以\(w\)后缀的形式出现。比如\(s=abcdefgdefgpfg\),令\(u=fg\),\(w=defg\),显然,\(endpos(u)\ne endpos(w)\),因为\(s\)中出现了\(pfg\),\(u\)出现了,但不是以\(w\)后缀的形式
- 引理2:考虑两个非空子串\(u\)和\(w\)(\(|u|\le |w|\))。要么\(endpos(u)\cap endpos(w) =\varnothing\),要么\(endpos(w)\subseteq endpos(u)\),取决于\(u\)是否为\(w\)的一个后缀(可以用上面的例子解释)
- 引理3:考虑一个\(endpos\)等价类,将类中的所有子串按长度非递增的顺序排序。那么每个子串都是它前一个子串的后缀,即较短的子串是较长子串的后缀。记这个等价类中最长串长度为\(maxlen\),最短串长度为\(minlen\),那么这个等价类中的子串长度恰好覆盖了整个区间\([minlen,maxlen]\)
后缀链接link
考虑SAM中某个不是\(t_0\)的状态\(v\)。状态\(v\)对应于一个等价类,如果定义\(w\)为这个等价类中最长串,那么这个等价类中的其他字符串都是\(w\)的后缀。
并且\(w\)的前几个后缀(按长度降序考虑)全部包含于这个等价类中,且其他后缀在其他等价类中。什么意思,还是用\(s=abcdefgdefgpfg\)这个例子,\(w=defg\),假设\(w\)是它所在\(endpos\)的代表元,那么\(endpos(w)=\{defg,efg\}\),但\(fg,g\)都是\(w\)的后缀,但它们与\(w\)不在同一个等价类中
记\(t\)为不与\(w\)在同一个等价类中但是\(w\)的后缀的最长中的一个,显然\(|t|=minlen-1\)。
于是在状态图中,我们从\(w\)所在的等价类\(v\)向\(t\)所在等价类连一条件记为\(link(v)\),这样的边只有一条
- 引理4:所有后缀链接构成一棵根节点为\(t_0\)的树
- 引理5:通过\(endpos\)集合构造的树(每个子节点的\(subset\) 都包含在父节点的 $subset \(中)与通过后缀链接\)link$构造的树相同。
其中蓝色的边是\(SAM\)中有向图的边,虚线的蓝色边为后缀连接\(link\),构成了一棵树
复杂度
- 空间复杂度\(O(n|\Sigma|)\),其中\(|\Sigma|\)为字符集的大小
- 时间复杂度\(O(n)\)
- SAM 的大小(状态数和转移数)为 线性的
构造方法
一般可以先求出\(SAM\),然后把\(link\)构成的树重新构建一遍
模板
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e6+10;
struct Node{
int len,link;
int ch[26];
}node[N];
int last=1,idx=1;
ll dp[N];
void SAM_Extend(int c){
int p=last,cur=++idx; //每次新加入的点是一个前缀
last=cur;
dp[idx]=1;
node[cur].len=node[p].len+1;
for(;p&&!node[p].ch[c];p=node[p].link) node[p].ch[c]=cur;
if(!p) node[cur].link=1;
else{
int q=node[p].ch[c];
if(node[q].len==node[p].len+1){
node[cur].link=q;
}else{
int nq=++idx;
node[nq]=node[q];
node[nq].len=node[p].len+1;
node[cur].link=node[q].link=nq;
for(;p&&node[p].ch[c]==q;p=node[p].link) node[p].ch[c]=nq;
}
}
}
//构建 link树
struct edges{
int v,nxt;
}e[N];
int cnt,head[N];
void add(int u,int v){
e[cnt]={v,head[u]},head[u]=cnt++;
}
//一顿操作
ll ans;
void dfs(int u){
for(int i=head[u];~i;i=e[i].nxt){
int v=e[i].v;
dfs(v);
dp[u]+=dp[v];
}
if(dp[u]>1) ans=max(ans,dp[u]*node[u].len);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
memset(head,-1,sizeof head);
string s;
cin>>s;
int n=s.size();
for(int i=0;i<n;i++) SAM_Extend(s[i]-'a');
for(int i=2;i<=idx;i++) add(node[i].link,i);
dfs(1);
cout<<ans<<'\n';
return 0;
}
应用
- 检查字符串是否出现
- 不同子串个数
- 方法一:在SAM有向无环图上用动态规划求
- 方法二:在\(link\)树上求,每个节点对应的子串数量是\(len(i)-len(link(i))=maxlen(i)-maxlen(link(i))\)
- 所有不同子串的总长度
- 字典序第\(k\)大子串
- 最小循环移位
- 出现次数
- 假设模式串为\(P\),文本串为\(T\),那么构造出\(T\)的SAM后,在\(link\)树上找到\(P\)所在的节点,对应节点的\(endpos\)大小就是\(P\)的出现次数。\(endpos\)大小直接在\(link\)数上从子节点递推即可。
- 所有出现位置
- 最短的没有出现的字符串
- 两个字符串的最长公共子串
- 多个字符串间的最长公共子串
标签:node,子串,复习,SAM,int,后缀,link,endpos 来源: https://www.cnblogs.com/Arashimu0x7f/p/16521494.html