这篇博客拖了好久好久,真是尴尬,原因大概是我感觉很多东西,都是直觉,有种只可意会不可言传的味道,想完全搞懂,但后来仔细想想,ac自动机好像本来就不是一个非常非常确定的算法,比如说,我该拿觉很多情况可以把算法卡成n^2。所以拖了很久,酱紫。
先谈谈AC自动机是什么。我们知道KMP,可以快速的对一个字符串,用一个模板进行匹配。然而当我们有多个模板的时候,再去重复的使用KMP算法就显得不太合适了。所以我们找到了AC自动机。再简单的考虑一下,AC自动机可以说,是在一棵tire树上,挂上KMP的失败指针即可。
我们给出tire树部分的代码。
void insert(char *s)
{
int lenn = strlen(s),k = 1;
for (int i = 0;i < lenn;i++)
{
if (sz[k][s[i] - 'a' + 1]) k = sz[k][s[i] - 'a' + 1];else
{
sum++;
k = sz[k][s[i] - 'a' + 1] = sum;
}
}
gs[k]++;
}
显然这是非常非常常见的tire树写法,关于tire树,可以参考我以前的博客。 http://blog.csdn.net/qq_35772697/article/details/53444358
然后我们给出构造失败指针的代码。
void ac_match()
{
dl.push(1);
while (!dl.empty())
{
int tp = dl.front(),k;
dl.pop();
for (int i = 1;i <= 26;i++)
{
if (!sz[tp][i]) continue;
k = pt[tp];
while (!sz[k][i]) k = pt[k];
pt[sz[tp][i]] = sz[k][i];
dl.push(sz[tp][i]);
}
}
}
我们回忆一下KMP的算法,我们在求出第i位的失败指针的时候,只会用到,i前面的位置。所以同理,我那们在构造失败指针的时候,我们通过宽度有限搜索来进行构造,是再合适不过的了。这里我们明确一下我的数组的含义。dl,宽搜队列。sz[i][j]i号结点的下一位j字母的序号。pt[i] i 号结点的失败指针。我们来详细的说明pt[i]的含义。pt[i],指我们在i点尝试寻找他是否有儿子j的时候,如果寻找失败,我们应该去尝试pt[i]这个结点的儿子j。如果不理解,可以参考一下博客http://blog.csdn.net/qq_35772697/article/details/53442548
我们给出匹配的代码。
void solve(char *s)
{
int lenn = strlen(s),k = 1;
for (int i = 0;i < lenn;i++)
{
mark[k] = 1;
int t = s[i] - 'a' + 1;
while (!sz[k][t]) k = pt[k];
k = sz[k][t];
if (!mark[k])
{
for (int j = k;j;j = pt[j])
{
tot += gs[j];
gs[j] = 0;
}
}
}
printf("%d\n",tot);
}
这段匹配代码是基于下面的hdu模板题而言,是求出,有多少模板串可以和目标串匹配。继续明确一下数组含义,gs[i]记录以i号结点结束的单词的个数。tot为匹配模板串的数量。mark[i]则保证了,所有可能达到匹配的位置都进行了一次考虑。
最后我觉得我应该说明关于代码的一些边界处理。比如我将0号结点的所有26个字母都设为1,避免了因为完全无法匹配无法停止while的情况。
在这里推荐一道hdu的ac自动机的模板题,hdu2222
并且在此给出AC代码。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
using namespace std;
queue <int> dl;
int T,n,tot,sum;
bool mark[1000000];
int sz[1000000][30],gs[1000000],pt[1000000];
char tp[1200000];
void insert(char *s)
{
int lenn = strlen(s),k = 1;
for (int i = 0;i < lenn;i++)
{
if (sz[k][s[i] - 'a' + 1]) k = sz[k][s[i] - 'a' + 1];else
{
sum++;
k = sz[k][s[i] - 'a' + 1] = sum;
}
}
gs[k]++;
}
void ac_match()
{
dl.push(1);
while (!dl.empty())
{
int tp = dl.front(),k;
dl.pop();
for (int i = 1;i <= 26;i++)
{
if (!sz[tp][i]) continue;
k = pt[tp];
while (!sz[k][i]) k = pt[k];
pt[sz[tp][i]] = sz[k][i];
dl.push(sz[tp][i]);
}
}
}
void solve(char *s)
{
int lenn = strlen(s),k = 1;
for (int i = 0;i < lenn;i++)
{
mark[k] = 1;
int t = s[i] - 'a' + 1;
while (!sz[k][t]) k = pt[k];
k = sz[k][t];
if (!mark[k])
{
for (int j = k;j;j = pt[j])
{
tot += gs[j];
gs[j] = 0;
}
}
}
printf("%d\n",tot);
}
int main()
{
scanf("%d",&T);
for (int mi = 1;mi <= T;mi++)
{
for (int i = 0;i <= sum;i++)
{
pt[i] = gs[i] = mark[i] = 0;
for (int j = 1;j <= 26;j++) sz[i][j] = 0;
}
for (int i = 1;i <= 26;i++) sz[0][i] = 1;
tot = 0,sum = 1;
scanf("%d",&n);
for (int i = 1;i <= n;i++)
{
scanf("%s",tp);
insert(tp);
}
ac_match();
scanf("%s",tp);
solve(tp);
}
return 0;
}
先谈谈AC自动机是什么。我们知道KMP,可以快速的对一个字符串,用一个模板进行匹配。然而当我们有多个模板的时候,再去重复的使用KMP算法就显得不太合适了。所以我们找到了AC自动机。再简单的考虑一下,AC自动机可以说,是在一棵tire树上,挂上KMP的失败指针即可。
我们给出tire树部分的代码。
void insert(char *s)
{
int lenn = strlen(s),k = 1;
for (int i = 0;i < lenn;i++)
{
if (sz[k][s[i] - 'a' + 1]) k = sz[k][s[i] - 'a' + 1];else
{
sum++;
k = sz[k][s[i] - 'a' + 1] = sum;
}
}
gs[k]++;
}
显然这是非常非常常见的tire树写法,关于tire树,可以参考我以前的博客。 http://blog.csdn.net/qq_35772697/article/details/53444358
然后我们给出构造失败指针的代码。
void ac_match()
{
dl.push(1);
while (!dl.empty())
{
int tp = dl.front(),k;
dl.pop();
for (int i = 1;i <= 26;i++)
{
if (!sz[tp][i]) continue;
k = pt[tp];
while (!sz[k][i]) k = pt[k];
pt[sz[tp][i]] = sz[k][i];
dl.push(sz[tp][i]);
}
}
}
我们回忆一下KMP的算法,我们在求出第i位的失败指针的时候,只会用到,i前面的位置。所以同理,我那们在构造失败指针的时候,我们通过宽度有限搜索来进行构造,是再合适不过的了。这里我们明确一下我的数组的含义。dl,宽搜队列。sz[i][j]i号结点的下一位j字母的序号。pt[i] i 号结点的失败指针。我们来详细的说明pt[i]的含义。pt[i],指我们在i点尝试寻找他是否有儿子j的时候,如果寻找失败,我们应该去尝试pt[i]这个结点的儿子j。如果不理解,可以参考一下博客http://blog.csdn.net/qq_35772697/article/details/53442548
我们给出匹配的代码。
void solve(char *s)
{
int lenn = strlen(s),k = 1;
for (int i = 0;i < lenn;i++)
{
mark[k] = 1;
int t = s[i] - 'a' + 1;
while (!sz[k][t]) k = pt[k];
k = sz[k][t];
if (!mark[k])
{
for (int j = k;j;j = pt[j])
{
tot += gs[j];
gs[j] = 0;
}
}
}
printf("%d\n",tot);
}
这段匹配代码是基于下面的hdu模板题而言,是求出,有多少模板串可以和目标串匹配。继续明确一下数组含义,gs[i]记录以i号结点结束的单词的个数。tot为匹配模板串的数量。mark[i]则保证了,所有可能达到匹配的位置都进行了一次考虑。
最后我觉得我应该说明关于代码的一些边界处理。比如我将0号结点的所有26个字母都设为1,避免了因为完全无法匹配无法停止while的情况。
在这里推荐一道hdu的ac自动机的模板题,hdu2222
并且在此给出AC代码。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
using namespace std;
queue <int> dl;
int T,n,tot,sum;
bool mark[1000000];
int sz[1000000][30],gs[1000000],pt[1000000];
char tp[1200000];
void insert(char *s)
{
int lenn = strlen(s),k = 1;
for (int i = 0;i < lenn;i++)
{
if (sz[k][s[i] - 'a' + 1]) k = sz[k][s[i] - 'a' + 1];else
{
sum++;
k = sz[k][s[i] - 'a' + 1] = sum;
}
}
gs[k]++;
}
void ac_match()
{
dl.push(1);
while (!dl.empty())
{
int tp = dl.front(),k;
dl.pop();
for (int i = 1;i <= 26;i++)
{
if (!sz[tp][i]) continue;
k = pt[tp];
while (!sz[k][i]) k = pt[k];
pt[sz[tp][i]] = sz[k][i];
dl.push(sz[tp][i]);
}
}
}
void solve(char *s)
{
int lenn = strlen(s),k = 1;
for (int i = 0;i < lenn;i++)
{
mark[k] = 1;
int t = s[i] - 'a' + 1;
while (!sz[k][t]) k = pt[k];
k = sz[k][t];
if (!mark[k])
{
for (int j = k;j;j = pt[j])
{
tot += gs[j];
gs[j] = 0;
}
}
}
printf("%d\n",tot);
}
int main()
{
scanf("%d",&T);
for (int mi = 1;mi <= T;mi++)
{
for (int i = 0;i <= sum;i++)
{
pt[i] = gs[i] = mark[i] = 0;
for (int j = 1;j <= 26;j++) sz[i][j] = 0;
}
for (int i = 1;i <= 26;i++) sz[0][i] = 1;
tot = 0,sum = 1;
scanf("%d",&n);
for (int i = 1;i <= n;i++)
{
scanf("%s",tp);
insert(tp);
}
ac_match();
scanf("%s",tp);
solve(tp);
}
return 0;
}