AC自动机 & Fail树 专题练习

时间:2023-03-09 00:24:09
AC自动机 & Fail树 专题练习

Fail树就是AC自动机建出来的Fail指针构成的树。

【bzoj3172】【xsy1713】单词

题意

给定一些单词,求每个单词在所有单词里面的出现次数。

分析

构建Fail树,记录每个单词最后一个位置在Fail树上的位置。

每个单词访问的每个节点的\(has+1\)。

然后在Fail树上进行树形dp。

每次询问,求对应位置的dp值。

核心代码

int n;
char s[L];

int rt,tot;
int loc[N];
struct Trie {
    int nx[A];
    LL has;
    int fail;
}tr[S];

int q[S];
int qh,qt;

void Ins(int frm,int &now,char *s,int cur,int len) {
    if (!now)
        now=++tot;
    tr[now].has++;

    if (cur==len+1) {
        loc[frm]=now;
        return;
    }

    int go=s[cur]-'a';
    Ins(frm,tr[now].nx[go],s,cur+1,len);
}

void Build(void) {
    rep(i,0,A-1)
        if (!tr[rt].nx[i])
            tr[rt].nx[i]=rt;
        else {
            tr[tr[rt].nx[i]].fail=rt;
            q[++qt]=tr[rt].nx[i];
        }
    while (qh!=qt) {
        int now=q[++qh];
        rep(i,0,A-1)
            if (!tr[now].nx[i])
                tr[now].nx[i]=tr[tr[now].fail].nx[i];
            else {
                tr[tr[now].nx[i]].fail=tr[tr[now].fail].nx[i];
                q[++qt]=tr[now].nx[i];
            }
    }
    per(i,qt,1)
        tr[tr[q[i]].fail].has+=tr[q[i]].has;
}

int main(void) {
    #ifndef ONLINE_JUDGE
    freopen("xsy1713.in","r",stdin);
    freopen("xsy1713.out","w",stdout);
    #endif

    rt=tot=1; scanf("%d",&n);
    rep(i,1,n) {
        scanf("%s",s+1);
        Ins(i,rt,s,1,strlen(s+1));
    }
    Build();

    rep(i,1,n) {
        LL t=tr[loc[i]].has;
        printf("%lld\n",t);
    }

    return 0;
}

【bzoj2754】【xsy1712】喵星球上的点名

题意

知道\(N\)位学生的姓、名。

\(M\)次点名,每次若点到某位学生姓或名的子串,该学生要报告。

问每次有多少学生报告,且最后每位学生报告多少次。

分析1

把每次点名的信息,即多个字符串,建立AC自动机。

对于每一位学生,考虑它对答案的贡献。

一种暴力的想法是:

把学生的姓和名分别在AC自动机上跑一遍,跑到一个位置的时候沿着Fail指针往上跳暴力更新答案,用时间戳维护某位学生是否在某次中被点名。

由于随机生成的树的深度不会特别大。

所以这种做法的效率虽然是\(O(n^2)\),但是实际跑起来会很快。

这里有很多小小的细节。

由于这个字符没有什么限制,所以考虑使用map来连边,然后遍历的时候还要用map<int,int> iterator

由于一个终点位置可能有多个字符串,所以要使用vector来记录。

核心代码:

//...

#define rep(i,a,b) for (int i=(a);i<=(b);i++)
#define pb push_back

typedef map<int,int> MI;
typedef map<int,int>::iterator MII;
typedef vector<int> VI;

//...

int rd(void);

int n,m;
struct Name {
    int len; VI lis;
    void Read(void) {
        len=rd();
        rep(i,1,len) {
            int x=rd();
            lis.pb(x);
        }
    }
}fam[N],uni[N],men[M];

int q[S];
int qh,qt;

int rt,tot;
struct T {
    int fail; MI nx;
    VI edp;
}tr[S];

int cnt[N];
int vis[M],eff[M];

//...

void Init(void) {
    rt=tot=1;
    tr[rt].fail=rt;
}

