AC自动机之初学习

昨天开始搞ac自动机,其实搞完kmp和trie树之后搞ac自动机还是很简单的,主要是形成一套ac自动机的代码风格比较艰难,找了份简洁明了实用性强的代码分析了一天,以后就按这个风格写了。

hdu2896

模板题,借此把我加注释的模板贴出来~

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>

using namespace std;

int next[100001][128];  //trie树
int fail[100001];   //失败指针,相当于kmp的next数组
int mark[100001];   //标记数组,记录节点信息
bool used[501];
int ans;
int root,sum;
int n,m;

int newnode()   //产生新的节点
{
    for (int i=0;i<128;i++)
        next[sum][i]=-1;
    mark[sum++]=-1;
    return sum-1;
}

void init()     //初始化
{
    sum=0;
    ans=0;
    root=newnode();
}

void insert(char * s,int id)    //构造trie树
{
    int len=strlen(s);
    int now=root;
    for (int i=0;i<len;i++)
    {
        if (next[now][s[i]]==-1)
            next[now][s[i]]=newnode();
        now=next[now][s[i]];
    }
    mark[now]=id;
}

void build()    //构造fail指针
{
    queue <int> mq;
    fail[root]=root;
    for (int i=0;i<128;i++)
    {
        if (next[root][i]==-1)
            next[root][i]=root;     //如果根节点没有这个孩子,那么直接将next设为根节点,即假设匹配这个字符之后来到了根节点
                                    //其实是将这个字符视为不能匹配的字符
        else
        {
            fail[next[root][i]]=root;   //否则,这个点的fail指针指向根节点,即如果这个节点的孩子匹配失败,回到根节点重新匹配
            mq.push(next[root][i]);     //这个节点形成的树非空,入队
        }
    }
    while (!mq.empty())
    {
        int now=mq.front();
        mq.pop();
        for (int i=0;i<128;i++)
        {
            if (next[now][i]==-1)   //如果当前节点的孩子节点不存在,将这个节点的孩子节点视为这个节点匹配失败后来到的位置的相同
                                    //孩子节点,如果它的fail节点也没有这个孩子,会形成一条回到根节点的链
                next[now][i]=next[fail[now]][i];
            else        //否则,将这个孩子节点的fail节点设为now的的fail节点的相同孩子节点位置
            {
                fail[next[now][i]]=next[fail[now]][i];
                mq.push(next[now][i]);
            }
        }
    }
    /*
    这样形成next构成一个闭合的图,对每个节点的所有孩子节点都能出发,要么走到其真正孩子节点,要么其没有这个孩子,相当
    于失配,走到失配后来到的位置,如果走到根节点还失配,则形成循环,看着是一条链,其实由于每次都是记录的之前的信息,记录
    之后寻找下一个位置的花费是1,可以画出这个bfs递推过程体会一下
    这样形成的fail就是正常的fail指针了,即当前节点匹配下一节点失败时要去的节点,由于这里面跟next联系到一起,他们相互作用,使得
    任意一次查找的花费都是1,画图体会下一条链是怎样形成的是最好的理解方式
    */
}

void query(char * s,int id)
{
    int len=strlen(s);
    int now=root;
    bool flag=false;
    memset(used,false,sizeof(used));
    for (int i=0;i<len;i++)
    {
        now=next[now][s[i]];
        int temp=now;
        while (temp!=root)
        {
            if (mark[temp]!=-1)
            {
                used[mark[temp]]=true;
                flag=true;
            }
            temp=fail[temp];
        }
    }
    if (!flag)
        return ;
    ans++;
    printf("web %d:",id);
    for (int i=1;i<=n;i++)
        if (used[i])
            printf(" %d",i);
    printf("\n");
}

char ch[10005];

int main()
{
    while (~scanf("%d",&n))
    {
        init();
        for (int i=1;i<=n;i++)
        {
            scanf("%s",ch);
            insert(ch,i);
        }
        scanf("%d",&m);
        build();
        for (int i=1;i<=m;i++)
        {
            scanf("%s",ch);
            query(ch,i);
        }
        printf("total: %d\n",ans);
    }
}

hdu3689

好吧,其实学ac自动机是为了做这个题目,结果被折磨了整整八个小时才看懂,网上各种说水题简单题,可是不知道那些概率的递推是如何推出来和证明的根本看不到,感谢matrix67的文章 http://www.matrix67.com/blog/archives/366 ,让我最后算是看明白了这个概率究竟是怎样一步步推出来的,以及为什么两份代码一份加一份减却能输出相同的结果了。附两份代码,一份是kmp版的方法,一份是ac自动机的方法,关键代码都已注释,具体自己看代码。

KMP版:求出所有不满足题意的,用1去减便是结果

#include <iostream>
#include <cstring>
#include <cstdio>

using namespace std;

int next[15];
char ch[15];
int n,m;
double p[26];
double dp[1010][15];    //当前走了i步,匹配长度为j
int f[1010][26];

void getnext()
{
    memset(f,0,sizeof(f));
    int len=strlen(ch+1);
    for (int i=2;i<=len;i++)
    {
        int j=next[i-1];
        while (j>0&&ch[j+1]!=ch[i])
            j=next[j];
        if (ch[j+1]==ch[i])
            j++;
        next[i]=j;
    }
    for (int i=0;i<=len;i++)
    {
        for (int t=0;t<26;t++)
        {
            if (p[t]>0)
            {
                int j=i;
                while (j>0&&ch[j+1]-'a'!=t)
                    j=next[j];
                if (ch[j+1]-'a'==t)
                    j++;
                f[i][t]=j;
            }
        }
    }
}

void DP()
{
    memset(dp,0,sizeof(dp));
    dp[0][0]=1;             //第0步匹配成功0个的概率为1
    int len=strlen(ch+1);
    for (int i=0;i<m;i++)   //枚举第几步
    {
        for (int t=0;t<len;t++)     //枚举匹配成功的长度,长度为len的不能继续递推,递推会造成重复计数
        {
            for (int k=0;k<26;k++)      //枚举下一步走向k字母
            {
                if (p[k]>0)     //f[t][k]代表下一步走向k时来到的新的匹配长度
                {
                    dp[i+1][f[t][k]]+=dp[i][t]*p[k];
                }
            }
        }
    }
    double ans=0;
    //走到m步仍然没有匹配成功的结果加起来便是不能匹配成功的概率,所有能匹配成功的结果
    //都没继续递推,所以走到m的时候没有匹配成功便代表一直没有匹配成功
    for (int i=0;i<len;i++)
        ans+=dp[m][i];
    printf("%.2f%%\n",100-ans*100);     //由于这一列的结果相加肯定为1(要么匹配成功过,要么没有),所以减去没有匹配成功的便是答案
}

int main()
{
    while (cin>>n>>m&&n+m)
    {
        memset(p,0,sizeof(p));
        memset(next,0,sizeof(next));
        for (int i=1;i<=n;i++)
        {
            char a;
            double b;
            cin>>a>>b;
            p[a-'a']=b;
        }
        scanf("%s",ch+1);
        next[0]=next[1]=0;
        getnext();
        DP();
    }
}

ac自动机版:求出所有满足题意的结果,相加
#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>

using namespace std;

int next[1010][26];
int fail[1010];
int sum,root;
int n,m;
double dp[1010][15];    //当前步数为i,匹配的节点编号为j(其实根节点是0,其余节点依次类推,自动机只有一条链,故也能理解为匹配长度)
double p[26];
char ch[15];

int newnode()
{
    for (int i=0;i<26;i++)
        next[sum][i]=-1;
    sum++;
    return sum-1;
}

void init()
{
    sum=0;
    root=newnode();
}

void insert(char * s)
{
    int now=root;
    int len=strlen(s);
    for (int i=0;i<len;i++)
    {
        int id=s[i]-'a';
        if (next[now][id]==-1)
            next[now][id]=newnode();
        now=next[now][id];
    }
}

void build()
{
    queue <int> mq;
    fail[root]=0;
    for (int i=0;i<26;i++)
    {
        if (next[root][i]==-1)
            next[root][i]=root;     
        else
        {
            fail[next[root][i]]=root;
            mq.push(next[root][i]);
        }
    }
    while (!mq.empty())
    {
        int now=mq.front();
        mq.pop();
        for (int i=0;i<26;i++)
        {
            if (next[now][i]==-1)
                next[now][i]=next[fail[now]][i];
            else
            {
                fail[next[now][i]]=next[fail[now]][i];
                mq.push(next[now][i]);
            }
        }
    }
}

int main()
{
    while (cin>>n>>m&&n+m)
    {
        memset(dp,0,sizeof(dp));
        memset(p,0,sizeof(p));
        memset(next,0,sizeof(next));
        memset(fail,0,sizeof(fail));
        for (int i=1;i<=n;i++)
        {
            char a;
            double b;
            cin>>a>>b;
            int id=a-'a';
            p[id]=b;
        }
        init();
        scanf("%s",ch);
        insert(ch);
        build();
        int len=strlen(ch);
        dp[0][0]=1;     //第0步走到根节点的概率为1
        for (int i=0;i<m;i++)
        {
            for (int t=0;t<26;t++)
            {
                for (int k=0;k<sum-1;k++)   //一共sum-1个节点,一旦推理到sum-1即发现了一个可行串,便不能向下推理了(防止重复)
                {
                    int v=next[k][t];   //从k节点出发走向t节点
                    dp[i+1][v]+=dp[i][k]*p[t];  //将这种情况形成的概率加到下一步中
                }
            }
        }
        double ans=0;
        for(int i=0; i<=m; i++)     //在每步形成可行串的概率加起来便是总的形成可行串的概率
            ans+=dp[i][sum-1];
        printf("%.2f%%\n",ans*100);
    }
}

