给定 个模式串 和一个文本串 ,求出 中有多少个 出现了。
考虑先将所有模式串插入到一棵 Trie 树中,然后在上面跑(不停从点 走到 的边权为 的儿子,即令 变为 )。这样若当前点 为某个 对应的点(可以在插入时给这个点打标记),则说明 在 中出现了。
但是这样不停跑可能会出现这样的情况:当前位于点 ,而 即不存在对应的儿子,此时我们无法直接走到 。不妨称这种情况为“失配”。
解决办法很简单,假设第一次出现这种情况时跑完了 ,当前在点 且 。则此时 对应的字符串是 。为了能继续往下跑,我们可以跳到一个 Trie 树上的节点 ,使得 代表的字符串是 的后缀,且 :

如果这样的 有很多个,那么跳到最深(代表的字符串最长)的那个一定是最优的(因为从它出发能跳到其它合法的 )。并且由于 Trie 中每个点的字符串两两不同,故最深的 一定只有一个。
不妨令 为最深的这样的 ,即对应字符串是点 对应字符串的后缀且深度最大的点 (需满足 )。
考虑 的求解。注意到 的深度一定小于 的深度,故可以使用 bfs 求解。先将 以及所有 的儿子的 设为 。对于一个点 及 :
- 令 并不断令 ,直到:
- ,则令 ;
- ,则令 ;
暴力跳太慢了,不妨在 时令 ,这样就不用暴力跳了:
- 若 ,则令 ;
即 实际上变成了:
- 从 出发,不断令 直到边权为 的儿子 存在, 就为这个 ;
求 数组的代码如下:
inline void build()
{
queue<int> q;
fail[0]=0;
for(int i=0;i<26;i++)
{
int v=son[0][i];
if(v!=0) fail[v]=0,q.push(v);
}
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
{
int v=son[u][i];
if(v!=0) fail[v]=son[fail[u]][i],q.push(v);
else son[u][i]=son[fail[u]][i];
}
}
}
对于例题,不难发现当在点 时,实际上能匹配 对应的字符串,即不断跳 经过的点都能匹配到。故还需记录一个点是否被跳过了,如果经过跳过的点就停止跳 :
inline int calc(char* s,int n) // sum[u] 表示有多少个文本串等于 u 表示的字符串
{
int u=0,res=0;
for(int i=1;i<=n;i++)
{
int id=s[i]-'a';
u=son[u][id];
int v=u;
while(v!=0&&sum[v]!=-1) // 跳到根节点或者跳到过就不用跳
{
res+=sum[v]; // 计算贡献
sum[v]=-1; // 标记跳到过了
v=fail[v]; // 继续跳 fail
}
}
return res;
}
实际上,由于每个点只有一个 ,且不断跳 必定停留在点 ,故 实际上是一棵根为 ,包含所有 Trie 中节点的树,即 fail 树。
练习: