将字符集转换为 nfa/dfa 的高效算法
Posted
技术标签:
【中文标题】将字符集转换为 nfa/dfa 的高效算法【英文标题】:Efficient algorithm for converting a character set into a nfa/dfa 【发布时间】:2011-04-02 02:23:18 【问题描述】:我目前正在研究扫描仪生成器。 生成器已经可以正常工作了。但是当使用字符类时,算法会变得很慢。
扫描仪生成器为 UTF8 编码文件生成扫描仪。应该支持全范围的字符(0x000000 到 0x10ffff)。
如果我使用大型字符集,例如任何运算符 '.'或 unicode 属性 L,nfa(以及 dfa)包含很多状态(> 10000)。因此,将 nfa 转换为 dfa 并创建最小 dfa 需要很长时间(即使输出的最小 dfa 仅包含几个状态)。
这是我当前创建 nfa 的字符集部分的实现。
void CreateNfaPart(int startStateIndex, int endStateIndex, Set<int> characters)
transitions[startStateIndex] = CreateEmptyTransitionsArray();
foreach (int character in characters)
// get the utf8 encoded bytes for the character
byte[] encoded = EncodingHelper.EncodeCharacter(character);
int tStartStateIndex = startStateIndex;
for (int i = 0; i < encoded.Length - 1; i++)
int tEndStateIndex = transitions[tStartStateIndex][encoded[i]];
if (tEndStateIndex == -1)
tEndStateIndex = CreateState();
transitions[tEndStateIndex] = CreateEmptyTransitionsArray();
transitions[tStartStateIndex][encoded[i]] = tEndStateIndex;
tStartStateIndex = tEndStateIndex;
transitions[tStartStateIndex][encoded[encoded.Length - 1]] = endStateIndex;
有谁知道如何更有效地实现该功能以仅创建必要的状态?
编辑:
更具体地说,我需要一个类似的函数:
List<Set<byte>[]> Convert(Set<int> characters)
???????
将字符 (int) 转换为 UTF8 编码 byte[] 的辅助函数定义为:
byte[] EncodeCharacter(int character)
...
【问题讨论】:
您正在为 byte 输入构建 xFA?对 (Utf16) 字符进行操作不是更容易(也更可靠)吗? 我不这么认为,使用 16 位字符时查找表的大小会增加。如果使用 utf16(与 utf8 相比),典型的输入文件也会更大。 对不起,我误会了!接受任何编码对于未来版本来说都是一个不错的选择。但为了简单起见,我认为只实现一种编码会更容易,而且 UTF-8 对我来说似乎很合适。 然后我得到了包含所有 0x10ffff 字符条目的查找表。那怎么实现转换表??? 您需要一种“智能”方式来处理字符集。一个简单的想法是有 1 个 0..0x10ffff 查找表(大但可能)来找到每个字符的 set#。 【参考方案1】:有很多方法可以处理它。它们都归结为在数据结构中一次处理一组字符,而不是枚举整个字母表。这也是您在合理的内存量中制作 Unicode 扫描仪的方法。
对于如何表示和处理字符集,您有很多选择。我目前正在使用一种解决方案,该解决方案保留边界条件和相应目标状态的有序列表。如果您必须在每个接合点扫描整个字母表,您可以更快地处理这些列表上的操作。事实上,它在 Python 中以可接受的速度运行已经足够快了。
【讨论】:
【参考方案2】:我将澄清我认为您所要求的:合并一组 Unicode 代码点,以便您生成状态最小的 DFA,其中转换表示这些代码点的 UTF8 编码序列。
当您说“更高效”时,它可能适用于运行时、内存使用或最终结果的紧凑性。有限自动机中“最小”的通常含义是指使用最少的状态来描述任何给定的语言,这就是“只创建必要的状态”所得到的。
每个有限自动机都有一个等价的状态最小值 DFA(参见Myhill-Nerode 定理[1],或Hopcroft & Ullman [2])。出于您的目的,我们可以直接使用 Aho-Corasick 算法 [3] 构建这个最小 DFA。
为此,我们需要一个从 Unicode 代码点到它们对应的 UTF8 编码的映射。无需预先存储所有这些 UTF8 字节序列;它们可以即时编码。 UTF8编码算法有据可查,这里不再赘述。
Aho-Corasick 首先构造一个trie。在您的情况下,这将是依次添加的每个 UTF8 序列的尝试。然后根据算法的其余部分,用转换将其转换为 DAG 对该树进行注释。有一个很好的overview of the algorithm here,但我建议阅读论文本身。
这种方法的伪代码:
trie = empty
foreach codepoint in input_set:
bytes[] = utf8_encode(codepoint)
trie_add_key(bytes)
dfa = add_failure_edges(trie) # per the rest of AC
这种方法(形成一个 UTF8 编码序列的 trie,然后是 Aho-Corasick,然后渲染出 DFA)是在实现我的正则表达式和有限状态机库时采用的方法,我正是这样做来构造 Unicode 字符类。在这里你可以看到代码:
UTF8 编码 Unicode 代码点:examples/utf8dfa/main.c
构造树:libre/ac.c
为每个字符类渲染出最小 DFA:libre/class/
其他方法(如该问题的其他答案中所述)包括处理代码点和表示代码点范围,而不是拼出每个字节序列。
[1] Myhill-Nerode:Nerode,Anil (1958),线性自动机变换,AMS 会议记录,9,JSTOR 2033204 [2] Hopcroft & Ullman (1979),第 3.4 节,定理 3.10,第 67 页 [3] 阿霍,阿尔弗雷德五世; Corasick, Margaret J.(1975 年 6 月)。 高效的字符串匹配:帮助书目搜索。 ACM 的通信。 18 (6): 333–340。
【讨论】:
对不起,如果这是一个菜鸟问题。我们真的需要在上面的伪代码中添加失败边吗?我的意思是,我们不需要在输入字符字节的某处开始搜索子字符串:一旦字节未能遵循特里树,那么我们就知道输入字节不属于字符类。我错过了什么吗?【参考方案3】:看看 Google RE2 和 TRE 等正则表达式库在做什么。
【讨论】:
我认为 Google RE2 做了我需要的事情,但它非常复杂......我在 code.google.com/p/re2/source/browse/re2/compile.cc 找到了一些有趣的代码(从第 559 行开始)【参考方案4】:我的扫描仪生成器遇到了同样的问题,所以我想出了用间隔树确定的 id 替换间隔的想法。例如 dfa 中的 a..z 范围可以表示为:97, 98, 99, ..., 122,而不是我将范围表示为 [97, 122],然后从中构建区间树结构,所以最后它们被表示为引用区间树的 id。给定以下 RE:a..z+,我们最终得到这样的 DFA:
0 -> a -> 1
0 -> b -> 1
0 -> c -> 1
0 -> ... -> 1
0 -> z -> 1
1 -> a -> 1
1 -> b -> 1
1 -> c -> 1
1 -> ... -> 1
1 -> z -> 1
1 -> E -> ACCEPT
现在压缩间隔:
0 -> a..z -> 1
1 -> a..z -> 1
1 -> E -> ACCEPT
从 DFA 中提取所有区间并从中构建区间树:
"left": null,
"middle":
id: 0,
interval: [a, z],
,
"right": null
将实际间隔替换为它们的 id:
0 -> 0 -> 1
1 -> 0 -> 1
1 -> E -> ACCEPT
【讨论】:
【参考方案5】:在这个库 (http://mtimmerm.github.io/dfalex/) 中,我通过在每个转换上放置一系列连续字符而不是单个字符来做到这一点。这是通过NFA构建,NFA->DFA转换,DFA最小化和优化的所有步骤进行的。
它相当紧凑,但它增加了每一步的代码复杂度。
【讨论】:
以上是关于将字符集转换为 nfa/dfa 的高效算法的主要内容,如果未能解决你的问题,请参考以下文章