解决单个字符串匹配的方法是KMP算法
得益于其next数组,整个算法时间复杂度被显著压缩。
在O(n+m)的时间内即可以算出模式匹配。
AC自动机是一个支持多模式匹配的算法。
也就是给出模式串A1,A2,A3……An,以及一个字符集S
在模式串长度不大的情况下算出匹配数
有N个模式串,平均长度为L;文章长度为M。
建立Trie树:O(N*L)
建立fail指针:O(N*L)
模式匹配:O(M*L) 所以,总时间复杂度为:O( (N+M)*L )。
【注:有时记得考虑字符范围常数】
便于理解,可以考虑类似于优化过的暴力算法
譬如在abcaabacc找acc
除了长度优化以外,最直接的优化方法:“只找a”
算是一个残废的KMP的next数组。
个人认为是理解失败指针的一个不错的方式
#include <bits/stdc++.h>
using namespace std;
const int maxn = 50;
struct TrieNode;
typedef TrieNode* Position;
//字典树节点
struct TrieNode{
Position next[maxn]; //next数组,字典树概念
Position fail; //失败指针,自动机新节点
int sum; //某道题需要存的值
TrieNode():fail(0),sum(0){
memset(next,0,sizeof(next)); //初始化
}
};
struct Trie{
inline static int toNumber(char t){
return t - 'a'; //把字符映射成数字,
}
Position root;
void insert(const char* s){ //插入函数
if(!root) //根为空,给根开辟空间
root = new TrieNode();
Position head = root; //迭代向下走,走不了开辟新节点
for (int i = 0 ; s[i] ; ++i){
int next_pos = Trie::toNumber(s[i]);
if(!head->next[next_pos])
head->next[next_pos] = new TrieNode();
head = head->next[next_pos];
}
head->sum++; //打个标记,意味着如果到这里就代表有个模式在这里匹配
}
//插入操作类似于字典树,仅有微小区别。
//以下是AC自动机核心部分,建立fail指针节点,利用BFS
void build_AC(){
Position tmp;
Position p;
queue<Position> Q; //队列
Q.push(root); //根入队
while(!Q.empty()){
tmp = Q.front(); Q.pop(); //基本操作
for (int i = 0 ; i < maxn ; ++i){
if(tmp->next[i])
{
if(tmp == root) //1.所有根节点的儿子节点的失败节点指向根
tmp->next[i]->fail = root; //因为一旦从这里匹配失败了自然只能从头再来
else
{
/*
2.如果不是根节点:
大致意思是:
在某个地方不匹配,就要找到另外一个有可能走的地方
确实,能走的地方(当前匹配)有很多,考虑由底向上
例子:
abababa
0123456
在位置6的a的失败指针应该指向位置4的a的下一个地方
在位置4的a的失败指针应该指向位置2的a的下一个地方
以此类推。。。。
这样一来,如果位置6的不匹配,首先走到4,然后再走到2,走到0
顺然而然直到匹配位置,或是回到根节点
*/
p = tmp->fail;
while(p)
{
if(p->next[i])
{
tmp->next[i] -> fail = p->next[i];
break;
}
p = p->fail;
}
if(p == NULL)
tmp->next[i]->fail = root; //如果找不到,只能从头再来。
}
Q.push(tmp->next[i]); //基本操作
}
}
}
}
//匹配部分,可能比上述好理解
int Ac_automation(const char* t){
int cnt = 0; //统计匹配数目,可忽略,随题变化
Position p = root;
int len = strlen(t);
for (int i = 0 ; i < len ; ++i){
int next_pos = Trie::toNumber(t[i]); //遍历字典树,看下一个是否存在
while(!p->next[next_pos] && p != root) p = p->fail;
//不行就一直沿着失败指针走,走到根节点为止
p = p->next[next_pos]; //1.找到了 2.回到根节点
//无论如何,按字典树走一步
// p为空的情况 也就是说,在上一步啥也没找到,回到根,根往后强行走一步
if(!p) //到了空指针处,把p修正为根,重新开始
p = root;
Position tmp = p;
while(tmp != root)
{
if(tmp->sum >= 0)
{
cnt += tmp->sum;
}
else break;
tmp = tmp -> fail;
//一句话就是匹配了一个串等于匹配了它所有的前缀。
//匹配到abababab 也就匹配到 ababab 也就匹配到 ababa , aba , ab 。.....
//可以自行改
}
}
return cnt;
}
};