后缀自动机 SAM

DFA 和 NFA

dfa 是可确定自动机,nfa 是不可确定自动机.

dfa 是对于每个状态,加上一个字符,只会有一条出边,不存在环.

dfa 在读入字符串 str 之后可以到达的状态是唯一的.

nfa 是对于每个状态,加上一个字符,可以有多条出边,可能存在环.

nfa 在读入字符串 str 之后可以到达的状态是不唯一的.

还有更多的区别,这里先不仔细写下.

sam 是 dfa.

DFA 的组成

  1. $\sum $ 代表字符集.
  2. 状态集合.
  3. init 初始状态,且只有一个.
  4. end 终止状态,可以有多个.
  5. 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(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)\) 可能会改变

相关性质
  1. 如果 \(trans(s,ch)\not=null\) ,则 \(trans(par(s),ch)\not=null\).
  2. \(Right(s,ch)\sub Right(trans(par(s)),ch)\)
  3. 如果状态 \(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,有两种情况.

  1. \(maxlen(q)=maxlen(v_p)+1\) , 此时可直接令 \(par(p)=q\) .

  2. \(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;
}
小结

有好多东西没有写上去……

posted @ 2021-07-05 18:51  xyangh  阅读(31)  评论(0)    收藏  举报