[关闭]
@dxbdly 2021-03-03T14:46:52.000000Z 字数 7632 阅读 144

G2020 19寒假集训Day3

信息学——19寒假集训


今天是G2020寒假集训Day3,主要讲字符串中的KMP,AC自动机。

一.KMP

算法是用来求字符串匹配问题的重要方法。

首先介绍两个定义:

  1. 文本串:即主串,将要被模式串匹配。
  2. 模式串:即子串,将要匹配文本串。

我们先看道题:

题面

算法分析:

暴力

用两个指针,分别指向文本串和模式串将要匹配的字符。

如果两个字符匹配成功,则

如果两个字符失配,则将

过程如下图:

时间复杂度需要优化

KMP :

思考:暴力算法的冗余在哪?

观察暴力算法,我们容易看出,当字符串失配时:

我们会让:

那么我们会发现

  1. 由于模式串的前缀ab匹配成功.
  2. 所以当$l1=2,l1=3$时不可能匹配成功,这时可以直接跳过。

所以说

  1. 我们在两字符串失配时会将很多有效信息遗漏,从而造成冗余

而KMP算法就是运用这些信息进行优化。

KMP算法思想

刚刚说过了,KMP算法就是利用失配时的信息进行优化。

还是用前面的图:

他在两字符串失配时,不动文本串指针,将模式串指针向前移动到特定位置上,继续匹配。

下面定义数组:

  1. next[i]表示模式串0~(i-1)的子串中前缀与后缀相等的最大长度。

那么我们如果得到了数组

即可在失配时令,由于前缀等于后缀的原因,中间跨过的那一段一定不能匹配,可以保证正确性。

那么如果得到了数组我们就可以将算法有效优化。

next数组的求法

仔细观察next数组的定义,你会发现其中有这样一段:

  1. 前缀与后缀相等的最大长度。

前缀与后缀相等?

那是不是就是用~的模式串匹配~的模式串!

所以说我们只要将模式串自己匹配自己就可以处理出next数组。

KMP的时间复杂度

我们采用势能法分析。

  1. 假设模式串长度为m,文本串长度为n
  2. 则模式串指针l2每右移一位增加1势能,且文本串指针l1移动到末尾时算法结束。
  3. 那么如果但前匹配上了,则文本串指针l1向后移动1位,且增加1势能。
  4. 如果没配上则减少x势能,文本串指针l1先后移动x位。
  5. 那么势能最多存m,指针l1最多移动n下。
  6. 所以时间复杂度为O(n+m).

AC代码

  1. // The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. inline int read()
  5. {
  6. int x=0;
  7. char c=getchar();
  8. bool f=0;
  9. while (!isdigit(c))
  10. f|=(c=='-'),c=getchar();
  11. while (isdigit(c))
  12. x=(x<<3)+(x<<1)+(c^48),c=getchar();
  13. return x;
  14. }
  15. string s1,s2;
  16. int len1,len2;
  17. int nex[1000005];
  18. inline void get_nex()
  19. {
  20. int k1=0,k2;
  21. nex[0]=k2=-1;
  22. while (k1<len2)
  23. {
  24. if (k2==-1||s2[k1]==s2[k2])
  25. nex[++k1]=++k2;
  26. else
  27. k2=nex[k2];
  28. }
  29. }
  30. inline void KMP()
  31. {
  32. int k1=0,k2=0;
  33. while (k1<len1)
  34. {
  35. if (k2==-1||s1[k1]==s2[k2])
  36. k1++,k2++;
  37. else
  38. k2=nex[k2];
  39. if (k2==len2)
  40. printf("%d\n",k1-k2+1),k2=nex[k2];
  41. }
  42. }
  43. int main(){
  44. cin>>s1>>s2;
  45. len1=s1.size(),len2=s2.size();
  46. get_nex();
  47. KMP();
  48. for (register int i=1;i<=len2;++i)
  49. printf("%d ",nex[i]);
  50. return 0;
  51. }

3. 例题UVA1328 Period

题面

  1. 样例输入:
  2. 3
  3. aaa
  4. 12
  5. aabaabaabaab
  6. 0
  7. 样例输出:
  8. Test case #1
  9. 2 2
  10. 3 3
  11. Test case #2
  12. 2 2
  13. 6 2
  14. 9 3
  15. 12 4

