AC自动机是真的难啊>_<.
听到自动机,我第一印象是KMP算法,因为字符串匹配算法从某种意义上也是一种自动机,先回顾一下KMP是如何进行操作的,这里就先简单介绍一下(不能喧宾夺主,之后会特别写一期),KMP最重要的就是记录下最长后缀,我喜欢把ne数组叫做上帝数组,后面的操作就是给定一个楔子,通过上帝数组进行一系列神奇的操作,自动跑完整个过程,得出我们想要的答案。
其实AC自动机也是一样的,不过是把一切都放到了一棵Trie树上进行操作,那么最重要的就是建Trie树和建自动机的过程,巧的是,AC自动机里存回跳边的数组也是ne。
第一步,建一棵Trie树,这一部分和我上次发的是一样的,建树能有什么坏心思呢?代码如下:
void insert(char *s)
{
int p = 0;
for(int i = 0; s[i]; i ++)
{
int j = s[i] - 'a';
if(!ch[p][j])
ch[p][j] = ++ idx;
p = ch[p][j];
}
cnt[p] ++;
}
接下来就是最难懂的部分:建自动机。
首先我们需要明白两个概念:回跳边和转移边。
所谓回跳边就是当前节点在先前已经建好的树的链上所能连接上的位置,举个例子,比如有两个单词,her和she,我们知道在建字典树的时候,这两个单词是被存在不同的链上的,假如我们已经建好了树,在我们构建自动机的时候,我们走到了she中的e位置,那么从前后缀的角度,两个单词中“he”的部分是公共的,我们就可以将she中的e接到her中的e上,这样我们之后在检索求答案的时候就可以忽略掉重复部分,从非重复部分开始进行下一步操作。所以我们不难知道回跳边所指的节点是当前节点的最长后缀,同时,我们是采用bfs的方式进行建树操作,因此,我们是通过当前节点的父节点进行建回跳边的,一层一层向上推,我们不难知道,当前节点,当前节点的父节点,父节点的回跳边指向的点,当前点的回跳边指向的点,四个点构成了一个四边形。
如果我们走到一个点发现没有儿子节点怎么办?我们就将当前节点转移到之前回跳边的下一位节点,保证不重复,因此当没儿子节点的时候,当前节点自建转移边,注意转移边是为了找到下一位,因此是建立在树上的,而回跳边是在找到时继续向下寻找用的,相当于一条虚拟边,所以需要一个ne数组存储。
先看代码:
void build()
{
queue<int> q;
for(int i = 0; i < 26; i ++)
if(ch[0][i])
q.push(ch[0][i]);
while(!q.empty())
{
int u = q.front();
q.pop();
for(int i = 0; i < 26; i ++)
{
int v = ch[u][i];
if(v)
{
ne[v] = ch[ne[u]][i];
q.push(v);
}
else
ch[u][i] = ch[ne[u]][i];
}
}
}
我们首先将每一个要查找的单词的首字母入队,如果队列不为空,我们就依次向下找接下来的字母,由于我们是通过字母映射来建树的,所以我们需要一个一个字母比对过去,如果我们发现有一个字母可以连上,我们就通过父节点为儿子节点建一条回跳边,如果找不到儿子节点,就为自己建一条转移边。
这里关于回跳边和转移边的功能只是简单介绍(这次是小屁孩不懂事,写着玩),在检索的过程中我会具体讲他们的功能。(例题下次粘)