后缀自动机

下面的 (SAM) 都是默认对于字符串 (s) 的。

定义

SAM 是一张有向无环图,其中,节点被称作状态,边被称作状态间的转移。
图有一个源点 (t_0) ,称作初始状态,其它各节点均可从 (t_0) 出发到达。
每个转移都标有一个字母,从每个节点出发的所有转移所标的字母均不同。
存在若干个终止状态。从初始状态出发走到任意一个终止状态,经过的路径上的所有转移所标的字母连起来一定为 (s) 的一个后缀。(s) 的每个后缀均对应一条这样的路径。
SAM 即为满足以上条件的节点数最少的自动机。
一个性质
任意从初始状态 (t_0) 开始的路径,路径上的所有转移所标的字母连起来一定为 (s) 的一个子串。(s) 的每个子串也都对应着一条这样的路径。
endpos
对于字符串 (s) 的任意非空子串 (t) ,记 (endpos(t))(t) 在字符串 (s) 中的所有结束位置组成的集合。
若两个子串的 (endpos) 集合相等,则它们在同一等价类中。
可以发现,SAM 的每个状态就对应着一个等价类,所以 SAM 的状态总数就等于等价类的个数 (+1)
几个结论
1.若字符串 (s) 的两个子串 (x)(y) ((|x|<|y|)) 的 (endpos) 相同,则 (x)(s) 中每次都是以 (y) 的后缀的形式存在的。
2.对于两个非空子串 (x)(y) ((|x|<|y|)) ,若 (x)(y) 的后缀,则 (endpos(x) subseteq endpos(y)) ,反之, (endpos(x) cap endpos(y) = varnothing)
3.对于一个等价类中的任意两个子串,较短者一定为较长者的后缀。同时,该等价类中的子串长度一定恰好覆盖一整个区间 ([x,y])
link
考虑 SAM 中某个不是源点的状态 (v) ,设它对应的等价类中最长的字符串为 (w) 。该等价类中的其它字符串均为 (w) 的后缀。
可以发现, (w) 的最长几个后缀都存在于这个等价类中,且其它后缀存在于其它等价类中。(因为有空后缀,所以一定会存在这样的后缀)
(t) 为最长的这样的后缀,就将 (v) 的后缀链接 (link(v)) 连到 (t)
下面,假设初始状态 (t_0) 也为一个只包含自己的等价类(即只包含空串),规定 (endpos(t_0)={0,1,2, cdots ,|s|})
又是几个结论
4.所有后缀链接构成一棵以 (t_0) 为根的树。
后缀链接一定连到严格比当前字符串短的串。
5.通过 (endpos) 集合构造的树与通过后缀链接构造的树相同。
根据上面的结论 (2) ,可以发现 (endpos) 集合构成一棵树。
考虑任意一个不是 (t_0) 的状态 (v) ,可以发现 (endpos(v) subsetneq endpos(link(v)))
结合这些,通过后缀链接构造的树本质上就是通过 (endpos) 构造的树。

下面,对于一个状态 (v) ,设
(longest(v)) 为该状态下最长的字符串, (maxlen(v)) 为它的长度。
(shortest(v)) 为最短的字符串, (minlen(v)) 为它的长度。
显然,有 (minlen(v)=maxlen(link(v))+1)

构造 SAM

首先,对于初始状态 (t_0) ,指定 (maxlen=0)(link=0)
设当前要插入的字符为 (c) ,插入 (c) 之前的字符串为 (s)
首先,创建一个新状态 (cur) ,对应于字符串 (s+c)
(last)(s) 对应的状态,则 (maxlen(cur)=maxlen(last)+1)
接下来,从 (last) 开始通过后缀链接遍历,对于遍历到的每个状态,尝试增加一条标着 (c) 的转移,如果原来就有了,直接结束循环。
如果最后遍历到了 (0) ,则表示字符串 (s) 中没有出现过 (c) ,所以 (link(cur)=1)
反之,假设已经存在的标着 (c) 的转移为 ((p,q))
设状态 (p) 对应于字符串 (x) ,因为 (p)(last) 的后缀链接上,所以 (x)(s) 的后缀。
因为存在 (q) 这个状态,所以 (x+c) 一定作为 (s) 的子串出现过。
此时,我们就要把 (cur) 的后缀链接连到一个满足其中最长的字符串为 (x+c) 的状态上。
(q) 满足这个条件,则 (maxlen(q)=maxlen(p)+1)
所以,满足上面的式子时, (link(cur)=q)
考虑最后一种情况,即 (maxlen(q)>maxlen(p)+1) 的情况。
此时,考虑把 (q) 拆开,使其中一个状态的 (maxlen=maxlen(p)+1)
新建一个状态 (v)
(maxlen(v)) 赋为 (maxlen(p)+1)
(q) 的所有转移赋给 (v) ,把 (link(v)) 赋为 (link(q)) ,把 (link(q)) 赋为 (v) ,把 (link(cur)) 赋为 (v)
最后,遍历一遍 (q) 的后缀链接,直到到了 (0) 或到了一个不能直接转移到 (q) 的状态。
把这些状态到 (q) 的转移改为到 (v) 的转移即可。

