描述

小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"为例。我们可以求出

  1. S: aabbabd
  2. T: abbabb
  3. 1a
  4. 2: ab
  5. 3: abb
  6. 4: abba
  7. 5: abbab
  8. 6: 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"为例

  1. S: aabbabd
  2. T': aaa
  3. 1: a (u, l) = (1, 1)
  4. 2: aa (u, l) = (2, 2), l>=n
  5. 3: 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:我试着总结一下,伪代码如下。

  1. ans = 0; //出现次数
  2. n = length(T);
  3. T' = T + T[1..n];
  4. u = S, l = 0;
  5. for i = 1 .. length(T'):
  6. while u != S AND trans[u][T'[i]] is NULL: //沿suffix-path(u->S)找到一个状态v满足trans[v][T'[i]]存在
  7. u = slink[i], l = maxlen[u];
  8. if trans[u][T'[i]] is not NULL:
  9. u = trans[u][T'[i]], l++;
  10. else: //直到初始状态S都没有trans[v][T'[i]]存在
  11. u = S, l = 0;
  12. if l > n: //要区分串T'[i-l+1 .. i]出现次数和T'[i-n+1 .. i]的出现次数
  13. while maxlen[slink[u]] >= n:
  14. u = slink[u], l = maxlen[u];
  15. if l >= n AND not visited[u]: //第一次在l>=n的情况下达到u
  16. visited[u] = TRUE;
  17. ans += |endpos(u)|;

小Ho:最后一个问题,本题有多个T怎么办呐?

小Hi:笨!处理每个T复杂度都是线性的,多个T就依次处理一遍就行了。

小Ho:原来如此。

Close

输入

第一行,一个由小写字母构成的字符串S,表示一部音乐作品。字符串S长度不超过100000。

第二行,一个整数N,表示有N段旋律。接下来N行,每行包含一个由小写字母构成的字符串str,表示一段旋律。所有旋律的长度和不超过 100000。

输出

输出共N行,每行一个整数,表示答案。

Sample Input

  1. abac
  2. 3
  3. a
  4. ab
  5. ca

Sample Output

  1. 2
  2. 2
  3. 1
  4.  
  5. 一个endpos只能被寻找到一次
  1. #pragma GCC optimize(2)
  2. #pragma G++ optimize(2)
  3. #include<cstring>
  4. #include<cstdio>
  5. #include<cmath>
  6. #include<iostream>
  7. #include<algorithm>
  8. #include<queue>
  9.  
  10. #define ll long long
  11. #define N 300007
  12. using namespace std;
  13. inline int read()
  14. {
  15. int x=,f=;char ch=getchar();
  16. while(!isdigit(ch)){if(ch=='-')f=-;ch=getchar();}
  17. while(isdigit(ch)){x=(x<<)+(x<<)+ch-'';ch=getchar();}
  18. return x*f;
  19. }
  20.  
  21. int n;
  22. struct sam
  23. {
  24. int cnt,last;
  25. int c[N][],fa[N],mx[N],endpos[N];
  26. sam(){cnt=last=;}
  27. void extend(int x)
  28. {
  29. int p=last,np=last=++cnt;mx[np]=mx[p]+;endpos[np]=;
  30. while(p&&!c[p][x])
  31. {
  32. c[p][x]=np;
  33. p=fa[p];
  34. }
  35. if(!p)fa[np]=;
  36. else
  37. {
  38. int q=c[p][x];
  39. if(mx[q]==mx[p]+)fa[np]=q;
  40. else
  41. {
  42. int nq=++cnt;mx[nq]=mx[p]+;
  43. memcpy(c[nq],c[q],sizeof(c[q]));
  44. fa[nq]=fa[q];
  45. fa[q]=fa[np]=nq;
  46. while(c[p][x]==q)c[p][x]=nq,p=fa[p];
  47. }
  48. }
  49. }
  50. int now=,pre=,du[N];queue<int>q[];
  51. void init_endpos()
  52. {
  53. for (int i=;i<=cnt;i++)
  54. if(fa[i])du[fa[i]]++;
  55. for (int i=;i<=cnt;i++)
  56. if(!du[i])q[pre].push(i);
  57. while(!q[pre].empty())
  58. {
  59. while(!q[pre].empty())
  60. {
  61. int x=q[pre].front();q[pre].pop();
  62. endpos[fa[x]]+=endpos[x];
  63. du[fa[x]]--;
  64. if(!du[fa[x]])q[now].push(fa[x]);
  65. }
  66. swap(now,pre);
  67. }
  68. }
  69. int u=,l=;ll ans;
  70. bool flag[N];
  71. void dp(int x,int n)
  72. {
  73. while(u&&!c[u][x])u=fa[u],l=mx[u];
  74. if(!u)u=,l=;
  75. else u=c[u][x],l++;
  76. if(l>n)while(mx[fa[u]]>=n)u=fa[u],l=mx[u];
  77. if(l>=n&&!flag[u])
  78. {
  79. flag[u]=true;
  80. ans+=endpos[u];
  81. }
  82. }
  83. void init()
  84. {
  85. printf("%lld\n",ans);
  86. ans=,u=,l=;
  87. memset(flag,,sizeof(flag));
  88. }
  89. }sam;
  90. char s[N],T[N];
  91.  
  92. int main()
  93. {
  94. scanf("%s",s+);int len=strlen(s+);
  95. for (int i=;i<=len;i++)sam.extend(s[i]-'a');
  96. sam.init_endpos();
  97. n=read();
  98. while(n--)
  99. {
  100. scanf("%s",T+);
  101. len=strlen(T+);
  102. for (int i=;i<=len;i++)
  103. T[i+len]=T[i];
  104. len=len*-;
  105. for (int i=;i<=len;i++)
  106. sam.dp(T[i]-'a',(len+)/);
  107. sam.init();
  108. }
  109. }

hihocoder 后缀自动机五·重复旋律8 求循环同构串出现的次数的更多相关文章

  1. hiho一下第131周 后缀自动机二·重复旋律8(循环相似子串)

    后缀自动机五·重复旋律8 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi平时的一大兴趣爱好就是演奏钢琴.我们知道一段音乐旋律可以被表示为一段数构成的数列. 小Hi ...

  2. hihoCoder 后缀自动机三·重复旋律6

    后缀自动机三·重复旋律6 时间限制:15000ms 单点时限:3000ms 内存限制:512MB 描述 小Hi平时的一大兴趣爱好就是演奏钢琴.我们知道一个音乐旋律被表示为一段数构成的数列. 现在小Hi ...

  3. hihoCoder #1465 : 后缀自动机五·重复旋律8

    http://hihocoder.com/problemset/problem/1465 求S的循环同构串在T中的出现次数 将串S变成SS 枚举SS的每个位置i,求出以i结尾的SS的子串 与 T的最长 ...

  4. hihoCoder.1465.后缀自动机五 重复旋律8(后缀自动机)

    题目链接 \(Description\) 给定母串S,求模式串的循环同构串在S中的出现次数. \(Solution\) 将模式串s复制一遍,在母串的SAM上匹配,记录以每个位置作为后缀所能匹配的最大长 ...

  5. hihocoder 1457 后缀自动机四·重复旋律7 求不同子串的和

    描述 小Hi平时的一大兴趣爱好就是演奏钢琴.我们知道一段音乐旋律可以被表示为一段数构成的数列. 神奇的是小Hi发现了一部名字叫<十进制进行曲大全>的作品集,顾名思义,这部作品集里有许多作品 ...

  6. HIHOcoder1465 后缀自动机五·重复旋律8

    思路 后缀自动机求最长循环串 首先有一个常用的处理技巧,将串复制一遍,长度大于n的子串中就包含了一组循环子串 然后是后缀自动机如何处理最长公共子串的问题 维护两个变量,u和l,u代表当前位置的最长公共 ...

  7. hihocoder 后缀自动机二·重复旋律5

    求不同子串个数 裸的后缀自动机 #include<cstring> #include<cmath> #include<iostream> #include<a ...

  8. hihocoder 后缀自动机四·重复旋律6

    题目 对于\(k\in[1,n]\)求出长度为\(k\)的子串出现次数最多的出现了多少次 我直到现在才理解后缀自动机上的子树和是什么意思 非常显然的一点是 \[endpos(link(u))⊇endp ...

  9. hihocoder 后缀自动机四·重复旋律7

    题目 在\(DAG\)上跑一个\(dp\)就好了 设\(ans_i\)表示到了\(SAM\)的\(i\)位置上所有的子串形成的数的和,之后我们顺便记录一个方案数\(d_i\) 之后我们直接转移就好了 ...

随机推荐

  1. array_unique() - 去除数组中重复的元素值

      array_unique() 定义和用法 array_unique() 函数移除数组中的重复的值,并返回结果数组. 当几个数组元素的值相等时,只保留第一个元素,其他的元素被删除. 返回的数组中键名 ...

  2. C++基础 静态成员

    静态成员是类的所有 对象共有的变量,在编译 阶段就必须分配空间. 需要注意: (1)静态成员变量的定义和使用 class Test{ static int a; }; ; void main() {} ...

  3. io编程,python

    IO在计算机中指Input/Output,也就是输入和输出. Stream(流): 可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动.Input Stream就是数据从外面(磁盘.网络)流 ...

  4. Android面试收集录11 Window+Activity+DecorView+ViewRoot之间的关系

    一.职能简介 Activity Activity并不负责视图控制,它只是控制生命周期和处理事件.真正控制视图的是Window.一个Activity包含了一个Window,Window才是真正代表一个窗 ...

  5. html5兼容处理&sublime text3配置html5环境

    1.为了兼容低版本的浏览器解析不了hmtl5标签,要在html文件中head内引入html5shiv.min.js文件 <!--[if lt IE 9]> <script src=& ...

  6. Java基本数据类型总结二

    Java 基本数据类型总结二 变量就是申请内存来存储值.也就是说,当创建变量的时候,需要在内存中申请空间. 内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据. 因此,通过 ...

  7. Java的接口和抽象类深入理解

    对于面向对象编程来说,抽象是它的一大特征之一.在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类.这两者确实有很多相似的地方,看了一整天别人怎么说,大致总结如下: 一.抽象类 在了解抽象类 ...

  8. 关于safaire下hash前面需要加/(正斜杠)

    就是我们1.1框架是根据hash跳转的,今天我2.0跳转到1.1,pc一直测下来都是好的, 然后发现手机上一直跳转有问题,然后排查了半小时左右才发现  hash前面需要加/ 分割. 例如:http:/ ...

  9. 《Cracking the Coding Interview》——第11章:排序和搜索——题目2

    2014-03-21 20:49 题目:设计一种排序算法,使得anagram排在一起. 解法:自定义一个comparator,使用额外的空间来统计字母个数,然后比较字母个数. 代码: // 11.2 ...

  10. Delphi中的关键字与保留字

    Delphi中的关键字与保留字 分类整理 Delphi 中的“关键字”和“保留字”,方便查询 感谢原作者的收集整理! 关键字和保留字的区别在于,关键字不推荐作标示符(编译器已经内置相关函数或者留给保留 ...