题解:

此题重点在于如何求一个字符串是否为周期串,以及周期的周长

观察KMP算法,我们会发现:

如果字符串是一个周期串,且他的长度为n

则一定满足,并且此时周期长度为

PS:其中为KMP所处理的失配数组

所以我们只需预处理出数组,然后对于n个前缀处理,总时间复杂度,可过!

AC代码:

  1. //The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. inline int read()
  5. {
  6. int x=0;
  7. char c=getchar();
  8. bool f=0;
  9. while (!isdigit(c))
  10. f|=(c=='-'),c=getchar();
  11. while (isdigit(c))
  12. x=(x<<3)+(x<<1)+(c^48),c=getchar();
  13. return x;
  14. }
  15. char s[2000005];
  16. int len,t;
  17. int nex[2000005];
  18. inline void get_nex()
  19. {
  20. int k1=0,k2;
  21. nex[0]=k2=-1;
  22. while (k1<len)
  23. {
  24. if (k2==-1||s[k1]==s[k2])
  25. nex[++k1]=++k2;
  26. else
  27. k2=nex[k2];
  28. }
  29. }
  30. int main(){
  31. while (len=read())
  32. {
  33. scanf("%s",s);
  34. get_nex();
  35. printf("Test case #%d\n",++t);
  36. for (register int i=2;i<=len;++i)
  37. if (nex[i]>0&&i%(i-nex[i])==0)
  38. printf("%d %d\n",i,i/(i-nex[i]));
  39. printf("\n");
  40. }
  41. return 0;
  42. }

二. AC自动机

图片引用自博客

题面

前置芝士点:Trie树,不会请自行百度(简单)

1. 定义

  1. 自动机:给你当前位置与一个操作字符,则可以找到唯一确定的下一位置
  2. AC自动机:AC自动机其实就是在Trie树上做KMP
  3. fail数组:可理解为KMP中的next数组

自动机用于处理多模式串匹配问题,此算法将树与结合在一起,完美的解决了这类问题。

有了的基础,学习AC自动机显得简单很多。

2.建立AC自动机(fail数组的求法)

假设模式串:

匹配串:

首先,我们将所有模式串建成一颗Trie树(不会请自行百度)

然后,考虑假如我们在匹配时失配了怎么办

采用思路,在失配时,跳到指针所指向的地方,且当前模式串后缀与新模式串前缀相同,然后继续往下寻找。

不同的是,数组一般由BFS的方式来建立。

首先,两个特判:

  1. 根节点的fail值就是本身。
  2. 根节点所有儿子的fail值是根。

然后一层一层往下递推,分两种情况讨论:

若:当前儿子未在树上出现,则将值为根。

如下图节点

若:当前儿子在树上出现

  1. 我们将他的file指针指向
  2. (他父亲fail指针所指向的节点的)(与他相同的)儿子节点

如下图节点

以此类推,将数组完全求出后是这样子滴:

到此为止,我们就成功的将自动机建立完成,并且求出了最重要的数组。

3.匹配操作

根据自动机的定义,我们可以将文本串看作操作指令

那么边所对连接的点就是下一状态

我们只需要不断地顺着边跳即可。

