KMP和AC自动机

KMP:

给定模式串$A[1~n]$和匹配串$B[1~m]$,求出$A$在$B$中出现的位置。

这就是经典的字符串匹配问题了,也许你会说$Hash$也可以线性解决,为什么还要学$KMP$?

因为$KMP$的作用并不仅仅是解决字符串匹配问题,$KMP$过程中得到的$Next$数组还可以在一些问题中发挥出巨大的作用。

Step 1:

我们要求出一个数组$Next$,$Next[i]$表示$A$中以$i$结尾的非前缀子串与$A$的前缀能够匹配的最大长度,即:

$$Next[i] = max\{j\} \quad (j < i \quad and \quad A[i - j + 1 \sim i] = A[1 \sim j])$$

假设$A = "abababaac"$,那么$Next[7] = 5$,因为$A[3 \sim 7] = A[1 \sim 5]$。

那怎么求$Next[i]$呢,假设我们现在已经求出了$Next[1 \sim i - 1]$,比如说我们现在要求$Next[7]$,且已知$Next[1 \sim 6]$。

我们直接在$Next[6]$的基础上进行匹配,这显然是最优的,因为$Next[6] = 4$,即$A[3 \sim 6] = A[1 \sim 4]$,现在我们来比较$A[7]$与$A[5]$。

因为$A[7] = A[5] = 'a'$,所以$Next[7] = 5$。然后是$Next[8]$,但这一次,$A[8] != A[6]$,那我们怎么办呢?

因为$A[5 \sim 7] = A[1 \sim 3], A[7] = A[1]$,所以我们还可以比较$A[8], A[4], A[8], A[2]$,可惜的是$A[8]$与它们都不相等。

那么我们只能从头开始匹配,但是$A[8] != A[1]$,所以$Next[8] = 0$。

上述过程很有道理,可是我们怎么知道要匹配$A[4], A[2]$呢,也就是说,我们怎么知道$A[5 \sim 7] = A[1 \sim 3], A[7] = A[1]$。

首先,我们知道$Next[7] = 5, Next[5] = 3$,即$A[3 \sim 7] = A[1 \sim 5], A[3 \sim 5] = A[1 \sim 3]$。

于是我们就知道:$A[7] = A[5] = A[3], A[6] = A[4] = A[2], A[5] = A[3] = A[1]$,即$A[5 \sim 7] = A[1 \sim 3]$。

同理,在考虑完$Next[5] = 3$后,我们同样可以根据$Next[3] = 1$得知$A[7] = A[1]$。

这就是一个指针不断在之前求出的$Next$数组上跳跃的过程,我们可以写出代码:

 1 void Get_Next()
 2 {
 3     Next[1] = 0;
 4     for(int i = 2, j = 0; i <= n; ++i)
 5     {
 6         while(j && A[j + 1] != A[i]) j = Next[j];
 7         if(A[j + 1] == A[i]) ++j;
 8         Next[i] = j;
 9     }
10 }

Step 2:

我们只需求出一个数组$f$,$f[i] = max\{j\} \quad (j ≤ i \quad and \quad B[i - j + 1 \sim i] = A[1 \sim j])$。

由于定义和$Next$数组类似,我们可以类推出$f$数组的求法:

 1 void Get_f()
 2 {
 3     for(int i = 1, j = 0; i <= m; ++i)
 4     {
 5         while(j && (j == n || A[j + 1] != B[i])) j = Next[j];
 6         if(A[j + 1] == B[i]) ++j;
 7         f[i] = j;
 8         if(f[i] == n) printf("%d\n", i - n + 1);
 9     }
10 }

例题(POJ1961):

题目大意:如果一个字符串$S$是由字符串$T$重复$K$次形成的,则称$T$是$S$的循环元,$K$为循环次数。给你一个长度为$N$的字符串$S$,对$S$的每一个前缀,如果它的最大循环次数大于$1$,则输出前缀的位置和最大循环次数。

先求出$S$的$Next$数组,根据定义,对于每个$i$,$S[i - Next[i] + 1 \sim i] = S[1 \sim Next[i]]$,且不存在更大的值满足这个条件。

比如当$Next[8] = 6$时,我们可以推出:$S[3 \sim 8] = S[1 \sim 6] => S[1 \sim 2] = S[3 \sim 4] = S[5 \sim 6] = S[7 \sim 8]$

同理,当$Next[i] = k$时,可以得到:$S[i - k + 1 \sim i] = S[1 \sim k] => S[1 \sim i - k] = S[i - k + 1 \sim (i - k + 1) + (i - k) - 1] = ... = S[i - k + 1 \sim i]$

也就是说:当$i - Next[i] | i$时,$S[1 \sim i - Next[i]]$就是$S[1 \sim i]$的最小循环元,最大循环次数即为$\frac{i}{i - Next[i]}$

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 using namespace std;
 5 
 6 const int MAXN = 1000010;
 7 
 8 int n, Next[MAXN];
 9 char s[MAXN];
10 
11 int main()
12 {
13     int Case = 0;
14     while(scanf("%d", &n) != EOF && n)
15     {
16         printf("Test case #%d\n", ++Case);
17         scanf("%s", s + 1);
18         Next[1] = 0;
19         for(int i = 2, j = 0; i <= n; ++i)
20         {
21             while(j && s[i] != s[j + 1]) j = Next[j];
22             if(s[i] == s[j + 1]) ++j;
23             Next[i] = j;
24             if(i % (i - Next[i]) == 0 && i / (i - Next[i]) > 1)
25                 printf("%d %d\n", i, i / (i - Next[i]));
26         }
27         puts("");
28     }
29     return 0;
30 }

 


 