void Ins(int frm,int &now,Name nam,int cur) {
    if (!now)
        now=++tot;
    if (cur==nam.len+1) {
        tr[now].edp.pb(frm);
        return;
    }
    int go=nam.lis[cur-1];
    Ins(frm,tr[now].nx[go],nam,cur+1);
}

void Build(void) {
    q[++qt]=rt;
    while (qh!=qt) {
        int now=q[++qh];
        for (MII it=tr[now].nx.begin();it!=tr[now].nx.end();it++) {
            int go=it->first,nxp=it->second; int t=tr[now].fail;
            while (t!=rt&&!tr[t].nx.count(go))
                t=tr[t].fail;
            if (tr[t].nx.count(go)&&now!=rt)
                t=tr[t].nx[go];
            tr[nxp].fail=t;
            q[++qt]=nxp;
        }
    }
}

int Trans(int now,int go) {
    int t=now;
    while (t!=rt&&!tr[t].nx.count(go))
        t=tr[t].fail;
    if (tr[t].nx.count(go))
        t=tr[t].nx[go];
    return t;
}

void Update(int frm,int now) {
    int t=now;
    while (t!=rt) {
        rep(i,1,tr[t].edp.size()) {
            int to=tr[t].edp[i-1];
            if (vis[to]!=frm) {
                vis[to]=frm;
                eff[to]++;
                cnt[frm]++;
            }
        }
        t=tr[t].fail;
    }
}

void Traver(int frm,Name nam) {
    int now=rt;
    rep(i,1,nam.len) {
        int go=nam.lis[i-1];
        now=Trans(now,go);
        Update(frm,now);
    }
}

int main(void) {
    //...

    n=rd(),m=rd();
    rep(i,1,n) {
        fam[i].Read();
        uni[i].Read();
    }
    rep(i,1,m)
        men[i].Read();

    Init();
    rep(i,1,m)
        Ins(i,rt,men[i],1);
    Build();

    rep(i,1,n) {
        Traver(i,fam[i]);
        Traver(i,uni[i]);
    }
    //...
}

分析2

上述算法的问题的复杂度瓶颈出现在:

跑到一个位置的时候沿着Fail指针暴力更新答案

这一步随机下来虽然很快,但是它是\(O(n)\)的。

我们考虑对它进行优化。

对于每一个学生,它访问的节点,是Fail树上多条到根的路径的并。

考虑两个询问。

【询问2】

求经过一个点的路径的并的个数

我们只需要把路径的并上的每一个点的点权+1即可。

实现方法(粘自popoqqq的博客)

http://blog.csdn.net/PoPoQQQ/article/details/43020531

将所有节点按照DFS序排序

每个点到根的路径上的所有节点权值+1

相邻两个点的LCA到根的路径上的所有节点权值-1

即是树链的并

当然我们不必修改路径查询单点 只需对单点进行修改 然后查询子树 用树状数组维护DFS序即可

然而这道题并不用动态更改。

所以前缀标记一下就好了。

【询问1】求一个路径的并经过的点的点权之和

把每个作为字符串结尾的点的点权赋为1。

在树上预处理一下前缀和。

每次树上倍增,并用前缀和快速搞出来就好了。

分析3

前两种方法都是对点名的信息建立AC自动机。

但实际上,对姓、名建立AC自动机亦可以。

首先,根据题目的限制条件。

我们可以把姓和名用一个连字符连接起来,当做一个字符串来处理。

对姓名建立AC自动机,标记终点。

然后顺序处理每一个点名信息。

【询问1】首先把该点名信息在AC自动机上跑到终点。

若跑不到终点就跑不了了,那么答案就是0。

若跑到了终点,那么答案就是该点在Fail树上对应的子树中,有多少个点被标记。

这个东西提前进行树形dp即可。

【询问2】把点名信息放在Fail树上。

Fail树自顶向下遍历,统计每个点到根的路径中被标记的次数。

小结

