这里简要的总结一下字符串匹配算法,包括KMP,Rabin_Karp,和AC自动机
KMP
这个算法利用的是子串前缀的信息,对于模式串,P,与文本串T,而言当我们匹配到P[q]与T[i]的时候,我们可以知道,此时p[0,…,q-1]一定已经与T[i-q+1,…i-1]是匹配好了的,所以我们移动子串的时候就不必用暴力的方法一个一个的移动,而是移动到
Pk⊂Pq−1
P
k
⊂
P
q
−
1
即P[0,…k]为q-1的后缀用k+1去与T[i]比较,因为P[0,…,k]肯定匹配了P[q-1]的后缀,即T[i]的后缀。
构造f
下面我们说明对于模式串的每一个元素
q
q
,预处理出满足的最大的
K
K
值
如果已经知道f[q-1] (q-1对应的K值)为k,那么,只要P[q] == P[k+1],f[q] = k+1;同时把匹配光标k,前移动一位。这是比较好理解的。
如果说没有匹配呢,因为我们已经知道f[q-1]对应的k值所以我们可以将当前匹配的光标回退到f[q-1]直到P[q]与P[k+1]匹配为止。
我们将0处的k值定义为-1,因此总会到达一个”匹配的位置”
void KMP_next( char * P,int *f,int n =-1)
{
int m = (n==-1)?strlen(P):n;
int k = f[0] = -1;//当前匹配下标
for(int q = 1 ; q<m ; ++q)
{
while(k >-1&& P[k+1]!=P[q])
k = f[k];
f[q] = P[k+1]==P[q]?++k:k;
}
}
匹配算法
/*
*找出模式串第一次出现的下标
*/
int KMP(int * T,int* P,int* f)
{
int n = strlen(T);
int m = strlen(P);
int j = -1;//当前匹配的下标位置
for(int i=0 ; i<n ; ++i)
{
while(j>=0 && P[j+1]!=T[i])j = f[j];
if(P[j+1]==T[i])++j;
if(j==m-1)return i-m+1;
}
return -1;
}
不难发现这和预处理的过程非常的相似,因为预处理过程就是在用自己匹配自己。
复杂度
改进
由上面的预处理函数我们已经知道,当在 P[q+1] P [ q + 1 ] 与 T[i] T [ i ] 匹配失败的时候下一次匹配的位置应该是 P[f[q]+1] P [ f [ q ] + 1 ] ,如果说我们已经知道了 P[f[q]+1] P [ f [ q ] + 1 ] 与 P[q+1] P [ q + 1 ] 是一样的那我们知道这一次比较一定是徒劳的,他至少应该回到 f[f[q]]+1 f [ f [ q ] ] + 1 (建议画图理解)
改进的KMP中 nt[i] n t [ i ] 表示的是失配函数,即: P[i]!=T[j] P [ i ] ! = T [ j ] 后 i i 应该去的地方在串 中表示, P[0....nt[i]−1]=P[...i−1] P [ 0.... n t [ i ] − 1 ] = P [ . . . i − 1 ] , 就是避免重复匹配
void ex_KMP_preprocess(const char pat[]) {
// pat[0....nt[i]-1] = pat[i-1], nt[i] 是 i 失败后应该转移的地方
int q=1;
nt[q] = 0;
int k=0;
while (pat[q]) {
if(k==0 || pat[k]==pat[q])//j=0 通配符
{
++k;++q;
nt[q] = (pat[k]!=pat[q]? k : nt[k] );
}else k = nt[k];
}
}
int KMP(const char pat[],const char T[]){
int i =1,j=0;
int len_pat = strlen(pat+1);
while (T[i]) {
if(j==0 || pat[j]== T[i]){
++i;++j;
if(j==len_pat+1)return i-len_pat;
}else j = nt[j];
}
}
f最长匹配前缀的性质
对于串 P[0,...,n−1] P [ 0 , . . . , n − 1 ] 我们一定有
P[i]=P[i+n−1−f[n−1]] P [ i ] = P [ i + n − 1 − f [ n − 1 ] ]
即对于
0,...,f[n−1]
0
,
.
.
.
,
f
[
n
−
1
]
我们有他的周期一定是n-1-f[n-1],对于字符串来说
n−1−f[n−1]
n
−
1
−
f
[
n
−
1
]
就是该字符串的最短循环节!
例The Minimum Length
重复因子
对于串 x=yr x = y r 我们定义他的重复因子为 r r 即由 y y 重复次拼接而成。则由上面的结论,一定有
r∗(i−1−f[i−1])=i r ∗ ( i − 1 − f [ i − 1 ] ) = i
例题:
poj 2406
基于重复因子Galil 和 Seifras提出了只需要
O(1)
O
(
1
)
额外空间的匹配方法
Galil & Seifras
AC自动机
这个是一个由Aho-Corasick发明的一个多串匹配的算法,简单的讲就是在Trie上的KMP,至于KMP中的失配函数则是用BFS来进行构建的
这里不准备详细讲解AC自动机算法,因为没人能比他自己的论文讲的明白,有兴趣的读者请移步:
Efficient
String Matching:
An Aid to
Bibliographic Search
Alfred V. Aho and Margaret J. Corasick
这里仅对他的实现做一个简单总结
Trie中的数据说明
struct ACTrie
{
int m;//模式串个数
int next[max_node][Sigma_size];//状态的转移,已经优化之后的
int f[max_node];//匹配失败函数,即后缀边
int val[max_node];//每个字符串的末节点处的值(即作为 (string,val)对)
const int fail = -1;//AC论文中的失配标记
int size;//状态总数
void init()//初始化
{
val[0] = fail;memset(next[0],fail,sizeof(next[0]));size = 1;
}
int new_node()//新增加一个状态节点
{
memset(next[size],fail,sizeof(next[size]));
val[size]= fail;
size++;
return size-1;
}
void add(char * s,int id)//存储字符串编号可用于在文本串中输出
{
int state = 0,n = strlen(s);
for(int i=0 ; i<n ; ++i)
{
int id = IDX(s[i]);
if(next[state][id]==fail)
next[state][id] = new_node();
state =next[state][id];
}
val[state] =id;
}
};
失配函数,next表
void make_fail()
{
queue<int> Q;f[0] =0;
for(int i=0 ; i<Sigma_size ;++i)
if(next[0][i]!=fail){f[next[0][i]] = 0;Q.push(next[0][i]);}//depth =0的状态
else{next[0][i]= 0;}//空状态的下一个状态为0
while(!Q.empty())
{
int r = Q.front();Q.pop();
for(int i=0 ; i<Sigma_size ; ++i)
{
int s = next[r][i];
if(s!=fail){
Q.push(s);
f[s] = next[f[r]][i];
}else next[r][i] = next[f[r]][i];//把无效比较进行压缩,对应AC论文中说的优化,
}
}
}
这里所做的优化和KMP是一样的应为我们实际可以从模式串中推断出有些匹配肯定会无效
匹配函数
void match(char * T)
{
int n = strlen(T);
int state= 0;
for(int i=0 ; i<n ; ++i)
{
int c = IDX(T[i]);
state = next[state][c];
int temp = state;
while(val[temp]!=fail)//沿着后缀边一直找子串
{
cnt[val[temp]]++;
temp = f[temp];
}
}
}
上述匹配模式不是必须的要看你计算的是什么,如果是简单统计每个串的个数,就可以这样做
来几道模板题压压惊
hdu 3065
给你m个子串让你求出每个串在模式串中出现的次数
完整AC代码
#include <iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#include<algorithm>
#define maxn 2000009
#define IDX(x) ((x))
#define Sigma_size 128
#define max_node 50010
using namespace std;
char T[maxn];
char P[1010][60];
char cnt[1010];
struct ACTrie
{
int m;//模式串个数
int next[max_node][Sigma_size];
int f[max_node];//匹配失败函数
int val[max_node];
const int fail = -1;
int size;
void init()
{
val[0] = fail;memset(next[0],fail,sizeof(next[0]));size = 1;
}
int new_node()
{
memset(next[size],fail,sizeof(next[size]));
val[size]= fail;
size++;
return size-1;
}
void add(char * s,int id)//存储字符串编号可用于在文本串中输出
{
int state = 0,n = strlen(s);
for(int i=0 ; i<n ; ++i)
{
int id = IDX(s[i]);
if(next[state][id]==fail)
{
next[state][id] = new_node();
}
state =next[state][id];
}
val[state] =id;
}
void make_fail()
{
queue<int> Q;f[0] =0;
for(int i=0 ; i<Sigma_size ;++i)
if(next[0][i]!=fail){f[next[0][i]] = 0;Q.push(next[0][i]);}//depth =0的状态
else{next[0][i]= 0;}
while(!Q.empty())
{
int r = Q.front();Q.pop();
for(int i=0 ; i<Sigma_size ; ++i)
{
int s = next[r][i];
if(s!=fail){
Q.push(s);
f[s] = next[f[r]][i];
}else next[r][i] = next[f[r]][i];
}
}
}
void match(char * T)
{
int n = strlen(T);
int state= 0;
for(int i=0 ; i<n ; ++i)
{
int c = IDX(T[i]);
state = next[state][c];
int temp = state;
while(val[temp]!=fail)
{
cnt[val[temp]]++;
temp = f[temp];
}
}
}
};
ACTrie ac;
int main()
{
//freopen("H:\\c++\\file\\stdin.txt","r",stdin);
while(scanf("%d",&ac.m)!=EOF)
{
ac.init();
memset(cnt,0,sizeof(cnt[0])*(ac.m+1));
for(int i=1 ;i<=ac.m ; ++i)
{
scanf("%s",P[i]);
ac.add(P[i],i);
}
ac.make_fail();
scanf("%s",T);
ac.match(T);
for(int i=1 ; i<=ac.m ;++i )
{
if(cnt[i])
{
printf("%s: %d\n",P[i],cnt[i]);
}
}
}
return 0;
}
hdu 2222
这里要求的只是模式串中出现了几个,而且模式串可能会重复!
hdu 2896模板题