后缀自动机

下面的 \(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\n",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;
}
posted @ 2021-01-21 08:37  BBD186587  阅读(108)  评论(0编辑  收藏  举报