首页 > 代码库 > hihocoder #1465 : 后缀自动机五·重复旋律8
hihocoder #1465 : 后缀自动机五·重复旋律8
#1465 : 后缀自动机五·重复旋律8
描述
小Hi平时的一大兴趣爱好就是演奏钢琴。我们知道一段音乐旋律可以被表示为一段数构成的数列。
小Hi发现旋律可以循环,每次把一段旋律里面最前面一个音换到最后面就成为了原旋律的“循环相似旋律”,还可以对“循环相似旋律”进行相同的变换能继续得到原串的“循环相似旋律”。
小Hi对此产生了浓厚的兴趣,他有若干段旋律,和一部音乐作品。对于每一段旋律,他想知道有多少在音乐作品中的子串(重复便多次计)和该旋律是“循环相似旋律”。
解题方法提示
解题方法提示
小Hi:我们已经对后缀自动机比较熟悉了,今天我们再来道稍有难度的题。
小Ho:好!这道题目让我们求的是若干串在另一个长串S中各自作为子串出现的次数,只是匹配的方式从完全相等变成了“循环同构”。
小Hi:没错!如果匹配方式是完全相等的话,本题就可以使用AC自动机或者trie图完美的解决。
小Ho:既然这个问题也和子串有关系,那么不妨试试后缀自动机。
小Hi:对。和上一期一样,你可以先想想如果询问只有一个串T应该怎么做。
小Ho:嗯,那自然就考虑所有T的循环同构的串在长串S中出现的次数。
小Hi:没错。循环同构有点麻烦,如果我们枚举与T循环同构的串,再依次判断是否在S中出现过。那复杂度至少是O(length(T)^2)的了。
小Ho:恩。比如T="abcd"的话,我们要判断4个串"abcd", "bcda", "cdab", "dabc"在S中出现的次数。
小Hi:为了能顺利解决循环同构的问题。我们要做点准备,先考虑这么一个问题。对于给定的字符串S和T,求S和T的最长公共子串。小Ho,你知道最长公共子串是什么吧? 要注意子串和子序列不同哦。
小Ho:知道。比如"aabbabd"和"abbabb"的最长公共子串就是"abbab"。
小Hi:我们利用SAM,可以做到对于T的每个位置i,都能求出以T[i]结尾的最长公共子串是什么。以你上面说的"aabbabd"和"abbabb"为例。我们可以求出
S: aabbabdT: abbabb 1:a2: ab3: abb4: abba5: abbab6: abb
小Ho:这个怎么求呢?
小Hi:我们先构造S的后缀自动机。然后对于每个结束位置T[i],维护两个变量。一个是当前状态u,表示T[i]结尾的最长公共子串在S的SAM的哪个状态里;二是当前匹配长度l,表示T[i]结尾的最长公共子串的长度。初始时u=初始状态S,l=0。
小Hi:假设我们已经知道T[i-1]对应的(u, l),要求T[i]对应的(u‘, l‘)。如果trans[u][T[i]]存在的话,那么直接令u‘=trans[u][T[i]], l‘ = l+1即可。
位置 | u | l |
---|---|---|
T[0] | S | l=0 |
T[1] | trans[S][a]=1 | 1 |
T[2] | trans[1][b]=8 | 2 |
T[3] | trans[8][b]=4 | 3 |
T[4] | trans[4][a]=6 | 4 |
T[5] | trans[6][b]=7 | 5 |
T[6] | trans[7][b]=NULL | ? |
小Ho:那如果遇到trans[u][T[i]]不存在怎么办? 比如处理的T[6]的时候,trans[7][b]不存在。
小Hi:这时我们要沿suffix-path(u->S)找到一个状态v满足trans[v][T[i]]存在。所以先找suffix[7]=8,而trans[8][b]=4存在。这时令u=trans[v][T[i]], l=maxlen(v)+1。
位置 | u | l |
---|---|---|
T[0] | S | l=0 |
T[1] | trans[S][a]=1 | 1 |
T[2] | trans[1][b]=8 | 2 |
T[3] | trans[8][b]=4 | 3 |
T[4] | trans[4][a]=6 | 4 |
T[5] | trans[6][b]=7 | 5 |
T[6] | trans[7][b]=NULL | ? |
T[6] | trans[suffix[7]][b]=trans[8][b]=4 | maxlen[8]+1=3 |
小Ho:这里trans[u][T[i]]不存在就沿着suffix-path(u->S)向前找的过程好像类似KMP中当前字符失配就沿next数组向前找?
小Hi:没错,过程是类似的。都是在"abbab"+‘b‘失配时,试图找到"abbab"的一个最长后缀s使得s+‘b‘不失配。我们知道"abbab"的后缀都在suffix-path(u->S)里,所以沿着suffix-path(u->S)向前找即可。
小Ho:如果最后直到初始状态S都没有trans[v][T[i]]存在呢?
小Hi:如果trans[S][T[i]]不存在,那字符T[i]肯定是没在字符串S中出现过。这时我们令u=S, l=0从T[i+1]重新开始即可。
小Hi:好了,现在对于T的每个位置i,我们都能求出以T[i]结尾的最长公共子串是什么。(严格来说我们知道最长公共子串的长度l)那我们能知道这个最长公共子串在S中出现了多少次么?
小Ho:这个我知道。我们既然已经知道子串对应的状态u,那么|endpos(u)|就是子串出现的次数。这个问题我们已经在hiho一下 第129周学习过了。
小Hi:现在我们的准备工作做完了。小Ho你先回顾一下前面的内容,下面我们要开始解决这周的问题了。
小Ho:好的。
小Hi:现在我们要处理T的循环同构串们。这里有一个常用的技巧,假设T的长度是n,我们令T‘=T + T[1..n-1]形成一个新的串T‘。例如对于"abcd",我们把"abc"拼在"abcd"后面,得到新的T="abcdabc"。这样"abcd"的循环同构串就变成了T‘="abcdabc"的长度为n的子串。
小Ho:哦!然后我们再用之前讲的方法求出在每个位置T‘[i]结束的最长公共子串。我们可以求出对应的(u, l),如果这时l>=n,那我们就得到了一个公共子串T‘[i-l+1 .. i]。这个子串在S中出现的次数是|endpos(u)|,又恰好包含T的循环同构串T‘[i-n+1 .. i]。
小Hi:基本思路是对的。但是要注意处理两个特殊情况。第一个情况是T的n个循环同构子串有重复(相同)的情况。比如T="aa",T‘="aaa",还是以S="aabbabd"为例
S: aabbabdT‘: aaa1: a (u, l) = (1, 1)2: aa (u, l) = (2, 2), l>=n3: aa (u, l) = (2, 2), l>=n
小Hi:T‘[2]和T‘[3]结尾的最长公共子串都是"aa",(u, l)都是(2, 2)。我们要避免"aa"的出现次数被统计2次,小Ho你想想要怎么办?
小Ho:恩,我们要记录一个状态是不是之前在l>=n的情况下到达过。如果到达过的话,下一次再到达就不要统计了。
小Hi:很好。我们还有第二个特殊情况要处理。那就是要区分串T‘[i-l+1 .. i]出现次数和T‘[i-n+1 .. i]的出现次数。前面说到,我们处理T‘[i]的时候求出当前状态u和匹配长度l。这时串T‘[i-l+1 .. i]一定是属于状态u的,T‘[i-l+1 .. i]的出现次数是|endpos(u)|。但是这时可能l>n,所以T‘[i-n+1 .. i]不一定属于状态u。T‘[i-n+1 .. i]是T‘[i-l+1 .. i]长度为n的后缀,可能在suffix-path(u->S)上,出现次数比T‘[i-n+1 .. i]多。
小Ho:这个也好办,我们只要沿着suffix-path(u->S)向上找,找到最靠近S的v满足maxlen[v]>=n (也就是minlen[v]<=n<=maxlen[v]),统计|endpos(v)|即可。
小Hi:这里有一个关键点,我们找到v之后可以直接令u=v。以免每次向前找v的复杂度过高。
小Ho:我试着总结一下,伪代码如下。
ans = 0; //出现次数n = length(T);T‘ = T + T[1..n];u = S, l = 0;for i = 1 .. length(T‘): while u != S AND trans[u][T‘[i]] is NULL: //沿suffix-path(u->S)找到一个状态v满足trans[v][T‘[i]]存在 u = slink[i], l = maxlen[u]; if trans[u][T‘[i]] is not NULL: u = trans[u][T‘[i]], l++; else: //直到初始状态S都没有trans[v][T‘[i]]存在 u = S, l = 0; if l > n: //要区分串T‘[i-l+1 .. i]出现次数和T‘[i-n+1 .. i]的出现次数 while maxlen[slink[u]] >= n: u = slink[u], l = maxlen[u]; if l >= n AND not visited[u]: //第一次在l>=n的情况下达到u visited[u] = TRUE; ans += |endpos(u)|;
小Ho:最后一个问题,本题有多个T怎么办呐?
小Hi:笨!处理每个T复杂度都是线性的,多个T就依次处理一遍就行了。
小Ho:原来如此。
输入
第一行,一个由小写字母构成的字符串S,表示一部音乐作品。字符串S长度不超过100000。
第二行,一个整数N,表示有N段旋律。接下来N行,每行包含一个由小写字母构成的字符串str,表示一段旋律。所有旋律的长度和不超过 100000。
输出
输出共N行,每行一个整数,表示答案。
- 样例输入
abac3aabca
- 样例输出
221
代码:
#include<algorithm>#include<iostream>#include<cstring>#include<cstdio>//by NeighThornusing namespace std;const int maxn=300000+5;int n,last=1,tail=1,cnt[maxn],out[maxn],vis[maxn],Max[maxn],Min[maxn],nxt[maxn][26],fail[maxn];char s[maxn];inline void build(char *s){ while(*s){ int p=last,t=++tail,c=*s++-‘a‘; Max[t]=Max[p]+1;cnt[t]=1; while(p&&!nxt[p][c]) nxt[p][c]=t,p=fail[p]; if(p){ int q=nxt[p][c]; if(Max[q]==Max[p]+1) Min[t]=Max[q]+1,fail[t]=q; else{ int k=++tail; fail[k]=fail[q]; fail[q]=fail[t]=k; Max[k]=Max[p]+1; Min[q]=Max[k]+1; Min[t]=Max[k]+1; Min[k]=Max[fail[k]]+1; memcpy(nxt[k],nxt[q],26*sizeof(int)); while(p&&nxt[p][c]==q) nxt[p][c]=k,p=fail[p]; } } else Min[t]=fail[t]=1; last=t; }}inline void calc(void){ int hd=1,ta=0,q[maxn]; for(int i=1;i<=tail;i++) out[fail[i]]++; for(int i=1;i<=tail;i++) if(!out[i]) q[++ta]=i; while(hd<=ta){ int top=q[hd++]; cnt[fail[top]]+=cnt[top]; out[fail[top]]--; if(!out[fail[top]]) q[++ta]=fail[top]; }}inline void solve(char *s){ memset(vis,0,sizeof(vis)); int u=1,n=strlen(s),len=0,ans=0; for(int i=n,j=0;j<n-1;j++,i++) s[i]=s[j]; s[n+n-1]=‘\0‘; while(*s){ int c=*s++-‘a‘; while(u!=1&&!nxt[u][c]) u=fail[u],len=Max[u]; if(nxt[u][c]) u=nxt[u][c],len++; else u=1,len=0; if(len>n) while(Max[fail[u]]>=n) u=fail[u],len=Max[u]; if(len>=n&&!vis[u]) vis[u]=1,ans+=cnt[u]; } printf("%d\n",ans);}signed main(void){ scanf("%s",s);build(s);calc(); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%s",s),solve(s); return 0;}
$By$ $NeighThorn$
hihocoder #1465 : 后缀自动机五·重复旋律8