后缀自动机初步

27 篇文章 0 订阅
25 篇文章 1 订阅

想学这个算法很久了

省选结束后终于下定决心好好学学!

先推荐两篇文章

http://hi.baidu.com/myidea/item/142c5cd45901a51820e25039?qq-pf-to=pcqq.group

http://blog.csdn.net/huyuncong/article/details/7583214

学好后缀自动机的关键就是理解其转移边与父亲变得不同意义。

同时理解一些后缀树的性质有助于我们理解后缀自动机的工作原理。

1、nsubstr

求各个长度的子串最多出现了多少次。

后缀自动机上的每个节点能够匹配的字符串的长度一定是从他能匹配的最短的子串到最长的子串一段连续区间

考虑后缀树中如何求一个子串的出现次数,即将此子串在树上匹配后最后一个节点下的叶子节点数为答案。(先将所有点bfs一遍,再逆序扫描利用父亲边转移即可)

我们可以用这个点更新他能匹配的长度最大值,很明显长度为i的可以更新长度为i-1的

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int Maxn=500000;
char S[Maxn];
int son[Maxn][26],fa[Maxn],len[Maxn];
int r[Maxn],f[Maxn],s[Maxn],t[Maxn];
int n,cnt,last,i,j;

void add(int x){
  int c = S[x]-'a';
  int np = ++cnt, p = last;
  last = np; len[np] = x;
  for (;p&&!son[p][c];p=fa[p]) son[p][c] = np;
  if (!p) fa[np] = 1;
  else
  {
  	int q = son[p][c];
  	if (len[p]+1==len[q]) fa[np] = q;
  	else
  	{
  	  int nq = ++cnt; len[nq] = len[p]+1;
  	  memcpy(son[nq],son[q],sizeof(son[q]));
  	  fa[nq] = fa[q];
  	  fa[np] = fa[q] = nq;
  	  for (;son[p][c]==q;p=fa[p]) son[p][c]=nq;
  	}
  }
}

int main(){
  freopen("spoj8222.in","r",stdin);
  freopen("spoj8222.out","w",stdout);
  scanf("%s",S+1);
  n = strlen(S+1);
  last = ++cnt;
  for (i=1;i<=n;i++) add(i);
  
  for (i=1,j=1;i<=n;i++)
    j = son[j][S[i]-'a'], r[j]++;
  for (i=1;i<=cnt;i++) s[len[i]]++;
  for (i=1;i<=n;i++) s[i]+=s[i-1];
  for (i=1;i<=cnt;i++) t[s[len[i]]--]=i;
  for (i=cnt;i>0;i--) r[fa[t[i]]]+=r[t[i]];
  for (i=1;i<=cnt;i++) f[len[i]]=max(f[len[i]],r[i]);
  for (i=n-1;i>0;i--) f[i]=max(f[i],f[i+1]);
  for (i=1;i<=n;i++) printf("%d\n",f[i]);
  return 0;
}

2、spoj  lcs

求两个串的最长公共子串

把一个串建后缀自动机,另一个串在这个自动机场匹配即可,匹配失败可以按照父亲边向上跳(和ac自动机比较像)。

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int Maxn=500005;
char S[Maxn];
int son[Maxn][26],fa[Maxn],len[Maxn];
int n,i,j,last,cnt,sum,ans;

void add(int x){
  int c=S[i]-'a';
  int np = ++cnt, p = last;
  last = np; len[np] = x;
  for (;p&&son[p][c]==0;p=fa[p]) son[p][c]=np;
  if (!p) fa[np] = 1;
  else
  {
  	int q = son[p][c];
  	if (len[p]+1 == len[q]) fa[np] = q;
  	else
  	{
  	  int nq = ++cnt; len[nq] = len[p]+1;
  	  memcpy(son[nq],son[q],sizeof(son[q]));
  	  fa[nq] = fa[q];
  	  fa[np] = fa[q] = nq;
  	  for (;son[p][c]==q;p=fa[p]) son[p][c]=nq;
  	}
  }
}

int main(){
  freopen("spoj.in","r",stdin);
  freopen("spoj.out","w",stdout);
  scanf("%s",S+1);
  n = strlen(S+1);
  last = ++cnt;
  for (i=1;i<=n;i++) add(i);
  scanf("%s",S+1);
  n = strlen(S+1);
  for (i=1,j=1,sum=0;i<=n;i++){
  	int c=S[i]-'a';
  	for (;j&&son[j][c]==0;sum=len[j=fa[j]]);
  	if (son[j][c]) sum++, j=son[j][c];
  	if (j==0) j=1;
	ans = max(ans, sum); 
  }
  printf("%d\n",ans);
  return 0;
}

3、spoj lcs2

求不超过十个串的最长公共子串

还是把第一个串建立后缀自动机,剩下的串在自动机上匹配,每个节点上记录那些点匹配及匹配长度,取所有串的最小值即为这个点的最长子串长,取所有串上的最大值即可。

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int Maxn=200005;
char S[10][Maxn];
int fa[Maxn],son[Maxn][26],len[Maxn];
int f[Maxn][10],s[Maxn],t[Maxn];
int N,cnt,i,j,k,sum,n,last,ans;

void add(int x){
  int c = S[0][x]-'a';
  int np = ++cnt, p = last;
  last = np; len[np] = x+1;
  for (;p&&son[p][c]==0;p=fa[p]) son[p][c]=np;
  if (!p) fa[np] = 1;
  else
  {
  	int q = son[p][c];
  	if (len[q] == len[p]+1) fa[np] = q;
  	else
  	{
  	  int nq = ++cnt; len[nq] = len[p]+1;
  	  memcpy(son[nq],son[q],sizeof(son[q]));
  	  fa[nq] = fa[q];
  	  fa[np] = fa[q] = nq;
  	  for (;p&&son[p][c]==q;p=fa[p]) son[p][c]=nq;
  	}
  }
}

int main(){
  //freopen("spoj.in","r",stdin);
  //freopen("spoj.out","w",stdout);
  while (~scanf("%s",S[N])) N++;
  n = strlen(S[0]);
  last = ++cnt;
  for (i=0;i<n;i++) add(i);
  for (i=1;i<N;i++){
  	n = strlen(S[i]);
  	for (j=0,k=1,sum=0;j<n;j++){
  	  int c = S[i][j]-'a';
  	  for (;k&&son[k][c]==0;sum=len[k=fa[k]]);
  	  if (son[k][c]) k=son[k][c], f[k][i]=max(f[k][i],++sum);
  	  if (k==0) k=1;
  	}
  }
  n = len[last];
  for (i=1;i<=cnt;i++) s[len[i]]++;
  for (i=1;i<=n;i++) s[i]+=s[i-1];
  for (i=1;i<=cnt;i++) t[s[len[i]]--]=i;
  for (i=cnt;i>0;i--)
    for (j=1;j<N;j++)
      f[fa[t[i]]][j] = max( f[fa[t[i]]][j], min(f[t[i]][j],len[fa[t[i]]]) );
  for (i=1;i<=cnt;i++){
    int tmp=10000000;
    for (j=1;j<N;j++)
      tmp = min(tmp,f[i][j]);
    ans = max(ans,tmp);
  }
  printf("%d\n",ans);
  return 0;
}


4、spoj  sublex

求严格的第k小子串

我们现在需要知道每个节点向下可以在匹配出多少个子串,可以利用转移边dp出答案,然后按照从小到大的顺序扫描转移即可。

注意多组询问,直接扫描'a'-'z'可能会超时,把儿子压缩一下减少每次扫描的数量

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int Maxn=200005;
char S[Maxn],lt[Maxn];
int son[Maxn][26],len[Maxn],fa[Maxn],size[Maxn];
int n,q,k,i,j,cnt,last,ct,s[Maxn],t[Maxn];

