【后缀自动机】自动机<->后缀树<->后缀数组

23 篇文章 0 订阅
13 篇文章 0 订阅

一直都说自动机建后缀树,一直没真正构出后缀树过...

其实建后缀树很简单,父亲边已经有了,关键是边代表的子串怎么求。从叶子一层一层向上,对于节点i,我们已经知道了它在原串的位置和逆序后缀长度,他的父亲的逆序后缀长度也知道,父亲又是i的逆序后缀的前缀,这就可以直接在原串定位了。

另外,通过父亲边还可以求出节点i可接受的最短子串,设i的最长子串长度为maxlen[i],最短子串长度为minlen[i],显然一开始如果没有别的节点分出i的子串,i接受的最短子串长度是1,后来rt[i]分出了i的子串,而一个节点的接受的子串长度是连续的,因此minlen[i]~maxlen[rt[i]]的子串都被分走了,那么新的minlen[i]=maxlen[rt[i]]+1,因此最后扫一遍就行了。

USACO DEC07 bclgold

输入一个串 s,t 一开始是空串。每次取出 s 的第一个字符或最后一个字符,删去,并加到 t 的串尾。求最小字典序的 t。

这道题用后缀数组就可以直接贪心了,虽说后缀数组更加方便,不过为了试建后缀树,还是用了后缀自动机。建立后缀树后,dfs序就可以求出后缀数组了。

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=200000;
int next[maxn][27],net[maxn][27],tail[maxn],l[maxn],vol[maxn],sa[maxn],rk[maxn],rt[maxn],ws[maxn];
int n,s1,s2,last;
char a[maxn];
void suffix_sam(int i,int &last)
{
	int chr=a[i]-a[n+1],x,y;
	s1++,l[s1]=l[last]+1,vol[s1]=i,tail[s1]=i;
	for (x=last,last=s1;x && (!next[x][chr]);x=rt[x]) next[x][chr]=s1;
	y=next[x][chr];
	if (!y) next[x][chr]=s1,rt[s1]=0;
	else if (l[x]+1==l[y]) rt[s1]=y;
	else {
		s1++,l[s1]=l[x]+1;
		for (int j=0;j<=26;j++) next[s1][j]=next[y][j];
		rt[s1]=rt[y],rt[y]=s1,rt[last]=s1;
		for (;x && (next[x][chr]==y);x=rt[x]) next[x][chr]=s1;
		if (next[x][chr]==y) next[x][chr]=s1;
	}
}
void link(int x,int y,int z){net[x][z]=y;}
void suffix_tree()
{
	for (int i=0;i<=n+n+1;i++) ws[i]=0;
	for (int i=0;i<=s1;i++) ws[l[i]]++;
	for (int i=1;i<=n+n+1;i++) ws[i]+=ws[i-1];
	for (int i=s1;i>=0;i--) sa[--ws[l[i]]]=i;
	for (int i=s1;i>=0;i--) {
		int ne=sa[i];
		if (!vol[rt[ne]]) vol[rt[ne]]=vol[ne];
		link(rt[ne],ne,a[vol[ne]-l[rt[ne]]]-a[n+1]);
	}
}
void dfs(int x)
{
	if (tail[x]) sa[++s2]=tail[x],rk[tail[x]]=s2;
	for (int i=0;i<=26;i++)		
		if (net[x][i]) dfs(net[x][i]);
}
void suffix_array()
{
	for (int i=0;i<=s1;i++) sa[i]=0;
	s2=0;
	dfs(0);
}
int main()
{
	freopen("bclgold.in","r",stdin);
	freopen("bclgold.out","w",stdout);
	scanf("%d\n",&n);
	for (int i=1;i<=n;i++) scanf("%c\n",&a[i]),a[n+n-i+1+1]=a[i];
	a[n+1]='A'-1;
	last=s1=0;
	for (int i=1;i<=n+n+1;i++) suffix_sam(i,last);
	suffix_tree();
	suffix_array();
	for (int i=1,j=1,k=n;i<=n;i++) {
		int nj=rk[n+n+1-j+1],nk=rk[k];
		if (nj<nk) printf("%c",a[j++]);
		else printf("%c",a[k--]);
        if (i%80==0) printf("\n");		
	}
	return 0;
}

