字符串匹配(kmp+trie+aho-corasic automaton+fail tree)
kmp
对于一个字符串,称 为它的前缀, 称为它的后缀
例如字符串的前缀有, 后缀有
如果前缀后缀之中有相同的……在匹配中可以起到出其不意的效果。
例如对于模式串, 文本串
设匹配进行到文本串的第位,模式串的第位,即模式串第位之前已经匹配成功(开头是第1位开始算的话)
然后我们就发现程序很顺利进行到并且匹配成功,这个时候,
然后就发现按照正常暴力思路就是回到重新开始暴力
但是我们其实是可以发现已经成功匹配的模式子串有相同的前缀后缀,即,且(这样的话前缀后缀应该有四位才对)
因为成功匹配,所以会有, 所以我们就可以知道,
这就是说在模式串的前三位我们又快速匹配成功了,根据上一次失败的匹配的结果。这里我们就可得到现在开始匹配
这里的3是根据已经匹配的模式子串的最长前缀后缀长度为3得到的
那么怎么快速求最长前缀后缀呢
对于字符串,注意这次从1开始(当然从0开始也没问题),我们要匹配,就要求出所有前缀 的最长前缀后缀长度。
也就是要求出所有的的最长前缀后缀的长度。
我们就设数组表示的最长前缀后缀的长度,很容易得到(明显犯规了嘛), (就一个字母哪来前缀后缀)
接下来就可以递推了
对于已经求得的,他表示, 那么如果,就会有,这就是一个相同的前缀后缀且一定是最长的(因为表示的也是最长的),
但是如果,是不是直接判呢。
我们也可以考虑看一下的最长相同前缀后缀。因为我们是一路推过来的,而且根据定义必定有前缀后缀的长度小于字符串的长度,即, 那么我们就可以知道的最长前缀后缀一定是已经求出来的,为,他表示,同时又因为,所以我们可以得到,
,也就是说长度为的子串也是模式子串的相同前缀后缀(只不过不是最长而已)
同理可得,均为的相同前缀后缀的长度,我们就可以一一看看这样的长度的相同前缀后缀能不能多接一个。边界就是0的时候,空串是不会有前缀后缀的了……
写成代码就是这样
for (int i=2;i<=lb;i++)
{
int j = kmp[i-1];
while(j&&b[i]!=b[j+1]) j=kmp[j];
if(b[j+1]==b[i])kmp[i]=j+1;
else kmp[i]=0;
}
trie
类似于字典的构造,建立一棵树,每个节点表示一个字母,层深度表示串的长度,把有相同前缀的串放在同一个子树内。可以快速完成多个文本串中匹配一个模式串的任务。
比如这棵trie,打了红色标记就表示有一个串到尾了。所以这棵trie存储了文本串
对于有相同前缀的串,都放在了字数内,那么如果我们找的模式串前缀也是,那么只需要在子树内寻找就可以了。
aho-corasic automaton
trie上kmp,一个文本串中匹配多个模式串
众所周知kmp是因为有回溯指针才能快速匹配,那么我们把多个模式串建立一个trie,然后用文本串一一匹配,如果找到标记了结束的红点就说明找到了一个模式串。
那么其实我们也可以建立一个回溯指针的。如果匹配失败就看看回溯指针那边能否匹配成功。
在AC自动机里面也叫做失配指针。
这里就是搞定之后的失配指针
对于建立了的字典树,第一层(也就是第一个字母)的失配指针全部指向根节点(就一个字母你指什么指了也是自己还是失配)并且扔入队列(对就用BFS)
然后每次就从队头取出元素,我们可以叫它, 如果的存在,那么我们就可以去寻找这个的失配指针。这里先说如果不存在,那么可以加一个小优化,把指向的失配指针的, 这样就可以直接连到可能匹配到的模式串。但是这样会破坏字典树的结构,在某些题目用了会WA……
这里失配指针也模仿kmp的的求法,如果的失配指针有,那么的的失配指针就指向 如果还是没有就看看有没有……因为失配指针指向的不是根节点就是同一个字母,保证指向的节点所表示的串会等于当前节点的某个后缀。
匹配的时候,就用文本串从根节点开始匹配,匹配成功文本串指针后移,并且看看下一个字母能不能匹配。如果不能,那就跳去失败指针…………如果最后到了根节点还是没有这个字母,那么……没办法,文本串指针还是要后移一位。
每匹配到一位,我们就检查所有的失配指针指向的节点,,如果有标志是某个模式串的末尾,那么就说明, 啊,我匹配到了!
但是与此同时也要把这个标志取消,以免以后重复访问到,重复计算。这样也可以标记访问过,下次不再访问,节省时间。
代码如下:(题目)
#include <cstdio>
#include <cstring>
#include <queue>
namespace Aho_Corasic_automaton
{
class A_C_maton
{
private:
public:
A_C_maton();
~A_C_maton();
int query(const char*);
void set_fail();
const A_C_maton* operator [](const int k) const
{ if(k < 0 or k >= 26) return nxt[26]; return nxt[k]; }
void add(const char*);
protected:
A_C_maton *fail, *nxt[27];
int ed;
};
A_C_maton::A_C_maton()
{
fail = NULL; ed = 0;
for(register int i = 0; i < 27; ++i) nxt[i] = NULL;
}
A_C_maton::~A_C_maton()
{
for(register int i = 0; i < 27; ++i) if(nxt[i]) delete nxt[i];
delete fail;
}
void A_C_maton::add(const char* str)
{
const register int len = std::strlen(str);
A_C_maton* p = this;
for(register int i = 0; i < len; ++i)
{
if(p -> nxt[str[i] - 'a'] == NULL) p -> nxt[str[i] - 'a'] = new A_C_maton;
p = p -> nxt[str[i] - 'a'];
}
++(p -> ed);
}
void A_C_maton::set_fail()
{
this -> fail = this;
std::queue <A_C_maton*> q;
for(register int i = 0; i < 27; ++i)
{
if(this -> nxt[i])
{
this -> nxt[i] -> fail = this;
q.push(this -> nxt[i]);
}else this -> nxt[i] = this;
}
while(not q.empty())
{
A_C_maton* p = q.front(); q.pop();
for(register int i = 0; i < 26; ++i)
{
if(p -> nxt[i])
{
q.push(p -> nxt[i]);
if(p -> fail -> nxt[i]) p -> nxt[i] -> fail = p -> fail -> nxt[i];
}else p -> nxt[i] = p -> fail -> nxt[i];
}
}
}
int A_C_maton::query(const char* str)
{
int ans = 0;
set_fail();
A_C_maton* p = this;
const register int len = std::strlen(str);
for(register int i = 0; i < len; ++i)
{
p = p -> nxt[str[i] - 'a'];
for(register A_C_maton* r = p; r and r -> ed != -1; r = r -> fail)
{
ans += r -> ed;
r -> ed = -1;
}
}
return ans;
}
}
using namespace Aho_Corasic_automaton;
A_C_maton* root = new A_C_maton;
int n;
char str[1000001];
int main()
{
scanf("%d", &n);
while(n--)
{
scanf("%s", str);
root -> add(str);
}
scanf("%s", str);
printf("%d\n", root -> query(str));
return 0;
}
fail tree
Aho-Corasic automaton求的是出现的模式串的个数
如果要求模式串出现的次数呢?
那么其实时间复杂度就有点高了。而且对于这样的模式串,不断跳失配指针,是很慢的。
那么怎么办呢。
我们回忆一下Aho-Corasic automaton的工作过程。
首先在Trie树里跳文本串,每跳到一个节点就沿着失配指针遍历下去。如果遍历到某个模式串的末尾,那么就说明这个模式串在文本串中出现了一次,可以打上一个标记。那么正常的Aho-Corasic automaton 就是跳完文本串之后,统计每个模式串的结尾的标记个数,就是这个模式串在文本串中出现的次数。
但是这样即使我们提前存储下每个模式串结尾在Trie中的位置,还是会很慢,因为我们不仅要跳很多个失配指针,而且还可能多次跳过一个节点(因为模式串的出现次数肯定很多orz)
我们就可以考虑一下怎么优化。
我们可以发现,一个节点被打上标记,当且仅当它被文本串直接访问或者通过跳失配指针访问。
所以对于每个被标记的节点,我们都可以沿着网线失配指针爬回去,一定可以访问到某个被文本串直接访问过的节点。
那么如果我们根据失配指针反向建有向图,一个模式串的结尾能走到多少个标记,说明有多少次文本串可以直接访问、或通过失配指针访问到这个模式串。
也就是说,我们只要让文本串直接访问到的节点打上标记,跳完之后就统计每个模式串的结尾节点,通过失配指针反向建的有向边来询问节点,统计标记的个数,就是这个模式串出现的次数。这样每个失配指针只跳了一次,文本串也只跳了一次Trie,复杂度大大降低
但是这里要注意建fail的时候不能用优化。这个优化其实就是改变了Trie的结构,同样也会改变失配指针,使其不满足一棵树的结构,就不能正常计算。
这里就告诉你,为什么根据失配指针反向建图是树的结构。
每个节点只有一个失配指针,所以出度是1。反向建图,每个节点的入度是1,也就是只有一个父亲。
那不就是树吗!
而且由于我们通过失配指针最后访问到的都是根,所以反向建图出来的树的根节点就是原来Trie的根。
所以对于每个模式串的末尾,其实就是统计以这个末尾节点为根的子树里面有多少个标记。
然后我们再想想标记是怎么来的。每次文本串遍历到节点才加上一个标记。
那么我们可以假设所有节点的初值都是0,然后每次文本串访问到一个节点就加上1(单点修改),最后就是查找这个子树里面所有节点的和(区间查询)。
单点修改+区间查询 = 树状数组
但是这是一棵树,怎么换成一段数组呢?
我们可以用前缀表达式,对于为根的子树, 大小为, 用前缀表达式dfs出dfs序dfn,那么就可以保证到全部都是这棵子树的上节点的下标,并且这棵子树上节点全部都在这个序列里。
所以我们通过来统计子树上标记的个数
看有注释的代码:(题目)
这道题,所谓的文章就是所有单词组合在一起,但是每个单词之间要插入一个分隔符。文本串跳到了分隔符就要跳回根节点重新跳。
#include<bits/stdc++.h>
using namespace std;
template <typename T>
struct Edge
{
Edge *nxt;
T* t;
}; /// 反向建图用链式前向星
struct Trie
{
Trie* nxt[27], *fail;
Edge <Trie> *edge;
int dfn, sz;
Trie* operator [] (int k) const
{
if(k < 0 or k > 26) return nxt[0];
return nxt[k];
}
Trie()
{
for(register int i = 0; i < 27; ++ i) nxt[i] = NULL;
fail = NULL; edge = NULL;
dfn = 0; sz = 1;
}
}*root;
Trie* add(Trie* p, char * str)
{
/// 把str加入到Trie中
for(register int i = 0; str[i]; ++i)
{
if(p -> nxt[str[i] - 'a' + 1] == NULL) p -> nxt[str[i] - 'a' + 1] = new Trie;
p = (*p)[str[i] - 'a' + 1];
}
return p;
}
int n;
char t[1000201];
Trie* ed[210];
void set_fail(Trie* p)
{
/// 建立失配指针
p -> fail = p;
std::queue <Trie*> q;
for(register int i = 0; i < 27; ++i)
{
if(p -> nxt[i])
{
p -> nxt[i] -> fail = p;
q.push(p -> nxt[i]);
}else p -> nxt[i] = p;
// 第一层字母如果就失配就要跳回根节点
}
while(not q.empty())
{
register Trie* j = q.front(); q.pop();
for(register int i = 0; i < 27; ++i)
{
if(j -> nxt[i])
{
q.push(j -> nxt[i]);
Trie* k = j -> fail;
while(k and k != p and k -> fail and k -> nxt[i] == NULL) k = k -> fail;
j -> nxt[i] -> fail = k -> nxt[i];
}/// else p -> nxt[i] = p -> fail -> nxt[i];
/// 这个优化不能要。如果没有这个节点就没有,优化了就破坏了结构,会影响下面建图
}
}
}
void addE(Trie* a, Trie* b)
{
/// 反向建图,加边
Edge<Trie> *e = new Edge<Trie>();
e -> nxt = a -> edge;
e -> t = b;
a -> edge = e;
return ;
}
void set_up(Trie *p)
{
/// 反向建图
queue <Trie*> q;
q.push(p);
/// 通过广搜来遍历每一个节点,通过失配指针反向建图
while(not q.empty())
{
Trie *j = q.front(); q.pop();
for(register int i = 1; i < 27; ++i)
{
if(j -> nxt[i] and j -> nxt[i] != p)
{
// 有这个子节点而且这个节点不是根节点
// 如果根节点进入到队列,那么就会永远在队列中……就MLE了
q.push(j -> nxt[i]);
addE(j -> nxt[i] -> fail, j -> nxt[i]);
}
}
}
}
int dfn;
void dfs(Trie *p)
{
/// 先序遍历求dfs序
p -> dfn = ++dfn;
for(register Edge<Trie>* i = p -> edge; i; i = i -> nxt)
{
dfs(i -> t);
// 对于每一条边都深搜下去
p -> sz += i -> t -> sz;
// 并且维护父节点的大小
}
}
int lowbit(int);
void change(int, int);
int query(int);
int sum[1000201];
// 树状数组求和
void solve(Trie* p, char *str, int len)
{
for(register int i = 0; i < len; ++i)
{
// 标记文本串直接跳到的节点
register const int x = str[i] - 'a' + 1;
while(not p -> nxt[x] and p -> fail != p) p = p -> fail;
p = p -> nxt[x];
change(p -> dfn, 1);
}
}
int len;
int main()
{
root = new Trie;
cin >> n;
for(register int i = 1; i <= n; ++ i)
{
cin >> (t + len);
ed[i] = add(root, t + len);
len += strlen(t + len);
t[len++] = 'a' - 1;
// 储存文章
}
set_fail(root);
set_up(root);
dfs(root);
solve(root, t, len);
for(register int i = 1; i <= n; ++i)
{
int ans = query(ed[i] -> dfn + ed[i] -> sz - 1) - query(ed[i] -> dfn - 1);
cout << ans << endl;
}
return 0;
}
int lowbit(int x)
{
return x & (-x);
}
void change(int n, int x)
{
for(int i = n; i <= dfn; i += lowbit(i)) sum[i] += x;
}
int query(int n)
{
int res = 0;
while(n)
{
res += sum[n];
n -= lowbit(n);
}
return res;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具