最近在学习字符串算法,学到KMP和AC自动机总有种似懂非懂的感觉,这个题的思路也是看的STD
KMP
个人感觉KMP的精髓就是失配函数,在当前匹配失败的情况下不必重头推倒再来,而是从特定的位置。i位置的失配函数为f[i],可表示i位置失配后去f[i]位置找,是因为f[i]位置之前的模板串与i位置前的模板串的等长后缀是相等的,这样做下去当发现整个模板串都遍历完成,就是找到了模板串作为目标串子串的结束位置)
AC自动机
AC自动机是用来处理多个模板串的,所以就用到了Trie树。个人认为AC自动机是Trie树和KMP结合,或者说是特殊版的Trie树。当用目标串在Trie树中查找时,如果走到当前节点A后,发现没有目标串下一个字符a的节点,就沿着失配边一直跳,直到到根或者到有子节点a的节点B,B就是需要重新开始的起点
不必向上找失配边的操作
算法竞赛入门经典这本书上写了两种做法,这个题需要用第二种,就是getfail时,枚举节点A所有可能的下一个节点B,如果A走不到B,那么不是将B跳过,因为这样的话查找的时候从A找不到节点B还要跳失配边直到根或者到有子节点B的节点。
所以搞一个类似于并茶几路径压缩的操作,即A走不到B,就把SON【A】【B】设为A沿着失配边向上的点的对应B节点的位置,就相当于自动跳了一次失配边
这题做法不写了,算法竞赛入门经典(蓝书)P218
#include<iostream> #include<cstdio> #include<cstring> #include<queue> #define fr(i,s,t) for (i=s;i<=t;i++) #define Clear(a) memset(a,0,sizeof(a)) using namespace std; int K,N,L,Size,son[20000][70],f[2000]; char S[200]; double pro[200],ans,dp[20000][200]; bool v[2000],vis[20000][200]; int id(char c){ if (c>='A'&&c<='Z') return c-'A'; if (c>='a'&&c<='z') return 26+c-'a'; return 52+c-'0'; } void Insert(){ int rt=0,d,i; for (i=0;i<strlen(S);i++){ d=id(S[i]); if (!son[rt][d]) son[rt][d]=++Size; rt=son[rt][d]; } v[rt]=1; } void Getfail(){ queue <int> Q; int i,u,y,x; for (i=0;i<63;i++){ u=son[0][i]; if (u) Q.push(u); } while (!Q.empty()){ x=Q.front(); Q.pop(); for (i=0;i<63;i++){ u=son[x][i]; if (!u){son[x][i]=son[f[x]][i];continue;} Q.push(u); y=f[x]; while (y&&!son[y][i]) y=f[y]; f[u]=son[y][i]; v[u]|=v[f[u]]; } } } double Dfs(int x,int L){ if (!L) return 1.0; if (vis[x][L]) return dp[x][L]; vis[x][L]=1; double &res=dp[x][L]; res=0; for (int i=0;i<63;i++) if (!v[son[x][i]]&&pro[i]) res+=pro[i]*Dfs(son[x][i],L-1); return res; } void Work(int Num){ Clear(pro); Clear(dp); ans=0; Clear(son); Size=0; Clear(vis); Clear(f); Clear(v); scanf("%d",&K); int i; fr(i,1,K) cin>>S,Insert(); scanf("%d",&N); fr(i,1,N){ char ch; cin>>ch; scanf("%lf",&pro[id(ch)]); } Getfail(); scanf("%d",&L); ans=Dfs(0,L); printf("Case #%d: %0.6lf\n",Num,ans); } int main(){ int T,i; scanf("%d",&T); fr(i,1,T) Work(i); return 0; }
。