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 }