从kmp到AC自动机

恰同学少年,凤华正茂;数风流人物,还看今朝。 / 2024-09-13 / 原文

知道kmp的请跳过这一段
找到最清晰的解析

kmp

我看了约114514个解析才搞懂

如何求next

首先,next[i]本应表示0~i的字符串的最长相同前缀后缀的长度。
不过为了方便匹配,实际可以存最长相同前缀后缀时前缀最后一个的地址
听起来好绕
那这么说吧:
例如串
abaabaabaab
next[0]=-1 肯定找不到
next[1]=-1 因为第一个前缀是a,它是b
next[2]=0 因为第一个前缀是a,它是a
next[3]=0 因为第一个前缀是a,它是a?
wait!不一样的点来啦!
先偷窥一下匹配的代码:

for (int i=1;i<m;i++){
    int j=next[i-1];
   	while ((s[j+1]!=s[i])&&(j>=0))j=next[j];
   	if (s[j+1]==s[i]) next[i]=j+1;
   	else next[i]=-1;
} 

也就是
a先问了它旁边那个,发现它匹配到了0
然后他也蠢蠢欲动
但下个是b
所以他的j被打到了next[2]=-1
然后循环没了
这时,他毫不惊奇地发现下一个是a
于是他就匹配上了(合理)
next[4]=1
因为它前面已经匹配到了,所以它可能是 前面匹配到的 前缀的地址+1的 那个字母
听起来还是好绕
说白了,它可以是
b先问了它旁边那个,发现它匹配到了0
然后他也蠢蠢欲动
发现下个是b
所以循环没了
这时,他毫不(是真的)惊奇地发现下一个是b
于是
abaab(内两b匹配上了)
理解了吧?
剩下的自己推,别问我
那么,现在烤馍片完成了,该做什么呢

next的作用

以下搬运自dalao题解,此处F[i]指next[i]+1
我们还是先给出一个例子:
A="abaabaabbabaaabaabbabaab"
B="abaabbabaab"
当然读者可以通过手动模拟得出只有一个地方匹配
abaabaabbabaaabaabbabaab
我们再用i表示当前A串要匹配的位置(即还未匹配),j表示当前B串匹配的位置(同样也是还未匹配),补充一下,若i>0则说明i-1是已经匹配的啦(j同理)。
首先我们还是从0开始匹配:

此时,我们发现,A的第5位和B的第5位不匹配(注意从0开始编号),此时i=5,j=5,那么我们看F[j-1]的值:
F[5-1]=2;
这说明我们接下来的匹配只要从B串第2位开始(也就是第3个字符)匹配,因为前两位已经是匹配的啦,具体请看图:

然后再接着匹配:

我们又发现,A串的第13位和B串的第10位不匹配,此时i=13,j=10,那么我们看F[j-1]的值:
F[10-1]=4
这说明B串的03位是与当前(i-4)(i-1)是匹配的,我们就不需要重新再匹配这部分了,把B串向后移,从B串的第4位开始匹配:

这时我们发现A串的第13位和B串的第4位依然不匹配

此时i=13,j=4,那么我们看F[j-1]的值:
F[4-1]=1
这说明B串的第0位是与当前i-1位匹配的,所以我们直接从B串的第1位继续匹配:

但此时B串的第1位与A串的第13位依然不匹配

此时,i=13,j=1,所以我们看一看F[j-1]的值:
F[1-1]=0
好吧,这说明已经没有相同的前后缀了,直接把B串向后移一位,直到发现B串的第0位与A串的第i位可以匹配(在这个例子中,i=13)

再重复上面的匹配过程,我们发现,匹配成功了!

这就是KMP算法的过程。
另外强调一点,当我们将B串向后移的过程其实就是i++,而当我们不动B,而是匹配的时候,就是i++,j++,这在后面的代码中会出现,这里先做一个说明。
最后来一个完整版的(dalao:话说做这些图做了好久啊!!!!):