void add(int x){
  int c = S[x]-'a';
  int np = ++cnt, p = last;
  last = np; len[np] = x; lt[np] = 'a'+c;
  for (;p&&son[p][c]==0;p=fa[p]) son[p][c]=np;
  if (!p) fa[np] = 1;
  else
  {
    int q = son[p][c];
    if (len[p]+1 == len[q]) fa[np] = q;
    else
    {
      int nq = ++cnt; len[nq] = len[p]+1;
      memcpy(son[nq],son[q],sizeof(son[q]));
      fa[nq] = fa[q]; fa[np] = fa[q] = nq;
      for (;p&&son[p][c]==q;p=fa[p]) son[p][c]=nq;
      lt[nq] = 'a'+c;
    }
  }
}

void answer_questions(){
  scanf("%d",&q);
  while (q--){
    scanf("%d",&k);
    //if (k>=size[1]) k=size[1]-1;
    for (i=1;k!=0;i=son[i][j],k--){
      for (j=0;j<26&&son[i][j];j++)
      if (k>size[son[i][j]]) k-=size[son[i][j]];
	else break;
      printf("%c",lt[son[i][j]]);
    }
    printf("\n");
  }
}

int main(){
  //freopen("sublex.in","r",stdin);
  //freopen("sublex.out","w",stdout);
  scanf("%s",S+1);
  n = strlen(S+1);
  last = ++cnt;
  for (i=1;i<=n;i++) add(i);

  for (i=1;i<=cnt;i++) s[len[i]]++;
  for (i=1;i<=n;i++) s[i]+=s[i-1];
  for (i=1;i<=cnt;i++) t[s[len[i]]--]=i;
  for (i=cnt;i>0;i--){
    size[t[i]] = 1;
    for (j=0;j<26;j++)
      size[t[i]] += size[son[t[i]][j]];
  }

  for (i=1;i<=cnt;i++)
    for (j=0,ct=0;j<26;j++)
    if (son[i][j]) son[i][ct++]=son[i][j];
  answer_questions();
  return 0;
}
5、codeforces 235C

陈老师的题,先跪orz

把母串建立后缀自动机,预处理出每个节点匹配的穿在原串中出现了多少次(用第1题的方法)。

找到询问串的循环节(每个串的后移最早出现循环就出现在第一个循环节上),在串尾复制一遍

将处理好的串在自动机上匹配。

每次走到匹配好的点,需要沿着父亲边跳直到刚好卡到串长(保证计数不漏),将出现次数加到答案中

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int Maxn=2000005;
char S[Maxn],T[Maxn];
int son[Maxn][26],len[Maxn],fa[Maxn],pre[Maxn];
int n,last,cnt,i,j,k,q,sum,ans,r[Maxn],b[Maxn],t[Maxn];

void add(int x){
  int c = S[x]-'a';
  int np = ++cnt, p = last;
  last = np; len[np] = x;
  for (;p&&son[p][c]==0;p=fa[p]) son[p][c] = np;
  if (!p) fa[np] = 1;
  else
  {
  	int q = son[p][c];
  	if (len[p]+1 == len[q]) fa[np] = q;
  	else
  	{
  	  int nq = ++cnt; len[nq] = len[p]+1;
  	  memcpy(son[nq],son[q],sizeof(son[nq]));
  	  fa[nq] = fa[q]; fa[np] = fa[q] = nq;
  	  for (;p&&son[p][c]==q;p=fa[p]) son[p][c]=nq;
  	}
  }
}

int calc_repetend(){  //计算循环节 
  pre[1] = 0;
  for (int i=2,j=0;i<=n;i++){
  	while (j&&T[j+1]!=T[i]) j=pre[j];
  	if (T[j+1]==T[i]) j++;
  	pre[i] = j;
  }
  int ret = n-pre[n];
  if (n%ret) ret = n;
  return ret;
}