AC代码:

  1. //The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. inline int read()
  5. {
  6. int x=0;
  7. char c=getchar();
  8. bool f=0;
  9. while (!isdigit(c))
  10. f|=(c=='-'),c=getchar();
  11. while (isdigit(c))
  12. x=(x<<3)+(x<<1)+(c^48),c=getchar();
  13. return f?-x:x;
  14. }
  15. int n;
  16. char s[1000005];
  17. int trie[1000005][30],cnt,word[1000005];
  18. int fail[1000005],ans;
  19. inline void add()
  20. {
  21. int root=0,len=strlen(s);
  22. for (register int i=0;i<len;++i)
  23. {
  24. int x=s[i]-'a'+1;
  25. if (!trie[root][x])
  26. trie[root][x]=++cnt;
  27. root=trie[root][x];
  28. }
  29. word[root]++;
  30. }
  31. inline void get_fail()
  32. {
  33. queue <int> q;
  34. for (register int i=1;i<=26;++i)
  35. if (trie[0][i])
  36. fail[trie[0][i]]=0,q.push(trie[0][i]);
  37. while (!q.empty())
  38. {
  39. int x=q.front();
  40. q.pop();
  41. for (register int i=1;i<=26;++i)
  42. {
  43. if (trie[x][i])
  44. fail[trie[x][i]]=trie[fail[x]][i],q.push(trie[x][i]);
  45. else
  46. trie[x][i]=trie[fail[x]][i];
  47. }
  48. }
  49. }
  50. inline void query()
  51. {
  52. int len=strlen(s),now=0;
  53. for (register int i=0;i<len;++i)
  54. {
  55. now=trie[now][s[i]-'a'+1];
  56. for (register int j=now;j&&word[j]!=-1;j=fail[j])
  57. ans+=word[j],word[j]=-1;
  58. }
  59. printf("%d",ans);
  60. }
  61. int main(){
  62. n=read();
  63. for (register int i=1;i<=n;++i)
  64. scanf("%s",s),add();
  65. get_fail();
  66. scanf("%s",s);
  67. query();
  68. return 0;
  69. }

4. 例题

P3796【模板】AC自动机(加强版)

题面

题解:

  1. 此题要求我们输出那些模式串在文本串中出现次数最多
  2. 我们只需要在建Trie树时将各个模式串结束位置打上对应标记
  3. 在匹配时累加标记,做完后统计一遍即可。
  4. 注意卡空间!!!

AC代码:

  1. //The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. inline int read()
  5. {
  6. int x=0;
  7. char c=getchar();
  8. bool f=0;
  9. while (!isdigit(c))
  10. f|=(c=='-'),c=getchar();
  11. while (isdigit(c))
  12. x=(x<<3)+(x<<1)+(c^48),c=getchar();
  13. return f?-x:x;
  14. }
  15. int n;
  16. string s[155],t;
  17. int trie[1000005][27],cnt,word[1000005];
  18. int fail[1000005],ans[1000005];
  19. inline void add(int l)
  20. {
  21. int root=0,len=s[l].size();
  22. for (register int i=0;i<len;++i)
  23. {
  24. int x=s[l][i]-'a'+1;
  25. if (!trie[root][x])
  26. trie[root][x]=++cnt;
  27. root=trie[root][x];
  28. }
  29. word[root]=l;
  30. }
  31. inline void get_fail()
  32. {
  33. queue <int> q;
  34. for (register int i=1;i<=26;++i)
  35. if (trie[0][i])
  36. fail[trie[0][i]]=0,q.push(trie[0][i]);
  37. while (!q.empty())
  38. {
  39. int x=q.front();
  40. q.pop();
  41. for (register int i=1;i<=26;++i)
  42. {
  43. if (trie[x][i])
  44. fail[trie[x][i]]=trie[fail[x]][i],q.push(trie[x][i]);
  45. else
  46. trie[x][i]=trie[fail[x]][i];
  47. }
  48. }
  49. }
  50. inline void query()
  51. {
  52. int len=t.size(),now=0;
  53. for (register int i=0;i<len;++i)
  54. {
  55. now=trie[now][t[i]-'a'+1];
  56. for (register int j=now;j;j=fail[j])
  57. ans[word[j]]++;
  58. }
  59. }
  60. inline void get_ans()
  61. {
  62. int maxx=0;
  63. for (register int i=1;i<=n;++i)
  64. maxx=max(maxx,ans[i]);
  65. printf("%d\n",maxx);
  66. for (register int i=1;i<=n;++i)
  67. if (ans[i]==maxx)
  68. cout<<s[i]<<endl;
  69. }
  70. inline void clear()
  71. {
  72. memset(fail,0,sizeof(fail));
  73. memset(word,0,sizeof(word));
  74. memset(ans,0,sizeof(ans));
  75. memset(trie,0,sizeof(trie));
  76. }
  77. int main(){
  78. while (n=read())
  79. {
  80. clear();
  81. for (register int i=1;i<=n;++i)
  82. cin>>s[i],add(i);
  83. get_fail();
  84. cin>>t;
  85. query(),get_ans();
  86. }
  87. return 0;
  88. }

P5357 【模板】AC自动机(二次加强版)

题面

题解:

此题向我们反映出一个问题:

  1. 其实我们说的匹配方式:顺着fail跳是可以被卡的。
  2. 如果出题人弄一个fail长为n的数据,我们的匹配操作将会被卡掉。
  3. 类似于匹配串:aaaaaaaaaa……的情况即可HACK

那么如何优化呢?

我们仔细思考

发现在学习自动机时,大部分用的是的套路,并没有将自动机是一个自动机的优势发挥!

我们其实可以发现

  1. 我们构造出来的自动机其实与Trie树没有关系!
  2. AC自动机只与通过fail指针连的边有关。

于是我们可以不看原树边,只看边,会发现他也构成了一颗树!我们将其称为

再联想的定义(也就是的定义)中有这样一条:

  1. 前缀与后缀相等!
  2. 所以说fail树同时与前缀,后缀有关系。

由字符串的性质我们可以知道:

  1. 前缀的后缀就是子串

由于这一性质,我们可以发现:

  1. 以某一模式串结尾字母为根的子树,其节点访问次数之和
  2. 就是原模式串匹配次数。

所以:
我们只需要

  1. 1.预处理出fail数组
  2. 2.建立fail树,将文本串的每个字符累加到fail树节点的访问次数中
  3. 3.求出以模式串结尾字母为根的子树访问次数之和。
  4. 即可保证匹配时间在O(n)内,又同KMP复杂度分析,求fail数组复杂度O(m),所以总共O(n+m).

通过AC自动机的fail树性质,我们成功优化了AC自动机算法,使其拥有了稳定的线性复杂度!

AC代码:

  1. //The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. inline int read()
  5. {
  6. int x=0;
  7. char c=getchar();
  8. bool f=0;
  9. while (!isdigit(c))
  10. f=f|(c=='-'),c=getchar();
  11. while (isdigit(c))
  12. x=(x<<3)+(x<<1)+(c^48),c=getchar();
  13. return f?-x:x;
  14. }
  15. int n;
  16. string s;
  17. struct node{
  18. int v,nex;
  19. }edge[200005];
  20. int head[200005],len,siz[200005];
  21. int trie[200005][27],cnt,word[200005],fail[200005];
  22. inline void make_map(int u,int v)
  23. {
  24. len++;
  25. edge[len].nex=head[u];
  26. edge[len].v=v;
  27. head[u]=len;
  28. }
  29. inline void add(int l)
  30. {
  31. int root=0,len=s.size();
  32. for (register int i=0;i<len;++i)
  33. {
  34. int x=s[i]-'a'+1;
  35. if (!trie[root][x])
  36. trie[root][x]=++cnt;
  37. root=trie[root][x];
  38. }
  39. word[l]=root;
  40. }
  41. inline void get_fail()
  42. {
  43. queue <int> q;
  44. for (register int i=1;i<=26;++i)
  45. if (trie[0][i])
  46. fail[trie[0][i]]=0,q.push(trie[0][i]);
  47. while (!q.empty())
  48. {
  49. int x=q.front();
  50. q.pop();
  51. for (register int i=1;i<=26;++i)
  52. {
  53. if (trie[x][i])
  54. fail[trie[x][i]]=trie[fail[x]][i],q.push(trie[x][i]);
  55. else
  56. trie[x][i]=trie[fail[x]][i];
  57. }
  58. }
  59. }
  60. inline void make()
  61. {
  62. int len=s.size(),now=0;
  63. for (register int i=0;i<len;++i)
  64. {
  65. now=trie[now][s[i]-'a'+1];
  66. ++siz[now];
  67. }
  68. for (register int i=1;i<=cnt;++i)
  69. make_map(fail[i],i);
  70. }
  71. inline void search(int x,int fa)
  72. {
  73. for (register int i=head[x];i;i=edge[i].nex)
  74. {
  75. int y=edge[i].v;
  76. if (y==fa)
  77. continue;
  78. search(y,x);
  79. siz[x]+=siz[y];
  80. }
  81. }
  82. int main(){
  83. n=read();
  84. for (register int i=1;i<=n;++i)
  85. cin>>s,add(i);
  86. get_fail();
  87. cin>>s;
  88. make(),search(0,-1);
  89. for (register int i=1;i<=n;++i)
  90. printf("%d\n",siz[word[i]]);
  91. return 0;
  92. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注