kmp例题代码实现:

#include <bits/stdc++.h>
using namespace std;
string s1,s2;
int n,m,i,j,_next[1000010];
int main(){
	cin>>s1>>s2;
	n=s2.size();
	m=s1.size();
	_next[0]=-1;
	for (i=1;i<n;i++){
	    j=_next[i-1];
    	while ((s2[j+1]!=s2[i])&&(j>=0))j=_next[j];
    	if (s2[j+1]==s2[i]) _next[i]=j+1;
    	else _next[i]=-1;
	}
	i=0;j=0;
	while (i<m) {
		if (s1[i]==s2[j]) {
			i++;
			j++;
			if(j==n)cout<<i-n+1<<endl,j=_next[j-1]+1;
		}else{
			if (j==0)i++;
			else j=_next[j-1]+1;
		}
	}
	for (i=0;i<n;i++){
	    cout<<_next[i]+1<<" ";
	}
	return 0;
}

c,怎么这么难调
知道Trie的请跳过这一段
网上的解析

Trie

大概论述一下过程:

  1. 选定要加入到Trie树中的字符串
  2. 从根节点开始依次判断当前结点的子节点中是否包含下一个字符
  3. 如果包含,则直接访问,重复第2步
  4. 否则,则建立这个结点,继续重复第2步
  5. 若进行第2步时已经到达了最后一个字符,则直接结束
    那么,我们来举个例子
    假如,我要构建成字典树的单词是her hen hers say said
    最终构建完的字典树就长这样:
   root
  /   \
  h   s
 /     \
 e      a
/ \    / \
r n    y i
|        |
s        d

然而我们并不知道这些东西分别代表那些字符串
于是,我们对每个字符串的结尾所在的那个节点加个标记。
于是乎,我们从根节点开始,一层层依次遍历,当读入到一个加了标记的结点时,一路读到的字符连成的字符串便是原来需要储存的字符串
那么,至于怎么加入一个串呀。。。
我们也来演示一下吧、。。。
比如当前这样子:

   root
  /   \
  h   s
 /     \
 e      a
/ \    / 
r n    y 
|        
s        

其他的串都已经加入,我们现在需要加入字符串said
那么,我们从根节点开始

   ro_ot(用_表示当前节点)
  /   \
  h   s
 /     \
 e      a
/ \    / 
r n    y 
|        
s        

发现根节点的子节点里面存在s这个字符结点
把一个指针移动过去,继续找接下来的字符

   root
  /   \
  h   _s
 /     \
 e      a
/ \    / 
r n    y 
|        
s        

接着,我们惊奇的发现a也存在了,于是继续遍历

   root
  /   \
  h   s
 /     \
 e      _a
/ \    / 
r n    y 
|        
s        

这个时候,却发现当前结点不存在一个i结点,那么,我们就手动的造一个i结点出来

   root
  /   \
  h   s
 /     \
 e      a
/ \    / \
r n    y i
|        
s        

同理:

   root
  /   \
  h   s
 /     \
 e      a
/ \    / \
r n    y i
|        |
s        d

OK,上代码!

#include <bits/stdc++.h>
using namespace std;
int n,end=0;
vector<int> fa(100005),son(100005),nth(100005),sa[26](100005);//窝喜欢用vector,别喷窝啊……
void build(int where,string s,int now){
	if(now>=s.size())return;
	if(son[where]&(1<<int(c[now]-'a')))build(sa[int(c[now]-'a')][where],s,now+1);
	else{
		end++;
		fa[end]=where;
		son[where]|=(1<<int(c[now]-'a'));
		sa[int(c[now]-'a')][where]=end;
	} 
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	while(cin>>n&&n){
		fa=nth;
		son=nth;
		fail=nth;
		end=0;
		for(int i=1;i<=n;i++){
			string s;
			cin>>s;
			build(0,s,0);
		}
	}
	return 0;
}