void add(char c)
{
	int cur=++cnt;
	s[cur].len=s[lst].len+1;
	int p=lst;
	while(p&&!s[p].nxt.count(c))
		s[p].nxt[c]=cur,p=s[p].link;
	if(!p) s[cur].link=1;
	else
	{
		int q=s[p].nxt[c];
		if(s[p].len+1==s[q].len) s[cur].link=q;
		else
		{
			int v=++cnt;
			s[v]=s[q],s[v].len=s[p].len+1;
			while(p&&s[p].nxt[c]==q)
				s[p].nxt[c]=v,p=s[p].link;
			s[q].link=v,s[cur].link=v;
		}
	}
	lst=cur;
}

应用

1
检查模式串 (t) 在文本串 (s) 中是否出现。
(t) 构造后缀自动机,从 (t_0) 开始根据 (s) 的字符转移,
如果能转移完整个 (s) ,则出现过,反之,没有出现。
2
对于一个 SAM 上的状态 (x) ,它贡献的不重复的子串即为 (maxlen(x)-maxlen(link(x)))

#include<iostream>
#include<cstring>
#include<cstdio>
#include<map>
#define int long long
using namespace std;
const int N=1e5+10;
int cnt,lst,ans,n;
struct SAM
{
	int len,link;
	map <int,int> nxt;
}s[N*2];
void add(int c)
{
	int cur=++cnt;
	s[cur].len=s[lst].len+1;
	int p=lst;
	while(p&&!s[p].nxt.count(c))
		s[p].nxt[c]=cur,p=s[p].link;
	if(!p) s[cur].link=1;
	else
	{
		int q=s[p].nxt[c];
		if(s[p].len+1==s[q].len) s[cur].link=q;
		else
		{
			int v=++cnt;
			s[v]=s[q],s[v].len=s[p].len+1;
			while(p&&s[p].nxt[c]==q)
				s[p].nxt[c]=v,p=s[p].link;
			s[q].link=v,s[cur].link=v;
		}
	}
	ans+=s[cur].len-s[s[cur].link].len,lst=cur;
}
signed main()
{
	cnt=1,lst=1;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	{
		int x;
		scanf("%lld",&x);
		add(x),printf("%lld
",ans);
	}
	return 0;
}

3
给定一个字符串,计算所有不同子串的总长度。
状态 (x) 的贡献为 (frac{maxlen(x)*(maxlen(x)+1)}{2}-frac{maxlen(link(x))*(maxlen(link(x))+1)}{2})
4
复制一遍串,在 SAM 上贪心地跑 (n) 步即可。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<map>
#define fi first
#define se second
#define int long long
using namespace std;
const int N=1e6+10;
int cnt,lst,n,a[N];
struct SAM
{
	int len,link;
	map <int,int> nxt;
}s[N*2];
void add(int c)
{
	int cur=++cnt;
	s[cur].len=s[lst].len+1;
	int p=lst;
	while(p&&!s[p].nxt.count(c))
		s[p].nxt[c]=cur,p=s[p].link;
	if(!p) s[cur].link=1;
	else
	{
		int q=s[p].nxt[c];
		if(s[p].len+1==s[q].len) s[cur].link=q;
		else
		{
			int v=++cnt;
			s[v]=s[q],s[v].len=s[p].len+1;
			while(p&&s[p].nxt[c]==q)
				s[p].nxt[c]=v,p=s[p].link;
			s[q].link=v,s[cur].link=v;
		}
	}
	lst=cur;
}
signed main()
{
	cnt=1,lst=1;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++) add(a[i]);
	for(int i=1;i<=n;i++) add(a[i]);
	int p=1;
	for(int i=1;i<=n;i++)
	{
		printf("%lld ",(*s[p].nxt.begin()).fi);
		p=(*s[p].nxt.begin()).se;
	}
	return 0;
}

5
因为要求的是最大值,所以,对于每一个状态,一定只有 (longest) 有用。
(f_i)(longest(i)) 出现的次数。
考虑一个状态 (v) ,若 (link(v)=x)
那么 (longest(x)) 一定是 (longest(v)) 的后缀。
所以 (f_i=1+ sum limits_{link(x)=i} f_x)

