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 }