最近在学编译原理, 里面在3.3节词法单元识别后面就提到了这个算法然后根据网上资料自己做了一遍,只支持英文字母
可能最后的效果没有oi-wiki上的效率高
1. 背景
大概涉及到的知识:
- Trie 树,一种字典树,可以看这里做的挺直观的
- BFS 广度优先搜索Trie树
- 状态压缩,随便做的小优化,可能有负效果对于数据量小(x
主要思想就是用int/long这种数据类型的每一位通过位运算当作bool而不是单独声明bool类型, 主要的算法就(n>>k)&1
取出n的第k位的数据n^(1<<k)
n的第k位取反
- AC自动机的失配(失效)算法
2. 思想
主要流程就, 先构造一棵trie树,然后用bfs构造每个节点的失配位置(最后效果就类似于DFA确定有限状态自动机),然后再遍历以匹配出结果
3. 代码
3.1 Trie树
比较简易的做法就是
1 | struct Node{ |
用数组的下标表示对应边(anscii, 比如 char - 'a'
)和对应的下一个节点
这里优化一下, 不然内存地址太分散然后其实这个数据结构不是很有必要
所以本文用一个二维数组代替这一套(nodes[x][y]
x是节点的编号,y是y+'a'
的边指向的节点 0<=y<=25, 比如a的边就是'a'-'a'=0
), 具象化表示就参考oi-wiki
1 | constexpr int s = 50; |
s
是数组大小,因为后面状态压缩的时候还要用到就提取出来,作用和#define
一样end
是代表对应下标的节点是不是单词的结尾(判断匹配是否成功)now
是下一个节点应该是哪个下标
然后就是写add()
或者insert
方法
1 | void trieAdd(const std::string &text) { // 插入单词 |
3.1.1 优化endNodes
这里做了一个可有可无的优化, 就把代表end节点从一个bool数组改成一个int32_t数组, 然后用32位中每一位表示一个节点是否为接受节点(0或1)
为了使数据方便取余, 最好用2的整次方为数位长度(如8, 16, 32等)
1 | constexpr int s = 50; |
所以上面的声明代码就变成这样, 和add方法
1 | void trieAdd(const std::string &text) { // 插入单词 |
然后当要去第p个节点是否是接受(结束)节点时:
1 | if ((endNodes[p / bitW] >> (p & (bitW - 1))) & 1) |
3.2 失配算法
主要就先准备一个fail数组(在这里我设立的是从1开始, 就不用提前赋值全部元素为-1, 因为元素可以是0), 下标代表对应的节点失配后跳转到哪个节点
然后用一个队列(queue)确保BFS因为先入先出
然后就循环每一个节点和子节点找失配位置
具体流程:
先从根节点开始 -> 依循bfs也就是宽度(广度)优先顺序搜索每个子节点->先遍历每个子节点的每条边->当边不为空(指向的子节点!=0, 因为边是不可能指向根节点), 对于每个子节点的边有3种情况:
- 如果父节点是0也就是根节点, 那当前边指向的子节点的失配位置就是根节点也就是0
- 如果父节点是失配位置有当前边, 那当前边指向的子节点的失配位置就是父节点的失配位置
- 如果以上都不是, 把父节点的失配位置看作这条边的父节点然后继续上面的流程知道父节点是根节点
执行上面的流程找到失配位置后把当前边对应的子节点压入队列然后开始下一条边
1 | int fail[s]{0}; |
3.3 寻找
就循环每条边, 如果匹配不上就移动到失配节点继续
1 | vector<string> trieFind(const std::string &text) { // 查找一个单词 |
3.4 测试代码
1 | string printVector(vector<string> t) { |
3.5 完整代码
1 | /* |
4. 可视化扩展
可以通过python的visual-automata来画
4.1 cpp 更改
这部分就不考虑优化了(
先声明一个vector<int> final;
储存最终节点
然后在triedAdd方法push_back
1 | void trieAdd(const std::string &text) { // 插入单词 |
要在main方法调用后
1 | nlohmann::json j; |
就可以拿到json格式的转换表
4.2 python
先根据pypi里的安装依赖, 然后把上面的json数据复制到下面代码里的jsonData
然后运行就可以了
1 | #!/usr/bin/python |
这里用nfa而不是dfa是因为dfa画起来线太多很乱, 因为每个点有26根可能的线, 所以就把失配位置用fail
线指向