#include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#include<map>
#define int long long
using namespace std;
const int N=1e6+10;
int cnt,lst,sz[N*2],n,ans;
char a[N];
vector <int> e[N*2];
struct SAM
{
	int len,link;
	map <char,int> nxt;
}s[N*2];
int MAX(int x,int y)
{
	return x>y?x:y;
}
void add(char c)
{
	int cur=++cnt;
	s[cur].len=s[lst].len+1;
	int p=lst;
	while(p&&!s[p].nxt.count(c))
		s[p].nxt[c]=cur,p=s[p].link;
	if(!p) s[cur].link=1;
	else
	{
		int q=s[p].nxt[c];
		if(s[p].len+1==s[q].len) s[cur].link=q;
		else
		{
			int v=++cnt;
			s[v]=s[q],s[v].len=s[p].len+1;
			while(p&&s[p].nxt[c]==q)
				s[p].nxt[c]=v,p=s[p].link;
			s[q].link=v,s[cur].link=v;
		}
	}
	sz[cur]=1,lst=cur;
}
void dfs(int x)
{
	for(int i=0;i<e[x].size();i++)
		dfs(e[x][i]),sz[x]+=sz[e[x][i]];
	if(sz[x]!=1) ans=MAX(ans,s[x].len*sz[x]);
}
signed main()
{
	cnt=1,lst=1;
	scanf("%s",a+1);
	n=strlen(a+1);
	for(int i=1;i<=n;i++) add(a[i]);
	for(int i=2;i<=cnt;i++) e[s[i].link].push_back(i);
	dfs(1),printf("%lld",ans);
	return 0;
}

6
先考虑 (t=0) 的情况。
从初始状态开始顺着 SAM 跑,能选小的字母则选小的字母。
此时,每个点的权值均为 1。
考虑 (t=1) 的情况,其实只是改变了点的权值。
对于每个点,新权值就是以它结尾的不同子串的数量,也就是 5 中的 (f)

#include<iostream>
#include<cstring>
#include<cstdio>
#include<map>
#define int long long
using namespace std;
const int N=1e6+10;
int cnt,lst,sz[N*2],n;
int d[N*2],id[N*2],tp,f[N*2];
char a[N];
struct SAM
{
	int len,link;
	map <char,int> nxt;
}s[N*2];
int MAX(int x,int y)
{
	return x>y?x:y;
}
void add(char c)
{
	int cur=++cnt;
	s[cur].len=s[lst].len+1;
	int p=lst;
	while(p&&!s[p].nxt.count(c))
		s[p].nxt[c]=cur,p=s[p].link;
	if(!p) s[cur].link=1;
	else
	{
		int q=s[p].nxt[c];
		if(s[p].len+1==s[q].len) s[cur].link=q;
		else
		{
			int v=++cnt;
			s[v]=s[q],s[v].len=s[p].len+1;
			while(p&&s[p].nxt[c]==q)
				s[p].nxt[c]=v,p=s[p].link;
			s[q].link=v,s[cur].link=v;
		}
	}
	f[cur]=1,lst=cur;
}
void O()
{
	for(int i=1;i<=cnt;i++) d[s[i].len]++;
	for(int i=1;i<=cnt;i++) d[i]+=d[i-1];
	for(int i=1;i<=cnt;i++) id[d[s[i].len]--]=i;
	for(int i=cnt;i>=1;i--)
	{
		if(tp) f[s[id[i]].link]+=f[id[i]];
		else f[id[i]]=1;
	}
	f[1]=0;
	for(int i=cnt;i>=1;i--)
	{
		sz[id[i]]=f[id[i]];
		for(char j='a';j<='z';j++)
			if(s[id[i]].nxt.count(j))
				sz[id[i]]+=sz[s[id[i]].nxt[j]];
	}
}
void query(int k)
{
	if(k>sz[1])
	{
		puts("-1");
		return;
	}
	int p=1;
	while(k) for(char i='a';i<='z';i++)
		if(s[p].nxt.count(i))
		{
			if(sz[s[p].nxt[i]]>=k)
			{
				k-=f[s[p].nxt[i]],putchar(i),p=s[p].nxt[i];
				break;
			}
			else k-=sz[s[p].nxt[i]];
		}
}
signed main()
{
	cnt=1,lst=1;
	scanf("%s",a+1);
	n=strlen(a+1);
	for(int i=1;i<=n;i++) add(a[i]);
	int k;
	scanf("%lld%lld",&tp,&k);
	O(),query(k);
	return 0;
}
原文地址:https://www.cnblogs.com/zhs1/p/14283678.html