hdu4416

给出一个A串,给出若干个B串,问A串中有几许个不合的子串不是B中的子串

用a串建立后缀自动机,然后b串在a串上跑,更新每个节点可接受的最短长度(注意lazy一下),对与每个节点i,我们只需知道它可接受的最短长度,然后(最长长度-最短长度+1)就是可接受的子串。

hdu的数据真是坑,一开始我完全不计算常数,结果tle的一塌糊涂,然后发现我额外在自动机上用所有转移边求初始时的最短长度多花了一倍的时间,于是改成用父亲边求,这样还是2900+ms卡着时限过。但是我在本机上测,明显要比网上的标称快,而且我也不认为自动机的常数大得过后缀数组,于是我把每次清空转移边改成时间戳,然后就只有600+ms...这是有多少组数据啊...

后缀自动机还是能不跑所有转移边就不跑,多组数据还是用时间戳吧...

#include <ctime>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m,L,s1,last,t,test;
char a[200000],b[200000];
int next[200000][26],rt[200000],len[200000],ws[200000],sa[200000],nex[200000][26];
int g[200000];
bool bj[200000];
void init()
{
    scanf("%d\n",&m);
    scanf("%s\n",a+1);
    n=strlen(a+1);
    last=s1=0;
    g[0]=0;
    for (int i=1;i<=n;i++) {
        int chr=a[i]-'a',x,y;
        ++s1,len[s1]=len[last]+1,rt[s1]=0;
        for (x=last,last=s1;x && (t!=nex[x][chr]);x=rt[x]) next[x][chr]=s1,nex[x][chr]=t;
        y=next[x][chr];
        if (t!=nex[x][chr]) next[x][chr]=s1,nex[x][chr]=t;
        else if (len[x]+1==len[y]) rt[s1]=y;
        else {
            ++s1,len[s1]=len[x]+1,rt[s1]=0;
			for (int j=0;j<=25;j++) next[s1][j]=next[y][j],nex[s1][j]=nex[y][j];
            rt[s1]=rt[y],rt[y]=s1,rt[last]=s1;
            for (;x && ((nex[x][chr]==t) && (next[x][chr]==y));x=rt[x]) next[x][chr]=s1;
            if ((nex[x][chr]==t) && (next[x][chr]==y)) next[x][chr]=s1;
        }
    }
    for (int i=0;i<=n;i++) ws[i]=0;
    for (int i=0;i<=s1;i++) ws[len[i]]++;
    for (int i=1;i<=s1;i++) ws[i]+=ws[i-1];
    for (int i=s1;i>=0;i--) sa[--ws[len[i]]]=i;
	for (int i=1;i<=s1;i++) g[i]=len[rt[i]]+1;
    memset(bj,0,sizeof(bj));
    for (int i=1;i<=m;i++) {
        scanf("%s\n",b+1);
        int L=strlen(b+1),s=0,sum=0;
        for (int j=1;j<=L;j++) {
            for (;s && (t!=nex[s][b[j]-'a']);s=rt[s],sum=len[s]) ;
            if (t==nex[s][b[j]-'a']) {
                s=next[s][b[j]-'a'];sum++;
                g[s]=max(g[s],sum+1);
                bj[s]=1;
            }
        }
    }
    long long ans=0;
    for (int i=s1;i>=1;i--) {
        int ne=sa[i],na=rt[ne];
        if (bj[ne]) g[na]=max(g[na],g[ne]),bj[na]=1;
        ans+=(len[ne]-min(g[ne],len[ne]+1)+1);
    }
    printf("Case %d: %I64d\n",test-t+1,ans);
}
int main()
{
	freopen("input.txt","r",stdin);
	freopen("output.txt","w",stdout);    
    scanf("%d\n",&t);
    for (test=t;t;t--) init();
    return 0;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值