由于博主自己不大会字符串算法,于是照旧打算从头写起
一、朴素的字符串匹配算法(Naive String Matching Algorithm)
又名暴力匹配算法(Brute Force Algorithm)
没有预处理
失配后向后移动一位
复杂度:$O(n^2)$
Code
1
2
3
4
5
6
7
8
9
10
11void (char *a,char *b)
{
int lena = strlen(a + 1),lenb = strlen(b + 1);
for(int i = 1;i <= lena; ++i)
for(int j = 1;j <= lenb; ++j)
{
if(a[i + j - 1] != b[j]) break;
return 1;
}
return 0;
}
二、哈希(Hash)
哈希($Hash$)是一种鹅妹子嘤的算法
在预处理后可以做到$O(1)$查找一个对象
一句话原理:以空间换时间。 ——蒋介石
具体实现:
栗子:
设计一个程序,以完成以下输入输出:
第一行,输入两个数字$n,m$。
第二行,输入$n$个字符串。
第三行,输入$m$个字符串。对于每一个字符串,询问其是否存在于第二行输入$n$个字符串中,若是则输出”YES”,若不是则输出”NO”。数据说明:输入的字符串的长度范围在$[1,10^5]$。
如果这道题的字符串都改成数字$[1leqslant a leqslant10^5]$,那是个人都会做,只要建一个$bool$数组$vis[10^5]$记录每一个数字是否出现过就做完了。
现在变成了字符串,也是可以做的;
因为有一个东西叫做$map$;
但是这个东西常数大啊怎么办?
没事,可以自己手写;
我们把每一个字符串看成一个$M$进制的$len$位数,其中这里$M$表示大于字符集大小的一个数,可以取130,233,666什么的。
为了避免溢出,我们对这个大数进行一个取模,$mod$可以取一个类似$99991$的质素。
Code
1
2
3
4
5
6
7
8
9
10const int M = 131;
const int mod = 99991;
int Hash(char *a)
{
int hash = 0,n = strlen(a + 1);
for(int i = 1;i <= len; ++i)
hash = (hash * M + a[i]) % mod;
return hash;
}那么问题来了,如果$hash$的值冲突怎么办呢?
1、通过增大哈希池大小来降低哈希值碰撞的问题,即调大$vis$数组大小与$mod$的大小(双模数Hash也可以),不足之处显而易见,空间大小有限定。
2、对于每个字符串的哈希值记录原有字符串,在冲突的情况下用别的方法匹配($NSMA$或$KMP
$)。
Hash如果不做一些特别的处理,很容易被卡,望各位自重。
三、Knuth-Morris-Pratt字符串匹配算法(KMP)
我们来观察一下$NSMA$算法,发现每一次模式串失配后文本串只会往后跳一位且重置模式串至第一位。
实际上我们并没有必要每一次都把模式串重置
设文本串为$s_1$,模式串为$s_2$,当前匹配到$s_1$的的第$i$个位置,$s_2$的第$j$个位置,$s_1[i] neq s_2[j]$
如果存在一个$k$,使得$s_2[1 cdots k] = s_2[j - k cdots j - 1]$,则$j$不必再重置至第一位,可以直接重置至第$k$位,因为前面的字符都一样。
重点来了:如何求这些$k$是多少呢?
因为在模式串的每一个位置都有可能发生失配,所以我们要对每一个$j$求出它对应的$k$,用$next$数组来存,$next[j] = k$,表示当$s_2[j] != s_1[i]$时,$j$的指针的下一个位置。
当$j = 1$时,显然失配后只能重新开始匹配,所以$next[1] = 0$,木得问题。
当$j > 1$时呢?如图。
我们可以发现:当$p[k] = p[j]$时,有$next[j + 1] = next[j] + 1$
看图感性理解,易证,留给读者当作业(滑稽.jpg)
如果$p[k] ne p[j]$怎么办呢?
如图:
此时,我们应将$k$的值赋为$next[k]$,因为我们已经找不到一个最长的前缀串满足$s_2[1 cdots k] = s_2[j - k cdots j - 1]$,所以我们只能找次小的前缀串满足该条件,显然,这个次小的前缀串就是$next[k]$。
小优化:
按上述方法找到的$next$大致上已经没什么问题了,但是遇到部分情况还是会多出一切不必要的比较。
如下图:
显然,在上述匹配过程中,当前位置产生失配时,$j$会跳到$next[j]$,也就是$2$,但是,这一步是完全没有意义的,因为我们已经知道这一位用$B$去匹配是会失配的,在跳了一下$next$数组后,我们用来匹配的字符却还是$B$,显然还是失配的。
解决:在求$next$时判一个相等就好了。
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using namespace std;
const ll maxn = 1e6 + 5 ;
ll Next[maxn],ln,lm;
char n[maxn],m[maxn];
void kmp()
{
ll k = 0;
Next[1] = k;
rep(i,2,lm)
{
while(k && m[i - 1] != m[k]) k = Next[k];
if(m[i] == m[++k]) Next[i] = Next[k];
else Next[i] = k;
}
}
int main()
{
scanf("%s %s",n + 1,m + 1);
ln = strlen(n + 1);
lm = strlen(m + 1);
kmp();
ll cnt = 1,i = 1;
while(i <= ln)
{
if(n[i] == m[cnt] || !cnt) ++cnt,++i;
else cnt = Next[cnt];
if(cnt == lm)
{
printf("%lld",i - lm + 1);
return 0;
}
}
puts("impossible");
return 0;
}
四、Aho-Corasick automation多字符串匹配算法(AC自动机)
对于多字符串的匹配问题,我们一般会用$Hash$或者$Trie$储存。
此处不讲$Hash$($hash$大法好啊)
$AC$自动机是什么?在$trie$上搞一个类似$KMP$的那个$next$数组的一个失配指针。我们用$fail$来表示这个东西。
我们先对题中给出若干个模式串建出一颗$trie$树,不过我们对这个数的每一个节点都添加一个信息,就是该节点的$fail$指针。
那我们怎么求这个$fail$指针呢?首先,深度为$2$的节点(即根节点的儿子)的$fail$指针都指向根节点,然后对于每一个节点$v$,它的$fail$指向:沿着它的父亲节点的失配指针,一直向上,直到我们找到拥有当前字母的子节点的节点的那个子节点。
具体实现可以看yyb大佬的代码
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
大专栏 「算法学习」字符串入门ss="line">28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
using namespace std;
struct Tree//字典树
{
int fail;
int vis[26];//子节点的位置
int end;//标记以这个节点结尾的单词编号
}AC[100000];//Trie树
int cnt=0;//Trie的指针
struct Result
{
int num;
int pos;
}Ans[100000];//所有单词的出现次数
bool operator <(Result a,Result b)
{
if(a.num!=b.num)
return a.num>b.num;
else
return a.pos<b.pos;
}
string s[100000];
inline void Clean(int x)
{
memset(AC[x].vis,0,sizeof(AC[x].vis));
AC[x].fail=0;
AC[x].end=0;
}
inline void Build(string s,int Num)
{
int l=s.length();
int now=0;//字典树的当前指针
for(int i=0;i<l;++i)//构造Trie树
{
if(AC[now].vis[s[i]-'a']==0)//Trie树没有这个子节点
{
AC[now].vis[s[i]-'a']=++cnt;//构造出来
Clean(cnt);
}
now=AC[now].vis[s[i]-'a'];//向下构造
}
AC[now].end=Num;//标记单词结尾
}
void Get_fail()//构造fail指针
{
queue<int> Q;//队列
for(int i=0;i<26;++i)//第二层的fail指针提前处理一下
{
if(AC[0].vis[i]!=0)
{
AC[AC[0].vis[i]].fail=0;//指向根节点
Q.push(AC[0].vis[i]);//压入队列
}
}
while(!Q.empty())//BFS求fail指针
{
int u=Q.front();
Q.pop();
for(int i=0;i<26;++i)//枚举所有子节点
{
if(AC[u].vis[i]!=0)//存在这个子节点
{
AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
//子节点的fail指针指向当前节点的
//fail指针所指向的节点的相同子节点
Q.push(AC[u].vis[i]);//压入队列
}
else//不存在这个子节点
AC[u].vis[i]=AC[AC[u].fail].vis[i];
//当前节点的这个子节点指向当
//前节点fail指针的这个子节点
}
}
}
int AC_Query(string s)//AC自动机匹配
{
int l=s.length();
int now=0,ans=0;
for(int i=0;i<l;++i)
{
now=AC[now].vis[s[i]-'a'];//向下一层
for(int t=now;t;t=AC[t].fail)//循环求解
Ans[AC[t].end].num++;
}
return ans;
}
int main()
{
int n;
while(233)
{
cin>>n;
if(n==0)break;
cnt=0;
Clean(0);
for(int i=1;i<=n;++i)
{
cin>>s[i];
Ans[i].num=0;
Ans[i].pos=i;
Build(s[i],i);
}
AC[0].fail=0;//结束标志
Get_fail();//求出失配指针
cin>>s[0];//文本串
AC_Query(s[0]);
sort(&Ans[1],&Ans[n+1]);
cout<<Ans[1].num<<endl;
cout<<s[Ans[1].pos]<<endl;
for(int i=2;i<=n;++i)
{
if(Ans[i].num==Ans[i-1].num)
cout<<s[Ans[i].pos]<<endl;
else
break;
}
}
return 0;
}