hdu2457

又是一道ac自动机加dp的题目,花了一晚上时间才写出来,还是不熟悉用ac自动机完成树形dp。。。

这道题让求改变某些点使得不含病毒串的最少改变步数,那么对目标串进行dp,对于目标串上的每个节点都有两种选择,要么改变要么不改变,然后,对每一步,枚举所有节点,从这个节点出发到其子节点作为待更新节点,看从这个节点出发能否使得下一步到待更新节点代表的状态更优,如果待更新节点和当前步数匹配的节点相同,则表示不改变这个节点,不同则表示改变这个节点花费加1,然后看步数为len时在哪个状态花费最小,输出即可。

附代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <map>
#include <cstdlib>

using namespace std;

int next[1010][4];
int fail[1010];
int mark[1010];
int sum,root;
map <char,int> mp;
int n,m;
char ch[1010];
int dp[1010][1010];     //当前匹配的长度为i,处于t状态,注意只需要匹配长度为len即匹配完成,由于值能变,所以在哪个位置不重要
const int INF=0x3f3f3f3f;

int newnode()
{
    for (int i=0;i<4;i++)
        next[sum][i]=-1;
    mark[sum++]=-1;
    return sum-1;
}

void init()
{
    sum=0;
    root=newnode();
}

void insert(char * s)
{
    int now=root;
    int len=strlen(s);
    for (int i=0;i<len;i++)
    {
        int id=mp[s[i]];
        if (next[now][id]==-1)
            next[now][id]=newnode();
        now=next[now][id];
    }
    mark[now]=1;
}

void build()
{
    queue <int> mq;
    fail[root]=-1;
    for (int i=0;i<4;i++)
    {
        if (next[root][i]==-1)
            next[root][i]=root;
        else
        {
            fail[next[root][i]]=root;
            mq.push(next[root][i]);
        }
    }
    while (!mq.empty())
    {
        int now=mq.front();
        mq.pop();
        for (int i=0;i<4;i++)
        {
            if (next[now][i]==-1)
                next[now][i]=next[fail[now]][i];
            else
            {
                fail[next[now][i]]=next[fail[now]][i];
                mq.push(next[now][i]);
            }
        }
    }
}

int min(int a,int b)
{
    return a>b?b:a;
}

void solve(char * s,int ca)
{
    int len=strlen(s);
    for (int i=0;i<=1000;i++)   //初始化所有节点花费都为无穷大
        for (int t=0;t<sum;t++)
            dp[i][t]=INF;
    dp[0][0]=0;     //当前匹配长度为0,在状态0,即根节点。
    int now=root;
    for (int i=0;i<len;i++)     //只需要匹配到长度为len即可,由len-1推出len
    {
        int id=mp[s[i]];
        for (int t=0;t<4;t++)       //当前待更新状态为next{k}[t]
        {
            for (int k=0;k<sum;k++)     //通过k状态推过来,注意对于自动机来说,每个节点便是一个状态
            {
                int temp=next[k][t];
                while (temp!=-1)        //遍历当前串所有后缀
                {
                    if (mark[temp]==1)  //如果为病毒串,跳出
                        break;
                    temp=fail[temp];
                }
                if (temp!=-1)       //证明当前串的某个后缀为病毒串,不可用
                    continue ;
                else
                {
                    int p=0;
                    if (t!=id)      //如果当前串的末尾和待匹配位置的值相同,则代表不改变,否则代表改变,花费+1
                        p=1;
                    dp[i+1][next[k][t]]=min(dp[i+1][next[k][t]],dp[i][k]+p);
                }
            }
        }
    }
    int ans=INF;
    for (int i=0;i<sum;i++)
        ans=min(ans,dp[len][i]);
    if (ans>=INF)
        ans=-1;
    printf("Case %d: %d\n",ca,ans);
}

int main()
{
    mp['A']=0;
    mp['G']=1;
    mp['C']=2;
    mp['T']=3;
    int ca=1;
    while (cin>>n&&n)
    {
        init();
        for (int i=0;i<n;i++)
        {
            scanf("%s",ch);
            insert(ch);
        }
        scanf("%s",ch);
        build();
        solve(ch,ca);
        ca++;
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值