int main(){
  //freopen("235C.in","r",stdin);
  //freopen("235C.out","w",stdout);
  scanf("%s",S+1);
  n = strlen(S+1);
  last = ++cnt;
  for (i=1;i<=n;i++) add(i);
  
  for (i=1,j=1;i<=n;i++)
    j=son[j][S[i]-'a'], r[j]++;
  for (i=1;i<=cnt;i++) b[len[i]]++;
  for (i=1;i<=n;i++) b[i]+=b[i-1];
  for (i=1;i<=cnt;i++) t[b[len[i]]--]=i;
  for (i=cnt;i>0;i--)
  	r[fa[t[i]]] += r[t[i]];
  scanf("%d",&q);
  while (q--){
  	scanf("%s",T+1);
  	n = strlen(T+1);
  	k = calc_repetend();
  	for (i=n+1;i<n+k;i++) T[i]=T[i-n];
  	for (i=1,j=1,sum=0,ans=0;i<n+k;i++){
  	  int c = T[i]-'a';
	  for (;j&&son[j][c]==0;sum=len[j=fa[j]]);
	  if (son[j][c]) j = son[j][c], sum++;
	  if (j==0) j=1;
	  if (i>=n && sum>=n){
	  	while (len[fa[j]]>=n)
	  	  sum = len[j=fa[j]];
	    ans += r[j];
	  }
	}
	printf("%d\n",ans);
  }
  return 0;
}

6、JLOI2015 弦论

求严格/不严格的第k小串。

其实是sublex的升级版。

严格的不用讲了吧,不严格的其实也很简单。

回顾一下“严格”其实是在说每个节点只匹配了一个点,不严格的话就是匹配了他在母串中的出现次数。

剩下的还用说吗?

#include <cstdio>
#include <cstring>
#include <algorithm>
  
using namespace std;
const int Maxn=1000005;
char S[Maxn],lt[Maxn];
int len[Maxn],fa[Maxn],son[Maxn][26];
int b[Maxn],t[Maxn];
int n,cnt,last,i,j,K,T;
long long r[Maxn],s[Maxn];
  
void add(int x){
  int c = S[x] - 'a';
  int np = ++cnt, p = last;
  last = np; len[np] = x; lt[np] = S[x];
  for (;p&&son[p][c]==0;p=fa[p]) son[p][c]=np;
  if (!p) fa[np] = 1;
  else
  {
    int q = son[p][c];
    if (len[p]+1 == len[q]) fa[np] = q;
    else
    {
      int nq = ++cnt; len[nq] = len[p]+1;
      memcpy(son[nq],son[q],sizeof(son[q]));
      fa[nq] = fa[q]; fa[np] = fa[q] = nq;
      for (;p&&son[p][c]==q;p=fa[p]) son[p][c]=nq;
      lt[nq] = S[x];
    }
  }
  r[ last ] ++;
}
  
int main(){
  //freopen("3998.in","r",stdin);
  //freopen("3998.out","w",stdout);
  scanf("%s",S+1);
  n = strlen(S+1);
  last = ++cnt;
  for (i=1;i<=n;i++) add(i);
  scanf("%d%d",&T,&K);
  //for (i=1;i<=cnt;i++) r[i]++;
  for (i=1;i<=cnt;i++) b[len[i]]++;
  for (i=1;i<=n;i++) b[i]+=b[i-1];
  for (i=1;i<=cnt;i++) t[b[len[i]]--]=i;
  for (i=cnt;i>0;i--)
    r[fa[t[i]]] += r[t[i]];
  if (T==0)
    for (i=1;i<=cnt;i++) r[i]=(bool)r[i];
  r[1] = 0;
  for (i=cnt;i>0;i--){
    s[t[i]] = r[t[i]];
    for (j=0;j<26;j++)
      s[t[i]] += s[son[t[i]][j]];
  }
  s[0] = r[0] = 0;
  if (s[1]<K) {printf("-1\n"); return 0;}
  for (i=1;K>0;K-=r[i]){
    for (j=0;j<26;j++)
    if (s[son[i][j]]<K) K -= s[son[i][j]];
      else break;
    i=son[i][j];
    printf("%c",lt[i]);
  }
  printf("\n");
  return 0;
}


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值