zoukankan      html  css  js  c++  java
  • 【模板】后缀自动机 (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;
    }
    
  • 相关阅读:
    155. 最小栈
    160. 相交链表
    PAT 1057 Stack
    PAT 1026 Table Tennis
    PAT 1017 Queueing at Bank
    PAT 1014 Waiting in Line
    PAT 1029 Median
    PAT 1016 Phone Bills
    PAT 1010 Radix
    PAT 1122 Hamiltonian Cycle
  • 原文地址:https://www.cnblogs.com/mogeko/p/13308090.html
Copyright © 2011-2022 走看看