学习笔记:KMP/AC自动机/trie图/fail树

前言

KMP是一个经典的字符串匹配算法。

然后AC自动机是基于KMP思想的一个多模板匹配算法。

trie图是AC自动机的一个优化。

fail树是AC自动机中fail指针构成的有特殊性质的树。

KMP算法

算法原理

设两个字符串长度为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)。

AC自动机

作用

用来做多模板匹配的,经常用来优化查找,经常和DP结合。

工作原理

有时候需要多模板匹配,就需要AC自动机了。同样是fail指针,不过AC自动机不再是个串,而是多个串组成的trie树。

匹配的时候同样是在失配时走到最长的前缀=当前后缀的地方。

实现方法

首先fail指针肯定是往层数比自己小的结点指,所以我们BFS预处理即可。

辅助数组(很重要)

last数组(后缀链接):由于我们在很长一个串上,有可能有些单词是当前串的后缀,而我们无法统计到它,last数组表示沿着fail找。

时间复杂度分析

预处理其实可以通过下面的代码看到都是O(1)预处理每个结点的。

匹配的时候分析方法同KMP,串的增加才会带来结点深度的增加,所以沿着fail走也不会超过n次。

所以时间复杂度是O(m*l+n)的线性时间。

trie图优化

我们在匹配的时候如果当前结点没有需要的儿子结点,那么就需要沿着fail指针走到第一个有当前需要的儿子。这样挺麻烦的,所以当无法转移时,我们直接在trie上连向第一个可以转移的点,trie树就变成了一个DAG图,写代码直接调用对应儿子即可。实际上这是保证了预处理的时间复杂度。

fail树

这不是个优化,是有时候对应的题型,因为每个结点fail指针唯一,所以刚好可以组成一棵树。

这棵树的性质是一个结点所代表串是所有子树结点代表的串的后缀,可以用来统计一些字符串出现次数什么的。

经典的一道题是NOI2011阿狸的打字机。

建议单独建立fail树,但是和原trie联系依然很紧密。

代码

几个注意的点

  1. rt从0开始有助于转移。
  2. fail数组必须要,last数组看需要。
  3. 建造fail指针的时候,需要手工加入第一层结点,否则答案会错。

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;
}

你可能感兴趣的:(AC自动机/Fail树,字符串,学习笔记/板子)