AC自动机
实现
- 基础的 Trie 结构:将所有的模式串构成一棵Trie
- KMP的思想:对Trie树上所有的结点构造失配指针,并建成图
root:初始化的根
num:节点数量
mark[]:记录val值
fail[]:fail数组,跳转数组
struct Ac_Automaton {
int root, num; //根和节点数
int tree[maxn][26]; //trie树节点
int mark[maxn]; //记录mark
int fail[maxn]; //fail数组
Ac_Automaton() { //初始化函数
root = num = 0;
memset(tree, 0, sizeof(tree));
memset(mark, 0, sizeof(mark));
memset(fail, 0, sizeof(fail));
}
void init() { //清空函数
memset(tree, 0, sizeof(int) * (num + 1) * 26);
memset(mark, 0, sizeof(int) * (num + 1));
memset(fail, 0, sizeof(int) * (num + 1));
root = num = 0;
}
}ac;
Trie 树构建
- 用tire 树的插入操作构建 Trie 树
void insert(char* str) {
int position = root;
for (int i = 0; str[i]; i++) {
int symbol = str[i] - 'a';
if (!tree[position][symbol])
tree[position][symbol] = ++num;
position = tree[position][symbol];
}
mark[position]++;
}
fail数组构造
特点
- 在失配的时候用于跳转的指针
- fail 数组跳转到最长真后缀的位置
- fail 指针会在字典树上的结点来回穿梭
- 每个点都只连出一条 fail 边,且连到的点对应的字符串长度更小,所以 fail 边构成了一棵 fail 树。
实现
利用部分已经求出 fail 指针的结点,推导出当前结点的 fail 指针
实现方式为BFS
-
如果 fail[p] 结点s 到 e 的边存在:
- 让fail[p]指向的点指向e
- 即在原本后缀相同的串内再加一个字符
-
如果*fail[p]*结点s 到 e 的边不存在:
- 继续寻找*fail[fail[p]]*指向的节点,重复上述过程,一直跳到根节点
- 如果没有,令*fail[u] =*根节点
如果真的模拟上述的过程BFS,想想复杂度就很高
这时考虑像并查集一样做路径压缩
void build() { queue<int> q; register int position; for (int i = 0; i < 26; i++) if (tree[root][i]) q.push(tree[root][i]); //直接将根节点的子节点加入队列,因为fail[root]=0,他会自己指向自己 while (!q.empty()) { position = q.front(); q.pop(); //取出第一个fail指针 for (int i = 0; i < 26; i++) { if (tree[position][i]) { //若fail存在指向的点 fail[tree[position][i]] = tree[fail[position]][i]; //由于已经路径压缩,所以只需要一次跳转即可 q.push(tree[position][i]); //将该位入队 } else tree[position][i] = tree[fail[position]][i]; //将他指向fail指针的fail[]指针指向的值,将不存在的点做路径压缩 } } }
多模式串匹配
int find(char* str) { register int position = root, res = 0; for (int i = 0; str[i]; i++) { position = tree[position][str[i] - 'a']; //循环遍历,因为上方的不匹配的字符已经建成了图,所以他实际上在图上跳来跳去 for (int j = position; j && ~mark[j]; j = fail[j]) res += mark[j], mark[j] = -1; //fail指针找到所有匹配的模式串 } return res; }