码迷,mamicode.com
首页 > 其他好文 > 详细

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

时间:2020-07-16 12:08:04      阅读:64      评论:0      收藏:0      [点我收藏+]

标签:ret   tps   scanf   --   信息   初始   大小   代码   alc   

感谢\(ivorysi\)学姐_(:з」∠)_给我讲了一上午才明白

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

后缀自动机是一个\({\rm DAG}\)
顾名思义,后缀自动机上,根到每个节点的路径都代表一个原串的后缀

对于字符串\(\texttt{aabb}\),它的后缀自动机为:
技术图片

性质

  • 后缀只有\(n\)个;
  • \(endpos\)表示一个子串结束的位置。对于两个子串\(u,v(|u|>|v|)\),若\(endpos_u = endpos_v\),则有\(u\supseteq v\),即\(v\)\(u\)的子串;
  • 每次最多新建\(2\)个节点,即空间上限为\(2n\)
  • 字符集大小一般为\(26\),因为后缀自动机是\({\rm 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\not = 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}\)
    • 将指向\(q\)的点改为指向\(q_{new}\),即\(p.ch[x]=q_{new}\),并不断向上找父亲\(p = p.fa\)

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

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

技术图片

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

技术图片

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

技术图片

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

技术图片

  • 加入第二位\(\texttt{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,\ \because q.len = p.len+1\)
    \(\therefore now.fa=q\),退出。

技术图片

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

技术图片

  • \(p=last=3;\ 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\),退出。

技术图片

  • 加入第四位\(\texttt{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,\ \because q.len \not = p.len+1\)
    \(\therefore\) 新建节点\(q_{new}\).

技术图片

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

技术图片

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

技术图片

  • 将指向\(q\)的节点改为指向\(q_{new}\)

技术图片

画图好累...

注意:

我的理解:以上述例子为例,当加入第四位,即第二个\(\texttt{b}\)时,后缀\(\texttt{b}\)的出现次数不再与\(\texttt{a}\)等同,所以需要新开一个节点计算。
节点用结构体封装,复制\(q_{new}=q\)时把信息全部复制过去了,不要忘记把\(cnt\)改为\(0\)

\(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;

应用

模板题:Luogu P3804

求出\(S\)的所有出现次数\(>1\)的子串的(出现次数\(\times\)长度)\(_{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;
}

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

标签:ret   tps   scanf   --   信息   初始   大小   代码   alc   

原文地址:https://www.cnblogs.com/mogeko/p/13308090.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!