后缀自动机 SAM
DFA 和 NFA
dfa 是可确定自动机,nfa 是不可确定自动机.
dfa 是对于每个状态,加上一个字符,只会有一条出边,不存在环.
dfa 在读入字符串 str 之后可以到达的状态是唯一的.
nfa 是对于每个状态,加上一个字符,可以有多条出边,可能存在环.
nfa 在读入字符串 str 之后可以到达的状态是不唯一的.
还有更多的区别,这里先不仔细写下.
sam 是 dfa.
DFA 的组成
- $\sum $ 代表字符集.
- 状态集合.
- init 初始状态,且只有一个.
- end 终止状态,可以有多个.
- trans 状态转移函数.
trans
\(trans(s,c)\) 表示状态 \(s\) 在加入字符 \(c\) 之后所能到达的状态.
\(null\) 表示不存在的状态.
如何 \(trans(s,c)\) 不存在,则 \(trans(s,c)=null\).
定义 \(trans(s,str)\) 表示在当前状态为 \(s\) ,读入 \(str\) 之后能到达的状态.
Reg
定义 \(Reg(A)\) 为自动机 \(A\) 能识别的所有字符串,即能使 \(trans(init,str)\sub end\) 的 \(str\).
定义 \(Reg(s)\) 为状态 \(s\) 开始所有能识别的所有字符串,即能使 \(trans(s,str)\sub end\) 的 \(str\).
我
sam 的功能
对于给定字符串 \(S\) ,\(S\) 的后缀自动机(sam)能识别 \(S\) 的所有后缀的自动机.
即 \(SAM(str)=true\) 当且仅当 \(str\) 是 \(S\) 的后缀.
不仅如此,sam 还能用来识别 \(S\) 的所有子串.
有关Fac, Suf
令母串为 \(S\) ,后缀集合为 \(Suf\) . 子串集合为 \(Fac\).
从位置 \(i\) 开始的后缀为 \(Suf(i)\) .
\(Suf[l,r)\) 表示 \([l,r)\) 这个区间构成的子串. ( 下标从 \(0\) 开始 )
状态
对于一个字符串 \(s\) ,如果 \(s\notin Fac\) ,那么,\(s\) 必定无法成为 \(S\) 的后缀. 没有理由浪费空间.
否则,sam 不放过 \(s\) 成为后缀的可能.
Right
定义 \(right(s)={r_1,r_2,\cdots,r_k}\) . 为一些字符串的右端点.
考虑,如果确定下 \(len\) ,字符串的长度,也就唯一地确定了字符串.
所以,对于 \(s\) 维护一个 \([minlen(s),maxlen(s)]\) .
此时,对于最少的状态,两两 \(right\) 结合要么包含,要么不相交.
考虑 \(r\in Right(a)\cap Right(b)\),
因为状态不同,所以 \([minlen(a),maxlen(a)]\cap[minlen(b),maxlen(b)]=\varnothing\).
显然,可以把 \(r\) 提出来,新建一个节点保证 \(right\) 的包含或不相交关系.
接着,发现,包含与不相交就是一个树的结构,所以状态数可以保证是线性的,其实最多达到 \(2n-1\) 个.
其次,发现对于状态 \(s\) ,\(maxlen(s)=minlen(par(s))+1\). 所以,只需要维护最大值 \(len\) 即可.
维护
考虑,现在已经拥有 \(T\) 的自动机了,在后面加上一个字符 \(x\) ,来获得 \(T+x\) 的自动机.
令 \(T\) 的长度为 \(L\).
首先,会发生改变的肯定是满足 \(Right(s)={L}\) 的 \(s\) 以及 \(s\) 的祖先,\(trans(s,ch)\) 可能会改变
相关性质
- 如果 \(trans(s,ch)\not=null\) ,则 \(trans(par(s),ch)\not=null\).
- \(Right(s,ch)\sub Right(trans(par(s)),ch)\)
- 如果状态 \(s\) 经过 \(ch\) 的边,能转移到状态 \(t\) ,则 \(max(t)\geq max(s)+1\).
新建节点
需要新建一个节点 \(p\) . \(Right(v_p)={L+1}\).
跟新转移
因为性质1与性质2,所以考虑从根节点到 \(Right(s)={L}\) 的 \(s\) 的链上,从下至上分别为\({v_1,v_2,v_3,\cdots,v_k}\),必定是链的从下至上一些节点 \(trans(v_i,x)=null\)
将 \(trans(v_i,x)=null\) 的节点修改成 \(trans(v_i,x)=p\).
接着,找到第一个 \(trans(v_i,x)\not=null\) 的节点 \(v_p\).
令 \(q=trans(v_p,x)\) ,根据性质3,有两种情况.
-
\(maxlen(q)=maxlen(v_p)+1\) , 此时可直接令 \(par(p)=q\) .
-
\(maxlen(q)>maxlen(v_p)+1\) ,那么,如果直接加入,会使得集合的大小缩小.
所以,考虑新建节点 \(nq\) ,使得 \(max(nq)=max(v_p)+1\).
并且,设置 \(par(q)=nq\),\(par(p)=nq\).
最后,对于在链上,\(trans(v_i,x)=q\) 的节点的 \(trans(v_ix)\) 修改成 \(nq\).
有关时间复杂度的证明
其他的并不难看出是线性的,对于从 \(q\) 修改成 \(nq\) 的,毫无头绪.
有关空间复杂度的证明
因为 sam 是 dfa,所以 sam 是一个dag.
考虑证明非树边的个数是线性的,思考一个匹配是经过一些树边+一条非树边+一些边组成的,考虑,用这个非树边对应这个后缀,此时,每个后缀最多对应一个非树边. 每个非树边最少被一个后缀对应,所以,空间是线性的.
实现
字符集用 map 实现. 对于固定范围可以开数组,state 数要是字符串长度的两倍.
class state{
public:
int link,len;
map<char,int>nxt;
}st[2000010];
int sz=0,lst=0;
void init(){
st[0].link=-1;
st[0].len=0;
sz++;
}
int cnt[2000010];
void extend(char c){
int cur=sz++;
cnt[cur]++;
st[cur].len=st[lst].len+1;
int p=lst;
while(p!=-1&&st[p].nxt.find(c)==st[p].nxt.end()){
st[p].nxt[c]=cur;
p=st[p].link;
}
if(p==-1){
st[cur].link=0;
lst=cur;
return;
}
int q=st[p].nxt[c];
if(st[q].len==st[p].len+1){
st[cur].link=q;
lst=cur;
return;
}
int nq=sz++;
st[nq].link=st[q].link;
st[nq].len=st[p].len+1;
st[nq].nxt=st[q].nxt;
st[cur].link=st[q].link=nq;
while(p!=-1&&st[p].nxt[c]==q){
st[p].nxt[c]=nq;
p=st[p].link;
}
lst=cur;
}
小结
有好多东西没有写上去……

浙公网安备 33010602011771号