昨天开始搞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++;
}
}