(1)多个字符串的处理手段

对于多个字符串的匹配等问题,常考虑能否把多个字符串通过中间加连字符的方式合并为一个字符串,化简问题。

这种思考的东西通常是逐步满足的思考。

(2)树链的并

一些初步的认知:

求dfn序。相邻两个求LCA进行转化。

(3)树上的暴力算法

树上有种随机起来效率不错的暴力算法:树上每个点往根跳。

这很多时候可以骗分。

这基于随机情况下,树上的点的深度不会特别大。

(4)关于map

typedef map<int,int> MI;
typedef map<int,int>::iterator MII;
MII mp;
void Traverse(void) {
    for (MII it=mp.begin();it!=mp.end();it++) {
        int x=it->first,y=it->second;
        //...
    }
}

【bzoj3881】【xsy1714】Divljak

题意

Alice有n个字符串S_1,S_2...S_n,Bob有一个字符串集合T,一开始集合是空的。

接下来会发生q个操作,操作有两种形式:

“1 P”,Bob往自己的集合里添加了一个字符串P。

“2 x”,Alice询问Bob,集合T中有多少个字符串包含串S_x。(我们称串A包含串B,当且仅当B是A的子串)

Bob遇到了困难,需要你的帮助。

分析

分析1

把\(S_1,S_2,...,S_n\)建立AC自动机。

那么,对于每个操作1,相当于对到根的树链的并上的每一个点增值;对于操作2,相当于求单点的值。

再次转化:单点修改,子树求和。

BIT+dfn序维护即可。

核心代码

//...

int n;
char s[L];

int rt,tot; int loc[N];
struct T {
    int nx[A];
    int fail;
}tr[S];
int q[S]; int qh,qt;

struct G {
    int v,nx;
    G(int _v=0,int _nx=0) {
        v=_v,nx=_nx;
    }
}mp[E];
int totG,hd[S];

int dep[S]; int unit,pre[U][S];
int siz[S]; int dfn[S],num;

int m;
char p[L];
int lis[L];

int trA[S];

void InsTrie(int frm,char *s,int len) {
    int now=rt;
    rep(i,1,len) {
        int go=s[i]-'a';
        if (!tr[now].nx[go])
            tr[now].nx[go]=++tot;
        now=tr[now].nx[go];
    }
    loc[frm]=now;
}
void Build(void) {
    rep(i,0,A-1)
        if (!tr[rt].nx[i])
            tr[rt].nx[i]=rt;
        else {
            tr[tr[rt].nx[i]].fail=rt;
            q[++qt]=tr[rt].nx[i];
        }
    while (qh!=qt) {
        int now=q[++qh];
        rep(i,0,A-1)
            if (!tr[now].nx[i])
                tr[now].nx[i]=tr[tr[now].fail].nx[i];
            else {
                tr[tr[now].nx[i]].fail=tr[tr[now].fail].nx[i];
                q[++qt]=tr[now].nx[i];
            }
    }
}

void Ins(int u,int v) {
    mp[++totG]=G(v,hd[u]); hd[u]=totG;
}
void DFS(int now) {
    siz[now]=1; dfn[now]=++num;
    fore(k,now) {
        dep[mp[k].v]=dep[now]+1; pre[0][mp[k].v]=now;
        DFS(mp[k].v);
        siz[now]+=siz[mp[k].v];
    }
}

int lowbit(int i) {
    return i&-i;
}
void Add(int x,int ad) {
    for (int i=x;i<=tot;i+=lowbit(i))
        trA[i]+=ad;
}
int Query(int x) {
    int sum=0;
    for (int i=x;i>0;i-=lowbit(i))
        sum+=trA[i];
    return sum;
}

int LCA(int x,int y) {
    if (dep[x]<dep[y]) swap(x,y);
    per(i,unit,0)
        if (dep[x]-(1<<i)>=dep[y])
            x=pre[i][x];
    if (x==y) return x;
    per(i,unit,0)
        if (pre[i][x]!=pre[i][y])
            x=pre[i][x],y=pre[i][y];
    return pre[0][x];
}