AC自动机:

给定多个模式串和一个匹配串,求有多少个模式串在匹配串里出现过,对于多模式串问题,$$

这时候就要用到我们的$AC$自动机了,其实$AC$自动机的根本思想与$KMP$基本相同,可以说是在$Trie$上的$KMP$算法。

利用$AC$自动机进行匹配只需要三步。(在下面的过程中,我们默认字符集为大写字母)

Step 1:

我们先把所有的模式串建成一颗$Trie$树(就是普通的$Trie$树)

 1 void build(char *s)
 2 {
 3     int len = strlen(s + 1), u = 1;
 4     for(int i = 1; i <= len; ++i)
 5     {
 6         int c = s[i] - 'A';
 7         if(!ch[u][c]) ch[u][c] = ++cnt;
 8         u = ch[u][c];
 9     }
10     ++ed[u];
11 }

注意这里的$cnt$初始值应该为1而不是0,因为已经有了一个根节点1。

Step 2:

假设我们现在在$Trie$树上进行匹配,$Trie$树上匹配到了节点$u$,匹配串$S$匹配到了$i$。

如果$Trie$树上存在一条字符为$S[i + 1]$的转移边,那么我们令$i + 1, u = ch[u][S[i + 1]]$。

如果不存在的话,我们需要找到另外一个节点,这个节点的深度应该尽量大(相当于前缀尽量长),并且这个节点代表的前缀与$u$代表的后缀相同。

注意到这个过程就类似于跳$Next$数组的过程,那我们能不能在$Trie$树上也建立一个这种数组,使得我们能快速找到所需要的节点呢?

当然可以,下面我们将类比$KMP$算法的过程,来建立在$Trie$上的$Next$数组。

假设我们已经计算到了节点$u$($u$及其父亲的$Next$已经得到),然后我们枚举$u$的子节点$x$,令$Next[u] = v$。

若$v$也存在一条和$u=>x$相同的转移边$v=>y$,那么我们就令$Next[x] = y$。

如果不存在,我们令$v = Next[v]$,然后重复这样的判断,如果$v$一直跳到了空节点(即根节点都无法匹配),那我们就令$Next[x]$为根节点。

 1 void bfs()
 2 {
 3     for(int i = 0; i <= 25; ++i) ch[0][i] = 1;
 4     queue<int> q; q.push(1); Next[1] = 0;
 5     while(!q.empty())
 6     {
 7         int u = q.front(); q.pop();
 8         for(int i = 0; i <= 25; ++i)
 9         {
10             if(!ch[u][i]) ch[u][i] = ch[Next[u]][i];
11             else
12             {
13                 q.push(ch[u][i]);
14                 Next[ch[u][i]] = ch[Next[u]][i];
15             }
16         }
17     }
18 }

需要注意的是第10行代码,这里进行了一个小优化,从而省略了失配时在树上不停往上跳的过程。

Step 3:

最后就是匹配的过程了,注意在每个位置我们都要往回跳,以确保能考虑到每个模式串。

 1 void Find(char *s)
 2 {
 3     int n = strlen(s + 1), u = 1;
 4     for(int i = 1; i <= n; ++i)
 5     {
 6         u = ch[u][s[i] - 'A'];
 7         for(int x = u; x; x = Next[x])
 8             if(ed[x])
 9             {
10                 //do something
11             }
12     }
13 }

例题:AC自动机

就是个模板题,注意在匹配的时候我们加入了一个剪枝优化,具体见代码。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int MAXN = 1000010;
 5 
 6 int n, ans;
 7 char s[MAXN];
 8 
 9 struct AC
10 {
11     int ch[MAXN][26], ed[MAXN], Next[MAXN], cnt;
12     
13     void build(char *s)
14     {
15         int len = strlen(s + 1), u = 1;
16         for(int i = 1; i <= len; ++i)
17         {
18             int c = s[i] - 'a';
19             if(!ch[u][c]) ch[u][c] = ++cnt;
20             u = ch[u][c];
21         }
22         ++ed[u];
23     }
24     
25     void bfs()
26     {
27         for(int i = 0; i <= 25; ++i) ch[0][i] = 1;
28         queue<int> q; q.push(1); Next[1] = 0;
29         while(!q.empty())
30         {
31             int u = q.front(); q.pop();
32             for(int i = 0; i <= 25; ++i)
33             {
34                 if(!ch[u][i]) ch[u][i] = ch[Next[u]][i];
35                 else
36                 {
37                     q.push(ch[u][i]);
38                     Next[ch[u][i]] = ch[Next[u]][i];
39                 }
40             }
41         }
42     }
43     
44     void Find(char *s)
45     {
46         int n = strlen(s + 1), u = 1;
47         for(int i = 1; i <= n; ++i)
48         {
49             u = ch[u][s[i] - 'a'];
50             for(int x = u; x && ~ed[x]; x = Next[x])
51             {
52                 ans += ed[x];
53                 ed[x] = -1;
54             }
55         }
56     }
57 }ac;
58 
59 int main()
60 {
61     scanf("%d", &n); ac.cnt = 1;
62     for(int i = 1; i <= n; ++i)
63     {
64         scanf("%s", s + 1);
65         ac.build(s);
66     }
67     ac.bfs();
68     scanf("%s", s + 1);
69     ac.Find(s);
70     printf("%d\n", ans);
71     return 0;
72 }

 

转载于:https://www.cnblogs.com/Aegir/p/10746842.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值