发链:http://neroysq.blogcn.com/articles/%E5%90%8E%E7%BC%80%E8%87%AA%E5%8A%A8%E6%9C%BA%E5%88%9D%E6%8E%A2.html
http://blog.sina.com.cn/s/blog_7812e98601012cim.html
详细构造见上述链接,此处介绍性质与理解
后缀自动机具有两大性质,考虑转移边为自动机,考虑父亲边为逆序后缀树(后缀数组为后缀树的dfs序),如果忽略字符集大小的常数,构造复杂度为o(n)
很好很强大,更深的性质呢?
对于一个节点i,根s
1、整个自动机可以接受字符串的所有子串,且自动机为一个有向拓扑图
2、一条到i的路径唯一对应一个子串
3、所有i可以接受的状态互相成后缀关系,且长度连续
4、对于i的父亲rt[i],rt[i]所能接受的所有子串都是i所能接受的所有子串的后缀
构造后缀自动机的时候有一步是判断l[a]+1==l[b],为的是防止多余状态被自动机接受,如果l[a]+1==l[b],那么b所能接受的最长子串是合法的,根据3,4可知所有其他状态也都是合法的,否则l[a]+1!=l[b]可能会出现不合法状态,那么新建节点使l[c]=l[a]+1就可以筛出合法状态,而rt指针的修改可以看作是逆序后缀树的压缩边被解压缩出一个节点并添加一个新的叶子节点(即新逆序后缀)
5、统计方式的理解。从后续题目来看有两种统计方法,一是根据转移边dp,二是根据父亲边dp,其实是统计两个不同东西,转移边dp可以统计出有多少子串以此节点接受的状态为前缀,根据父亲边dp可以统计出以该节点接受的最长子串为后缀的子串有多少(逆序后缀树i所代表的是i所接受最长子串)
6、其实字典树也可以建立后缀自动机,考虑我们对于串建的时候,记last是为了找出最后一个的最长子串,而在字典树上就是字典树的父亲,所以按dfs序及父亲就可以建出字典树的后缀自动机
7、(逆序后缀树i所代表的是i所接受最长子串)这句话的意思是,将该串所有的逆序后缀建立一棵后缀树,而自动机中的每个节点i,它在后缀树中一直沿父亲边走向根\所代表的逆序后缀==节点i在自动机中接受到的最长子串
总而言之,后缀自动机兼有两种特性,如果想知道后缀的公共前缀之类,或子串重复次数等等与后缀联系的上的,可以用后缀树的性质,如果与所有子串相关,自动机就比较好用
1.SPOJ NSUBSTR
题意:给一个字符串S,令F(x)表示S的所有长度为x的子串中,出现次数的最大值。求F(1)..F(Length(S)) (感谢clj的翻译>_<)
用逆序后缀树的话叶子数就是出现次数(可以基排后直接统计,那时理解还不是很深,所以真的把树建出来了)
#include
#include
#include
int tail[500001],net[2000000],sora[2000000],len[500001],f[500001],rt[500001];
int next[500001][27],ans[500001],s1,ss;
char ch[500001];
void dfs(int x)
{
int i,ne;
f[x]=(x==tail[x]) ? 1 : 0;
for (i=x;net[i]!=0;) {
i=net[i],ne=sora[i];
if (ne==rt[x]) continue;
dfs(ne);
f[x]+=f[ne];
}
if (f[x]>ans[len[x]]) ans[len[x]]=f[x];
}
void origin() {for (int i=1;i<=s1;i++) tail[i]=i;ss=s1;}
void link(int x,int y)
{
ss++,net[tail[x]]=ss,tail[x]=ss,sora[ss]=y;
}
void init()
{
int l,i,ne,last,x,y,j;
scanf("%s\n",ch+1);
l=strlen(ch+1);ch[0]='z'+1;
next[0][ch[0]-'a']=++s1,rt[s1]=0,len[s1]=1,last=s1;
for (i=1;i<=l;i++) {
ne=ch[i]-'a';++s1;len[s1]=i+1;
for (x=last,last=s1;!next[x][ne]&& x;x=rt[x]) next[x][ne]=s1;
y=next[x][ne];
if (!y) {next[x][ne]=s1;continue;}
if (len[x]+1==len[y]) rt[s1]=y;
else {
s1++;len[s1]=len[x]+1;
for (j=0;j<=26;j++) next[s1][j]=next[y][j];
rt[last]=s1;rt[s1]=rt[y],rt[y]=s1;
for (;x;x=rt[x]) if (next[x][ne]==y) next[x][ne]=s1;else break;
if (next[x][ne]==y) next[x][ne]=s1;
}
}
origin();
for (i=1;i<=s1;i++) link(rt[i],i);
dfs(0);
for (i=1;i<=l;i++) printf("%d\n",ans[i]);
}
int main()
{
freopen("spoj8222.in","r",stdin);
freopen("spoj8222.out","w",stdout);
init();
return 0;
}
2.SPOJ LCS
题意:求两个字符串的最长公共子串
由于后缀自动机能接受所有子串,所以对其中一个建好后缀自动机后,用另一个在自动机上匹配(匹配失败可以由rt指针找出最长公共后缀子串),就可以求出任意位置能匹配的最长长度
#include
#include
#include
int ans,sum,len[500000],next[500000][26],rt[500000],last,s1;
char ch[500000];
void init()
{
int i,j,l,s,ne,x,y;
scanf("%s\n",ch+1);
l=strlen(ch+1);
for (i=1,last=0;i<=l;i++) {
ne=ch[i]-'a';
for (x=last,last=++s1;x && !next[x][ne];x=rt[x]) next[x][ne]=s1;
y=next[x][ne];len[s1]=i;
if (!y) {next[x][ne]=s1;continue;}
else {
if (len[x]+1==len[y]) rt[s1]=y;
else {
len[++s1]=len[x]+1;
for (j=0;j<='z'-'a';j++) next[s1][j]=next[y][j];
rt[last]=s1,rt[s1]=rt[y],rt[y]=s1;
for (;x && next[x][ne]==y;x=rt[x]) next[x][ne]=s1;
if (next[x][ne]==y) next[x][ne]=s1;
}
}
}
scanf("%s\n",ch+1);
l=strlen(ch+1);
for (s=0,i=1,ans=sum=0;i<=l;i++) {
ne=ch[i]-'a';
for (;s && !next[s][ne];sum=len[s=rt[s]]) ;
if (next[s][ne]) sum++,s=next[s][ne];
if (sum>ans) ans=sum;
}
printf("%d\n",ans);
}
int main()
{
freopen("spoj1811.in","r",stdin);
freopen("spoj1811.out","w",stdout);
init();
return 0;
}
3.SPOJ LCS2
题意:求n个字符串的最长公共子串(n<=10)
按9个字符串建的话,ldl超时了,所以不妨按一个建,用其余9个在上面匹配,注意如果一个点的某子串被匹配了,那他父亲代表的所有子串都会被匹配,因为满足后缀关系(参见之前性质分析)
#include
#include
#include
int f[200001][10],rt[200001],next[200001][26],st[200001],len[200001],g[200001],ws[200001];
int s1,last,ans,sum,tim;
char ch[100001];
void init()
{
int i,l,j,x,y,ne,s;
scanf("%s\n",ch+1);
l=strlen(ch+1);
for (last=0,i=1;i<=l;i++) {
ne=ch[i]-'a';
for (x=last,last=++s1;!next[x][ne] && x;x=rt[x]) next[x][ne]=s1;
y=next[x][ne],len[s1]=i;
if (!y) {next[x][ne]=s1;continue;}
else {
if (len[x]+1==len[y]) rt[s1]=y;
else {
len[++s1]=len[x]+1;
for (j=0;j<='z'-'a';j++) next[s1][j]=next[y][j];
rt[last]=s1,rt[s1]=rt[y],rt[y]=s1;
for (;x && next[x][ne]==y;x=rt[x]) next[x][ne]=s1;
if (next[x][ne]==y) next[x][ne]=s1;
}
}
}
for (i=1;i<=l;i++) ws[i]=0;
for (i=1;i<=s1;i++) ws[g[i]=len[i]]++;
for (i=1;i<=l;i++) ws[i]+=ws[i-1];
for (i=s1;i>=1;i--) st[ws[len[i]]--]=i;
for (;!(scanf("%s\n",ch+1)==EOF);) {
l=strlen(ch+1);
tim++;
for (s=0,sum=0,i=1;i<=l;i++) {
ne=ch[i]-'a';
for (;s && !next[s][ne];sum=len[s=rt[s]]) ;
if (next[s][ne]) sum++,s=next[s][ne];
f[s][tim]=(sum>f[s][tim]) ? sum : f[s][tim];
}
for (i=s1;i>=1;i--) {
s=st[i];
f[rt[s]][tim]=(f[rt[s]][tim]>f[s][tim]) ? f[rt[s]][tim] : f[s][tim];
g[s]=(g[s]ans) ? g[st[i]] : ans;
printf("%d\n",ans);
}
int main()
{
// freopen("spoj1812.in","r",stdin);
// freopen("spoj1812.out","w",stdout);
init();
return 0;
}
4.SPOJ SUBLEX
题意:给定一个字符串,用它的所有子串建立一棵字母树,每次找第k小的子串。
自动机可以接受所有子串,如果我们知道一个节点能拓展出多少状态(实际是可由dp完成),那么直接按顺序在自动机上走,就可以o(n)的回答询问(实际是26n,spoj上会超时,要用hash将转移边优化一下)
#include
#include
#include
int f[180001],st[180001],len[180001],rt[180001],next[180001][26],ws[180001],net[500000],tail[180001],sora[500000],pow[500000];
int m,last,s1,ss;
char ch[180001];
void link(int x,int y,int i)
{
ss++,net[tail[x]]=ss,tail[x]=ss,sora[ss]=y,pow[ss]=i;
}
void init()
{
int i,j,l,x,y,ne,s,k;
scanf("%s\n",ch+1);
l=strlen(ch+1);
for (last=0,i=1;i<=l;i++) {
ne=ch[i]-'a';
for (x=last,last=++s1;x && !next[x][ne];x=rt[x]) next[x][ne]=s1;
y=next[x][ne],len[s1]=i;
if (!y) {next[x][ne]=s1;continue;}
else {
if (len[x]+1==len[y]) rt[s1]=y;
else {
len[++s1]=len[x]+1;
for (j=0;j<='z'-'a';j++) next[s1][j]=next[y][j];
rt[last]=s1,rt[s1]=rt[y],rt[y]=s1;
for (;x && next[x][ne]==y;x=rt[x]) next[x][ne]=s1;
if (next[x][ne]==y) next[x][ne]=s1;
}
}
}
for (i=1;i<=s1;i++) ws[len[i]]++,tail[i]=i;ss=s1;
for (i=1;i<=l;i++) ws[i]+=ws[i-1];
for (i=s1;i>=1;i--) st[ws[len[i]]--]=i;
for (i=s1;i>=0;i--) {
x=st[i],f[x]++;
for (j=0;j<='z'-'a';j++) {
if (!next[x][j]) continue;
f[x]+=f[next[x][j]];
link(x,next[x][j],j);
}
}
scanf("%d\n",&m);
for (;m;m--) {
scanf("%d\n",&k);
for (s=0;k;) {
for (i=s;net[i]!=0;) {
i=net[i],ne=sora[i];
if (f[ne]
多串建后缀自动机,通常的方法是加入最小字符进行分割,但是其实不需要这么复杂,只需要每个串都重新从根节点开始插入就可以了,这样子会产生一些冗余节点(没有从根到它的路径),但是能遍历到的部分都是正确的,其实就相当于先建了一棵字母树,然后每个节点的last就是它在字母树上的祖先,然后依照这个字母树建后缀自动机
updata:不会产生冗余节点的做法(冗余节点在运用父亲边的时候可能会出问题),如果先建字母树,再建自动机是不会出问题的,但是不需要这么麻烦,只需边插边走的时候判断一下下一个节点是不是还在最长链上即可,这样就保证走的是字母树的边
hdu4436
#include
#include
#include
#include
#include
const int mo=2012;
using namespace std;
int ss,n,f[2000000],next[500000][10],l[2000000],rt[2000000],g[2000000];
char ch[2000000];
int u[500000];
void add(int &last,int chr)
{
int x,y;
ss++,l[ss]=l[last]+1;
for (x=last,last=ss;x && (!next[x][chr]);x=rt[x]) next[x][chr]=ss;
y=next[x][chr];
if (!y) next[x][chr]=ss,rt[ss]=0;
else if (l[x]+1==l[y]) rt[ss]=y;
else {
++ss,l[ss]=l[x]+1;
for (int j=0;j<=9;j++) next[ss][j]=next[y][j];
rt[ss]=rt[y],rt[y]=ss,rt[last]=ss;
for (;x && (next[x][chr]==y);x=rt[x]) next[x][chr]=ss;
if (next[x][chr]==y) next[x][chr]=ss;
}
}
void origin()
{
for (int i=0;i<=ss;i++) {
l[i]=rt[i]=0;
for (int j=0;j<=9;j++)
next[i][j]=0;
}
ss=0;
}
bool cmp(int i,int j)
{
return l[i]
hdu4436(updata版)
#include
#include
#include
#include
#include
const int mo=2012;
using namespace std;
int ss,n,f[2000000],next[500000][10],l[2000000],rt[2000000],g[2000000];
char ch[2000000];
int u[500000];
void add(int &last,int chr)
{
int x,y;
ss++,l[ss]=l[last]+1;
for (x=last,last=ss;x && (!next[x][chr]);x=rt[x]) next[x][chr]=ss;
y=next[x][chr];
if (!y) next[x][chr]=ss,rt[ss]=0;
else if (l[x]+1==l[y]) rt[ss]=y;
else {
++ss,l[ss]=l[x]+1;
for (int j=0;j<=9;j++) next[ss][j]=next[y][j];
rt[ss]=rt[y],rt[y]=ss,rt[last]=ss;
for (;x && (next[x][chr]==y);x=rt[x]) next[x][chr]=ss;
if (next[x][chr]==y) next[x][chr]=ss;
}
}
void origin()
{
for (int i=0;i<=ss;i++) {
l[i]=rt[i]=0;
for (int j=0;j<=9;j++)
next[i][j]=0;
}
ss=0;
}
bool cmp(int i,int j)
{
return l[i]