void Traver(char *p,int len) {
    int now=rt;
    rep(i,1,len) {
        int go=p[i]-'a';
        now=tr[now].nx[go];
        lis[++lis[0]]=now;
    }
}
int cmp(int a,int b) {
    return dfn[a]<dfn[b];
}
void Modify(char *p,int len) {
    lis[0]=0;
    Traver(p,len);
    sort(lis+1,lis+lis[0]+1,cmp);
//  int t;
    lis[0]=unique(lis+1,lis+lis[0]+1)-lis-1;
    rep(i,1,lis[0]) {
        Add(dfn[lis[i]],1);
//      t=Query(dfn[2]+siz[2]-1)-Query(dfn[2]-1);
    }
    rep(i,1,lis[0]-1) {
        int anc=LCA(lis[i],lis[i+1]);
        Add(dfn[anc],-1);
//      t=Query(dfn[2]+siz[2]-1)-Query(dfn[2]-1);
    }
}

int QueryAns(int x) {
    int id=loc[x];
    int rl=Query(dfn[id]-1);
    int rr=Query(dfn[id]+siz[id]-1);
    int t=rr-rl;
    return t;
}

//...

int main(void) {
    #ifndef ONLINE_JUDGE
    freopen("xsy1714.in","r",stdin);
    freopen("xsy1714.out","w",stdout);
    #endif

    n=rd(); rt=tot=1;
    rep(i,1,n) {
        scanf("%s",s+1);
        InsTrie(i,s,strlen(s+1));
    }
    Build();

    rep(i,2,tot)
        Ins(tr[i].fail,i);
    unit=(int)(log(tot)/log(2)); pre[0][rt]=rt;
    DFS(rt);
    rep(i,1,unit) rep(j,1,tot)
        pre[i][j]=pre[i-1][pre[i-1][j]];

    //...
}

分析2

首先,我们考虑对P建立AC自动机。

离线建的功能肯定更强,所以我们离线所有操作,建立P的AC自动机。

考虑询问2,我们这样来描述:求AC自动机上某一个点在Fail树上的子树中被多少个不同的P访问过。所以我们只需要处理出每个P访问了那些点,然后把树的问题用dfn序转化,再用HH的项链那道题的方法解决即可。

【bzoj2434】【xsy1715】阿狸的打字机

题意

粘贴自popoqqq的博客:http://blog.csdn.net/PoPoQQQ/article/details/41518097

初始字串为空,首先给定一系列操作序列,有三种操作:

1.在结尾加一个字符

2.在结尾删除一个字符

3.打印当前字串

然后多次询问第x个打印的字串在第y个打印的字串中出现了几次

分析

首先在线建立Trie树。

处理出Fail指针。

一个问题可以描述为\((x,y)\):

求\(\sum_{在AC自动机上,y的祖先k}[在Fail树上,x为k的祖先]\)

我们可以在Fail上树上倍增,枚举\(y\)的祖先\(k\),判断\(x\)是否为\(k\)的祖先。

现在考虑优化。

注意这是多组询问,我们可以考虑把问题离线下来。

由于我们要枚举\(y\)的祖先,所以考虑把Trie树进行DFS,并用树状数组动态更新单点,查询就只是子树求和了。

用dfn序映射到一条直线上。

小结

Fail树是AC自动机建立之后的附属品,也是建立之后使用AC自动机的必需品。

使用Fail树关键要抓住这样一些性质:

【Fail树的性质】

对于Fail树上的两个节点\((i,j)\),若\(i\)为\(j\)的祖先,那么有:

①到达AC自动机的\(i\)节点对应的字符串 是 到达AC自动机的\(j\)节点对应的字符串 的 子串

②到达AC自动机的\(j\)节点对应的字符串 包含着 到达AC自动机的\(i\)节点对应的字符串

建立AC自动机的对象不同,就要使用不同的性质进行处理。