• 【模板】后缀自动机 (SAM)


    感谢(ivorysi)学姐_(:з」∠)_

    作图工具:(Google)绘图

    后缀自动机 ({ m (Suffix Automaton,SAM)})是一个用来匹配单模板串的所有子串的算法。
    ({ m SAM})的空间复杂度、构造的时间复杂度都是(O(n))的。

    后缀自动机是一个({ m DAG})
    后缀自动机上,根到每个节点的路径都代表一个原串的子串

    对于字符串( exttt{aabb}),它的后缀自动机为:

    性质

    • 后缀只有(n)个;
    • (endpos)表示一个子串结束的位置。对于两个子串(u,v(|u|>|v|)),若(endpos_u = endpos_v),则有(usupseteq v),即(v)(u)的子串;
    • 每次最多新建(2)个节点,即空间上限为(2n)
    • 字符集大小一般为(26),因为后缀自动机是({ m DAG}),所以时间复杂度是(kn)(k)是常数)。

    (parent)

    设根节点为(1),加入到第(R)位,存在((1,i])((1,R])的后缀且长度最大,即((1,i] = (L,R])(i)最大,则(fa[R]=i)
    (|L,R|)可以为(0),即(fa[R] = 1)
    有点类似于AC自动机的失配函数。

    • 正串的(parent)树$是反串的后缀树。好像忘了怎么证明了

    初始化

    每个节点需要储存的信息有:
    (ch[26]):子节点
    (fa):父节点
    (len):从根节点到该节点的(代表的字符串的)长度
    (cnt)(0/1),若该节点在后缀链上,则为(1)

    构造

    需要储存的信息有:
    (root):根节点
    (last):上一个加入的节点(每次(+1)
    (siz):后缀自动机的节点个数

    流程:

    设当前插入的字符为(x).

    • 向后缀链的末尾插入一个新节点(now)
    • 检查(now)在后缀链上的上一个节点(p = last),是否存在字符为(x)的子节点(q = p.ch[x])
      若不存在,则连边(p.ch[x] = now),继续向上找父亲(p = p.fa)
    • 退出循环时,若(p=0)说明没有匹配到的后缀,(now.fa = root),结束。
    • (p ot = 0),则检查(p->q)是否为后缀链上的边;
      • (q.len = p.len+1),说明匹配到了一个存在的后缀,(now.fa = q),结束。
      • 否则,说明这样的后缀不存在于已经加入的后缀链中,需要新建一个节点(q_{new})来表示。
        (q_{new})复制(q)父节点子节点信息,(q_{new}.len = p.len+1)
        (q_{new})不是后缀链上原有的点,所以(q_{new}.cnt = 0)
      • 新建的((1,q_{new}])((1,now])((1,q])的后缀,所以(q_{new})(now)(q)的父节点,
        (now.fa = q.fa = q_{new})
      • 从当前(p)开始,将指向(q)的点改为指向(q_{new}),即(p.ch[x]=q_{new}),并不断向上找父亲(p = p.fa)

    依旧以串( exttt{aabb})为例。

    • 首先,加入根节点,(root=last=siz=1).

    • 加入第一位( exttt{a}).
      • 新建节点(now).

    • (p=last=1; p)不存在(ch[a]),连边(p.ch[a]=now;)

    • (p=p.fa=0),则(now.fa=root),退出。

    • 加入第二位( exttt{a}).
      • 新建节点(now).

    • (p=last=2; p)不存在(ch[a]),连边(p.ch[a]=now;)

    • (p=p.fa=1; q=p.ch[a]=2)
      (q.len=2,p.len=1, ecause q.len = p.len+1)
      ( herefore now.fa=q),退出。

    • 加入第三位( exttt{b}).
      • 新建节点(now).

    • (p=last=3; p)不存在(ch[b]),连边(p.ch[b]=now;)

    • (p=p.fa=2; p)不存在(ch[b]),连边(p.ch[b]=now;)

    • (p=p.fa=1; p)不存在(ch[b]),连边(p.ch[b]=now;)

    • (p=p.fa=0),则(now.fa=root),退出。

    • 加入第四位( exttt{b}).
      • 新建节点(now).

    • (p=last=4; p)不存在(ch[b]),连边(p.ch[b]=now)

    • (p=p.fa=1;\q=p.ch[b]=4;)

    • (q.len=4,p.len=1, ecause q.len ot = p.len+1)
      ( herefore) 新建节点(q_{new}).

    • (q)的父子信息复制给(q_{new})(q_{new}.len = p.len+1)(q_{new}.cnt = 0).

    • (now)(q)的父亲改为(q_{new}).

    • 从当前(p)开始,将指向(q)的节点改为指向(q_{new})

    画图好累...

    (code)

    struct SuffixAutomaton {
    	struct node {
    		int ch[26],fa,len,cnt;
    		void clean() {
    			memset(ch,0,sizeof(ch));
    			fa = len = cnt = 0;
    		}
    	} S[maxn<<1];
    	int root,last,siz;
    
    	void init() {
    		for(int i = 1; i <= siz; i++)
    			S[i].clean();
    		root = last = siz = 1;
    	}
    
    	void insert(int c) {
    		int p = last, now = ++siz;
    		S[now].cnt = 1;
    		S[now].len = S[p].len+1;
    		for(; p && !S[p].ch[c]; p = S[p].fa)
    			S[p].ch[c] = now;
    		if(!p) S[now].fa = root;
    		else {
    			int q = S[p].ch[c];
    			if(S[q].len == S[p].len+1)
    				S[now].fa = q;
    			else {
    				int q_new = ++siz;
    				S[q_new] = S[q];
    				S[q_new].cnt = 0;
    				S[q_new].len = S[p].len+1;
    				S[now].fa = S[q].fa = q_new;
    				for(; p && S[p].ch[c] == q; p = S[p].fa)
    					S[p].ch[c] = q_new;
    			}
    		}
    		last = now;
    	}
    } SAM;
    

    注意:

    我的理解:以上述例子为例,当加入第四位,即第二个( exttt{b})时,后缀( exttt{b})的出现次数不再与( exttt{a})等同,所以需要新开一个节点计算。
    节点用结构体封装,复制(q_{new}=q)时把信息全部复制过去了,不要忘记把(cnt)改为(0)
    一个检查作图是否正确的方法:对于每个字串,从根节点往下找,看能不能找到。

    后缀自动机的一些可能形态

    ({ m S=} exttt{aaaaaaaaa})
    对于每一位(i),最长后缀的长度都为(i-1),非常优美。

    ({ m S=} exttt{cbabaacba})
    简化一下,看作依次加入( exttt{cba,ba,a,cba})
    加入( exttt{ba})时,后缀( exttt{ba})的数量不再与( exttt{cba})等同,需要新建节点;
    加入( exttt{a})时,后缀( exttt{a})的数量不再与( exttt{ba})等同,需要新建节点;
    似乎可以一直套娃下去...
    加入( exttt{cba})时,最长的后缀为前面的( exttt{cba})

    应用

    模板题:Luogu P3804

    求出(S)的所有出现次数(>1)的子串的(出现次数( imes)长度)(_{max})

    由下到上更新(parent)树,最后计算每个节点的贡献即可。
    为了保证由下到上更新,将节点按拓扑序排序。根据性质,一定有(i.len<fa[i].len)
    因此,用桶排序将节点按(len)从大到小排序,得到的即为拓扑序。
    将后缀链上的点的(cnt)设为(1),其余点设为(0)
    (i.cnt = i.cnt + sum j.cnt(fa[j]=i))

    完整代码如下

    #include<cstdio>
    #include<iostream>
    #include<cmath>
    #include<cstring>
    #define MogeKo qwq
    using namespace std;
    
    const int maxn = 1e6+10;
    
    char s[maxn];
    int b[maxn<<1],que[maxn<<1];
    long long ans;
    
    struct SuffixAutomaton {
    	struct node {
    		int ch[26],fa,len,cnt;
    		void clean() {
    			memset(ch,0,sizeof(ch));
    			fa = len = cnt = 0;
    		}
    	} S[maxn<<1];
    	int root,last,siz;
    
    	void init() {
    		for(int i = 1; i <= siz; i++)
    			S[i].clean();
    		root = last = siz = 1;
    	}
    
    	void insert(int c) {
    		int p = last, now = ++siz;
    		S[now].cnt = 1;
    		S[now].len = S[p].len+1;
    		for(; p && !S[p].ch[c]; p = S[p].fa)
    			S[p].ch[c] = now;
    		if(!p) S[now].fa = root;
    		else {
    			int q = S[p].ch[c];
    			if(S[q].len == S[p].len+1)
    				S[now].fa = q;
    			else {
    				int q_new = ++siz;
    				S[q_new] = S[q];
    				S[q_new].cnt = 0;
    				S[q_new].len = S[p].len+1;
    				S[now].fa = S[q].fa = q_new;
    				for(; p && S[p].ch[c] == q; p = S[p].fa)
    					S[p].ch[c] = q_new;
    			}
    		}
    		last = now;
    	}
    
    	void calc() {
    		for(int i = 1; i <= siz; i++)
    			b[S[i].len]++;
    		for(int i = 1; i <= siz; i++)
    			b[i] += b[i-1];
    		for(int i = 1; i <= siz; i++)
    			que[b[S[i].len]--] = i;
    		for(int i = siz; i; i--)
    			S[S[que[i]].fa].cnt += S[que[i]].cnt;
    		for(int i = 1;i <= siz;i++)
    			if(S[i].cnt > 1) ans = max(ans,(long long)S[i].cnt*S[i].len);
    		printf("%lld",ans);
    	}
    
    } SAM;
    
    int main() {
    	scanf("%s",s+1);
    	int n = strlen(s+1);
    	SAM.init();
    	for(int i = 1; i <= n; i++)
    		SAM.insert(s[i]-'a');
    	SAM.calc();
    	return 0;
    }
    
  • 相关阅读:
    设计模式-原型模式(06)
    看起来很懵的java内存加载面试题
    回数
    花式赋值
    常量
    Python解释器安装
    计算机基础小结
    网络的瓶颈效应
    __init__和__new__
    super()方法详解
  • 原文地址:https://www.cnblogs.com/mogeko/p/13308090.html
Copyright © 2020-2023  润新知