AC自动机-题解集合

P4052 [JSOI2007]文本生成器

题意:

给定一堆短字符串,再给定 \(m\) 长度的随机字符串(小写字符),求短字符串在随机字符串中出现的次数,对 \(10007\) 取模。

分析:

这种关于字符串出现次数之类的题目,基本上都是 \(AC\) 自动机。


有人说过,\(AC\) 自动机的 \(DP\) 都很套路。

\(dp[i][j]\) 表示当前在节点 \(j\) ,且串长度为 \(i\) 的情况,有时候再加一维表示这个状态里面包含了哪些东西。

而且 \(AC\) 自动机的 \(DP\) 经常用矩阵乘法优化。


我们发现,找不识别的比找识别的好实现。因此进行容斥。

定义 \(dp[i][j]\) 表示当前在 \(j\) 点且串长为 \(i\) 时不经过单词结尾的路径条数,然后从父亲往儿子转移,即:

if(!val[c[j][k]])//不存在单词结尾,就可进行转移
    dp[i][c[j][k]]+=dp[i-1][j];

初始化为 \(f[0][0]=1\).

注意一个单词的后缀是一个可读的单词,那么这个单词一定也是可读的,我们就不能继续往这个单词走了,也就是上面代码的 \(if\) 部分。

最后统计在长度为 \(m\) 的情况下,在节点 \([1,cnt]\) 各有多少不存在的情况,加和即可。

代码

P3311 [SDOI2014] 数数

题意:

给定一些数字串,再给定一个 \(n\) ,求在 \([1,n]\) 的数中不存在这些数字串的数 的个数。

分析:

其实这题就是上一题的增强版,这里有了数位限制,即使用数位 \(dp\) 来计算个数。

当我们转移到数字串的末尾时,是非法的,但是我们并不清楚每个串插入的地方是违法的。

此时,可以有转移:\(val[y]|=val[fail[y]]\)

关于数位 \(dp\) ,还有需要注意的:

  1. 反转匹配数组,
  2. 要注意没有前导零的时候不能转移到 Trie 上的其他节点,因为这个时候我们的串还没开始。

关键部分代码:

int dfs(int now,int pos,int limit,int lead) {//位数,位置,最高位,前导0
	if(now <= 0) return !val[pos];
	if(val[pos]) return 0;//不能访问
	if(f[now][pos][limit][lead]!=-1) return f[now][pos][limit][lead]; //数位dp返回
	int top=limit?(m[now]-'0'):9,res=0;//限制位数,记录答案
	for(int i=0;i<=top;i++){
	(res+=dfs(now-1,
             (lead&&(i==0))?0:c[pos][i]/*我们的串还没开始*/
             (limit&&(i+'0'==m[now])),//限制位数
             (lead&&(i==0))))%mod;//前导0
    }
	return f[now][pos][limit][lead]=res;
}
int main(){

........
    reverse(m+1,m+1+len);//反转数组,因为玄学下标的出现(没有得到解释)
    memset(f,-1,sizeof(f));
    int ans = dfs(len,0,1,1);
    ans=(ans+mod-1)%mod;
}

[TJOI2013]单词

题意:

一堆字符串,求每个字符串在这堆串中出现了多少次。

分析:

这里涉及到了 \(Fail\) 指针的定义:指向树上该字符串最长后缀的末尾。

也就是当跑完一个字符串时,我们会到其他相同字符串的末尾,这样就增加了一次答案。

这样,我们就有思路:每一个节点记录属于多少个字符串,为他的权值。

一个节点表示的字符串表示在整个字典中出现的次数 \(=\)\(Fail\) 树中的子树的权值的和。

代码:

void add(int x){
    ......
    for(int i=1;i<=len;i++){
        int u=s[i]-'a';
        if(!c[now][u]) c[now][u]=++cnt;
        now=c[now][u];
        val[now]++;//点权值
    }
    a[x]=now;//记录末尾
}
void build(){
    int head=0,tail=0;
    for(int i=0;i<26;i++) if(ch[0][i]) q[++tail]=ch[0][i];
    while(head<tail){
        int x=q[++head];
        for(int i=0;i<26;i++){ 
            if(ch[x][i]){
                q[++tail]=ch[x][i];
                fail[ch[x][i]]=ch[fail[x]][i];
            }
            else ch[x][i]=ch[fail[x]][i];
        }
    }
}
void solve(){
    for(int i=cnt;i>=0;i--) val[fail[q[i]]]+=val[q[i]];
    for(int i=1;i<=n;i++) printf("%d\n",val[a[i]]);
}

[COCI2015]Divljak

题目大意:

一开始 \(n\) 个字符串集合,有 \(Q\) 个操作:

  1. 集合中插入一个新串
  2. 询问给定字符串是多少个集合中的字符串的子串。

思路:

肯定是先建立 \(AC\) 自动机,记录每个字符串的结束位置。

\((fail[x],x)\) 建树,然后将这棵树求出每个点的 \(dfs\) 序。

