KMP是一个经典的字符串匹配算法。
然后AC自动机是基于KMP思想的一个多模板匹配算法。
trie图是AC自动机的一个优化。
fail树是AC自动机中fail指针构成的有特殊性质的树。
设两个字符串长度为n和m。
两个模板匹配,如果暴力匹配是暴力枚举起点,最坏时间复杂度O(n * m * min(n,m) )
利用一个叫做失配指针的东西,f[i]表示当前字符串中等于当前后缀的前缀。
举个例子:abcdffffabcd,那么最后一个失配指针就是4,因为’abcd’=’abcd’
预处理初这个失配指针,然后就可以在当前失配的时候快速找到第一个可能被配对的元素。
这里的代码有用到我们习惯用1为第一个元素,但是下标开始为0这个性质,一些字符就少了调整的需要,看起来比较简单,但可能理解稍微有点不容易,注意上面提到的这个细节即可。
void get_fail(char *p,int *f)
{
int n=strlen(p),i=0,j=-1;//注意j的初值,j是当前前缀。
f[0]=-1;//这是为了直接失配比较方便转移。
while(iwhile(j>=0&&p[i]!=p[j])j=fail[j];
i++;j++;
fail[i]=j;
}
}
int KMP(char *p,char *t)
{
get_fail(p,fail);
int n=strlen(p),m=strlen(t);
int i=0,j=0,cnt=0;
while(iwhile(j>=0&&p[j]!=t[i])j=fail[j];
i++,j++;
if(j==n)cnt++,j=fail[j];
}
return cnt;
}
时间复杂是O(n+m)的,预处理n,然后匹配m
证明的话需要结合代码。
可能根据fail指针的确不知道j要递减次,但是j增加的时候i也增加了,所以最终时间复杂度是线性的,预处理O(n)+匹配O(m)。
用来做多模板匹配的,经常用来优化查找,经常和DP结合。
有时候需要多模板匹配,就需要AC自动机了。同样是fail指针,不过AC自动机不再是个串,而是多个串组成的trie树。
匹配的时候同样是在失配时走到最长的前缀=当前后缀的地方。
首先fail指针肯定是往层数比自己小的结点指,所以我们BFS预处理即可。
last数组(后缀链接):由于我们在很长一个串上,有可能有些单词是当前串的后缀,而我们无法统计到它,last数组表示沿着fail找。
预处理其实可以通过下面的代码看到都是O(1)预处理每个结点的。
匹配的时候分析方法同KMP,串的增加才会带来结点深度的增加,所以沿着fail走也不会超过n次。
所以时间复杂度是O(m*l+n)的线性时间。
我们在匹配的时候如果当前结点没有需要的儿子结点,那么就需要沿着fail指针走到第一个有当前需要的儿子。这样挺麻烦的,所以当无法转移时,我们直接在trie上连向第一个可以转移的点,trie树就变成了一个DAG图,写代码直接调用对应儿子即可。实际上这是保证了预处理的时间复杂度。
这不是个优化,是有时候对应的题型,因为每个结点fail指针唯一,所以刚好可以组成一棵树。
这棵树的性质是一个结点所代表串是所有子树结点代表的串的后缀,可以用来统计一些字符串出现次数什么的。
经典的一道题是NOI2011阿狸的打字机。
建议单独建立fail树,但是和原trie联系依然很紧密。
HDU 2222 Keywords Search
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn=1e4+105,maxs=1e6+105;
int T;
int n,buc[maxn*55];
char s[maxs];
struct AC_zidongji
{
static const int mx_sz=26;
int rt,np,ch[maxn*55][30],val[maxn*55],cnt[maxn*55],fail[maxn*55],last[maxn*55];
void Initial()
{
rt=np=0;//这里最好是从0开始,比较利于码代码,空结点直接对应跟接电就比较好
memset(cnt,0,sizeof(cnt));
memset(ch,0,sizeof(ch));
memset(val,0,sizeof(val));
memset(fail,0,sizeof(fail));
}
int getid(char c)
{
return c-'a';
}
void ins(char *s,int k)
{
int now=rt;
for(int i=0;s[i];i++)
{
int id=getid(s[i]);
now=ch[now][id]?ch[now][id]:ch[now][id]=++np;
}
cnt[now]++;//可能多次访问同一结点
val[now]=k;
}
void getfail()//因为我们懒得存fa数组,所以我们在父亲结点计算自己点的fail
{
queue<int>q;
fail[rt]=0;
for(int p=0;p//必须单独考虑第一层结点咯
{
int j=ch[rt][p];
if(j){fail[j]=0;q.push(j);last[j]=0;}
}
while(!q.empty())
{
int i=q.front();q.pop();
for(int p=0;pint j=ch[i][p];
if(!j){ch[i][p]=ch[fail[i]][p];continue;}
q.push(j);
int v=fail[i];
while(v && !ch[v][p])v=fail[v];
fail[j]=ch[v][p];
last[j]=val[fail[j]]?fail[j]:last[fail[j]];
}
}
}
void Count(int now)
{
if(now)
{
buc[val[now]]=cnt[now];
Count(last[now]);
}
}
void query(char *s)
{
int now=rt;
for(int i=0;s[i];i++)
{
int id=getid(s[i]);
now=ch[now][id];
val[now]?Count(now):Count(last[now]);
}
}
}AC;
void Init()
{
scanf("%d",&n);
memset(buc,0,sizeof(buc));
AC.Initial();
for(int i=1;i<=n;i++)
{
scanf("%s",s);
AC.ins(s,i);
}
AC.getfail();
}
void work()
{
scanf("%s",s);
AC.query(s);
int ans=0;
for(int i=1;i<=n;i++)ans+=buc[i];
printf("%d\n",ans);
}
int main()
{
//freopen("in.txt","r",stdin);
scanf("%d",&T);
while(T--)
{
Init();
work();
}
return 0;
}