利用树上差分思想,把要插入的 \(T\) 集合在 \(trie\) 树中统计走过的节点,记录一下。

再按 \(dfn\) 排序,将相邻两个节点的在树上的位置 \(+1\) ,表示多一个串匹配。

但是他们的 \(lca\)\(lca\) 的祖先很明显是 \(+2\) 串匹配,不符合此串在集合中的出现次数,因此 \(lca\) 位置标记 \(-1\).

对于此过程,运用树状数组统计答案,树链剖分后,按每个节点的 \(dfs\) 序维护,同时也可以跑 \(lca\) .

出现次数就是 其节点和其子树的和

#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&-x
const int N=2e6+5,M=1e5+5;
int c[N][26],val[N],fail[N],cnt;
int nxt[N],ver[N],head[N],tot;
int fa[N],dfn[N],dep[N],sizes[N],son[N],top[N],rk[N],dfstime;

int n,Q,a[N],treesum[N]; char str[N]; 

void add(int x,int y){
    ver[++tot]=y; nxt[tot]=head[x]; head[x]=tot;
}
void ins(char *s,int id){
    int len=strlen(s);int now=0;
    for(int i=0;i<len;i++){
        int x=s[i]-'a';
        if(!c[now][x]) c[now][x]=++cnt;
        now=c[now][x];//指向地址
    }
    val[id]=now;//单词末尾
}
void build(){
    queue<int> q;
    for(int i=0;i<26;i++)//第一层是根节点0
        if(c[0][i])//遍历第二层,搜索存在的子树
            fail[c[0][i]]=0,q.push(c[0][i]);//fail指向根节点
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=0;i<26;i++){//搜索这一棵树
            if(c[x][i]){
                fail[c[x][i]]=c[fail[x]][i];//让这个节点的失败指针指向(((他父亲节点)的失败指针所指向的那个节点)的下一个节点)
                q.push(c[x][i]);//存在子树,就压入队列
            }
            else
                c[x][i]=c[fail[x]][i];//否则就让这个子节点指向当前节点fail指针的子节点 
        } 
    }
}
void dfs1(int x,int father){
    sizes[x]=1; 
    fa[x]=father;
    dep[x]=dep[father]+1;
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i]; if(y==father) continue;
        // fa[y]=x;
        dfs1(y,x);
        sizes[x]+=sizes[y];
        if(!son[x]||sizes[y]>sizes[son[x]]) son[x]=y;
    }
}
void dfs2(int x,int topfather){
    dfn[x]=++dfstime;//dfs序
    top[x]=topfather;//这个点所在重链的顶端,对于求lca和链有极大帮助
    if(!son[x]) return;
    dfs2(son[x],topfather);//我们首先进入重儿子来保证一条重链上各个节点dfs序连续
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i];
        if(y!=son[x]&&y!=fa[x]) dfs2(y,y);//位于轻链底端,top为本身
    }
}
int lca(int x,int y){
    while(top[x]!=top[y]){
        if(dep[top[x]]<dep[top[y]]) swap(x,y);
        x=fa[top[x]];
    }
    if(dep[x]>dep[y]) swap(x,y);
    return x;
}

void update(int x,int val){
    for(;x<=dfstime;x+=lowbit(x)) treesum[x]+=val;
}
int query(int x){
    int res=0;
    for(;x;x-=lowbit(x)) res+=treesum[x]; return res;
}

bool cmp(int x,int y){return dfn[x]<dfn[y];}

inline void solve1(){
    int x=0,tp=0;
    for(int i=0;str[i];i++){
        x=c[x][str[i]-'a']; a[++tp]=x;//在 trie 树中统计走过的节点
    }
    sort(a+1,a+1+tp,cmp);
    bool flag=false;
    for(int i=1;i<=tp;i++){
        update(dfn[a[i]],1);//相邻两个节点在树上的位置 +1 ,表示多一个串匹配
        if(flag) update(dfn[lca(a[i],a[i-1])],-1);
        else flag=true;
    }
}

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) scanf("%s",str),ins(str,i);
    build();
    // for(int i=1;i<=cnt;i++) cout<<fail[i]<<" "<<i<<endl;
    for(int i=1;i<=cnt;i++) add(fail[i],i);
    
    dfs1(0,cnt+1); dfs2(0,0);
    // cout<<dfstime<<endl;
    // for(int i=0;i<=cnt;i++) cout<<dfn[i]<<" "; cout<<endl;
    cin>>Q;
    while(Q--){ int opt;
        scanf("%d",&opt);
        if(opt==1) scanf("%s",str),solve1();
        else if(opt==2){ 
            int x; scanf("%d",&x);
            printf("%d\n",query(dfn[val[x]]+sizes[val[x]]-1)-query(dfn[val[x]]-1));
        }       
    }
    system("pause");
    return 0;
}

posted @ 2021-09-23 21:49  Evitagen  阅读(71)  评论(0编辑  收藏  举报