DFA 最小化

Posted

技术标签:

【中文标题】DFA 最小化【英文标题】:DFA minimization 【发布时间】:2012-06-23 08:13:55 【问题描述】:

我有一个关于 的问题。因此,我使用了众所周知的技术将正则表达式转换为 NFA,然后使用 goto / 闭包算法从中构造 DFA。现在的问题是如何最小化它?我在这里看过有关它的演讲:http://www.youtube.com/watch?v=T9Z66NF5YRk,但我仍然无法理解这一点。什么是 ?这只是合并相同的状态(在相同字符上进入相同状态的状态)还是不同的东西?

所以,我从以下语法开始:

%digit = '0'..'9'
%letter = 'a'..'z' | 'A'..'Z'
%exponent = ("e" | "E") ("+" | "-")? digit+

T_INT = digit+
T_FLOAT = T_INT exponent
T_IDENTIFIER = (letter | "$" | "_") (letter | "$" | "_" | digit)*

最终得到以下 DFA(表示为 JSON):


    "START": [
        "type": "range",
        "from": 36,
        "to": 36,
        "shift": "1"
    , 
        "type": "range",
        "from": 48,
        "to": 57,
        "shift": "2"
    , 
        "type": "range",
        "from": 65,
        "to": 90,
        "shift": "1"
    , 
        "type": "range",
        "from": 95,
        "to": 95,
        "shift": "1"
    , 
        "type": "range",
        "from": 97,
        "to": 122,
        "shift": "1"
    ],
    "1": [
        "type": "range",
        "from": 36,
        "to": 36,
        "shift": "1"
    , 
        "type": "range",
        "from": 48,
        "to": 57,
        "shift": "1"
    , 
        "type": "range",
        "from": 65,
        "to": 90,
        "shift": "1"
    , 
        "type": "range",
        "from": 95,
        "to": 95,
        "shift": "1"
    , 
        "type": "range",
        "from": 97,
        "to": 122,
        "shift": "1"
    , 
        "shift": ["t_identifier"]
    ],
    "2": [
        "type": "range",
        "from": 48,
        "to": 57,
        "shift": "2"
    , 
        "type": "range",
        "from": 69,
        "to": 69,
        "shift": "3"
    , 
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "3"
    , 
        "shift": ["t_int"]
    ],
    "3": [
        "type": "range",
        "from": 43,
        "to": 43,
        "shift": "5"
    , 
        "type": "range",
        "from": 45,
        "to": 45,
        "shift": "5"
    , 
        "type": "range",
        "from": 48,
        "to": 57,
        "shift": "4"
    ],
    "4": [
        "type": "range",
        "from": 48,
        "to": 57,
        "shift": "4"
    , 
        "shift": ["t_float"]
    ],
    "5": [
        "type": "range",
        "from": 48,
        "to": 57,
        "shift": "4"
    ]

那么如何最小化呢?

更新:

好的,这是我的算法。给定以下 DFA:


    0: [
        from: 97,
        to: 97,
        shift: 1
    ],
    1: [
        from: 97,
        to: 97,
        shift: 3
    , 
        from: 98,
        to: 98,
        shift: 2
    ],
    2: [
        from: 98,
        to: 98,
        shift: 4
    ],
    3: [
        from: 97,
        to: 97,
        shift: 3
    , 
        from: 98,
        to: 98,
        shift: 4
    ],
    4: [
        from: 98,
        to: 98,
        shift: 4
    ]

我这样做是为了尽量减少它:

    对于每个状态(在我的示例中编号为 0、1、2、3、4),获取标识此类状态的唯一哈希(例如,对于 state0,这将是:from=97,to=97,shift =1,对于 state1,这将是:from=97,to=97,shift=3&from=98,to=98,shift=2 等等……)

    比较获得的哈希值,如果我们找到两个相同的哈希值,则将其合并。在我的例子中,state2 hash 为:from=98&shift=4&to=98,state4 hash 为:from=98&shift=4&to=98,它们是相同的,所以我们可以将它们合并到新的 state5 中,之后 DFA 将看起来像这样:

    
    0: [
        from: 97,
        to: 97,
        shift: 1
    ],
    1: [
        from: 97,
        to: 97,
        shift: 3
    , 
        from: 98,
        to: 98,
        shift: 5
    ],
    3: [
        from: 97,
        to: 97,
        shift: 3
    , 
        from: 98,
        to: 98,
        shift: 5
    ],
    5: [
        from: 98,
        to: 98,
        shift: 5
    ]
    

    继续这个,直到我们没有任何变化,所以下一步(合并状态 1 和 3)会将 DFA 转换为以下形式:

    
    0: [
        from: 97,
        to: 97,
        shift: 6
    ],
    6: [
        from: 97,
        to: 97,
        shift: 6
    , 
        from: 98,
        to: 98,
        shift: 5
    ],
    5: [
        from: 98,
        to: 98,
        shift: 5
    ]
    

    没有更多相同的状态,这意味着我们已经完成了。

第二次更新:

好的,给定以下正则表达式:'a' ('ce')* ('d' | 'xa' | 'AFe')+ | 'b' ('ce')* ('d' | 'xa' | 'AFe')+ 'ce'+ 我有以下 DFA (START -> start state, ["accept"] -> so to说转换到接受状态):


    "START": [
        "type": "range",
        "from": 98,
        "to": 98,
        "shift": "1.2"
    , 
        "type": "range",
        "from": 97,
        "to": 97,
        "shift": "17.18"
    ],
    "1.2": [
        "type": "range",
        "from": 120,
        "to": 120,
        "shift": "10"
    , 
        "type": "range",
        "from": 100,
        "to": 100,
        "shift": "6.7"
    , 
        "type": "range",
        "from": 65,
        "to": 65,
        "shift": "8"
    , 
        "type": "range",
        "from": 99,
        "to": 99,
        "shift": "4"
    ],
    "10": [
        "type": "range",
        "from": 97,
        "to": 97,
        "shift": "6.7"
    ],
    "6.7": [
        "type": "range",
        "from": 99,
        "to": 99,
        "shift": "15"
    , 
        "type": "range",
        "from": 120,
        "to": 120,
        "shift": "13"
    , 
        "type": "range",
        "from": 100,
        "to": 100,
        "shift": "6.7"
    , 
        "type": "range",
        "from": 65,
        "to": 65,
        "shift": "11"
    ],
    "15": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "14.accept"
    ],
    "14.accept": [
        "type": "range",
        "from": 99,
        "to": 99,
        "shift": "16"
    , 
        "shift": ["accept"]
    ],
    "16": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "14.accept"
    ],
    "13": [
        "type": "range",
        "from": 97,
        "to": 97,
        "shift": "6.7"
    ],
    "11": [
        "type": "range",
        "from": 70,
        "to": 70,
        "shift": "12"
    ],
    "12": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "6.7"
    ],
    "8": [
        "type": "range",
        "from": 70,
        "to": 70,
        "shift": "9"
    ],
    "9": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "6.7"
    ],
    "4": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "2.3"
    ],
    "2.3": [
        "type": "range",
        "from": 120,
        "to": 120,
        "shift": "10"
    , 
        "type": "range",
        "from": 100,
        "to": 100,
        "shift": "6.7"
    , 
        "type": "range",
        "from": 65,
        "to": 65,
        "shift": "8"
    , 
        "type": "range",
        "from": 99,
        "to": 99,
        "shift": "5"
    ],
    "5": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "2.3"
    ],
    "17.18": [
        "type": "range",
        "from": 120,
        "to": 120,
        "shift": "25"
    , 
        "type": "range",
        "from": 100,
        "to": 100,
        "shift": "22.accept"
    , 
        "type": "range",
        "from": 65,
        "to": 65,
        "shift": "23"
    , 
        "type": "range",
        "from": 99,
        "to": 99,
        "shift": "20"
    ],
    "25": [
        "type": "range",
        "from": 97,
        "to": 97,
        "shift": "22.accept"
    ],
    "22.accept": [
        "type": "range",
        "from": 120,
        "to": 120,
        "shift": "28"
    , 
        "type": "range",
        "from": 100,
        "to": 100,
        "shift": "22.accept"
    , 
        "type": "range",
        "from": 65,
        "to": 65,
        "shift": "26"
    , 
        "shift": ["accept"]
    ],
    "28": [
        "type": "range",
        "from": 97,
        "to": 97,
        "shift": "22.accept"
    ],
    "26": [
        "type": "range",
        "from": 70,
        "to": 70,
        "shift": "27"
    ],
    "27": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "22.accept"
    ],
    "23": [
        "type": "range",
        "from": 70,
        "to": 70,
        "shift": "24"
    ],
    "24": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "22.accept"
    ],
    "20": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "18.19"
    ],
    "18.19": [
        "type": "range",
        "from": 120,
        "to": 120,
        "shift": "25"
    , 
        "type": "range",
        "from": 100,
        "to": 100,
        "shift": "22.accept"
    , 
        "type": "range",
        "from": 65,
        "to": 65,
        "shift": "23"
    , 
        "type": "range",
        "from": 99,
        "to": 99,
        "shift": "21"
    ],
    "21": [
        "type": "range",
        "from": 101,
        "to": 101,
        "shift": "18.19"
    ]

故事是一样的,我如何最小化它?如果我在所有这些表结构中遵循经典的 Hopcroft 算法,确定不可区分的状态,将它们合并在一起等等,那么我将获得包含 15 个状态的 DFA(使用这个工具:http://regexvisualizer.apphb.com/ 和这个正则表达式 a(ce )(d|xa|AFe)+|b(ce)(d|xa|AFe)+ce+ 检查)。以下是使用 Hopcroft 算法缩小后 DFA 的样子:

我提出的算法,在“重新思考”Hopcroft 的算法之后,构建的 DFA 比您在上面看到的要小(对不起图像质量,我不得不一步一步地重新绘制它以了解为什么它更小):

这就是它的工作原理,关于“状态等价”的决定是基于给定状态(例如“START”)的哈希函数的结果,如果我们开始构建可以从 DFA 构造的短字符串从那个状态。给定上面的 DFA 和 START 状态,我们可以构造以下字符串:98->120, 98->100, 98->65, 98->99, 97->120, 97->100, 97->65 , 97->99 所以让它成为 START 状态的哈希函数的结果。如果我们为 DFA 中的每个状态运行此函数,我们将看到对于某些状态,此函数为我们提供相同的结果(“1.2”、“6.7”、“2.3”和“10”、“13”和“15” , "16" AND "11", "8", "26", "23" AND "12", "9", "4", "5", "20", "21" AND "17.18", " 18.19" AND "25", "28" AND "27", "24") 所以我们需要做的就是将这些状态合并在一起。

我发现我在某个地方错了,但不明白我的算法生成的最小化 DFA 有什么问题?

【问题讨论】:

这里也有讨论:binarysculpting.com/2012/03/21/dfa-state-minimization 所以我希望最后能从某个地方得到答案... 谢谢大家,我试图找到更优雅的解决方案来最小化 DFA 的尝试没有成功,所以我终于实现了经典的 Hopcroft,我很满意 :) 【参考方案1】:

您提出的算法没有进行完全最小化,因为它没有检测到行为相同的复杂结构。要理解这一点,请查看此 DFA(由 JFLAP 绘制):

最小化将结合 q1 和 q2,但概述的算法无法做到。

与此相反,Hopcroft 的算法最初会这样划分:

   q0, q1, q2, q3

然后拆分第一组,因为q0没有到q3的过渡:

   q0, q1, q2, q3

不要进一步拆分,因为 q1 和 q2 的行为相同。

【讨论】:

嗨,Gunther,谢谢你的提示,你说得对,我的原始算法无法处理 q1 和 q2 之间的合并,但如果我稍作修改,它就可以处理:当获得状态的散列,我需要检查字符是否进入我们找到它的相同状态。所以 q1 的散列是:from=99,to=99,shift=itself,q2 的散列是:from=99,to=99,shift=itself。然后它也适用于这个 DFA :) 你同意吗?你能想出一些使用这种修改后的算法不会被缩小的 DFA 吗? 不,抱歉,事情没有那么简单。想象一下任意复杂但重复的子自动机,而不是“c”弧。 我已经试过了,它在复杂结构上给了我最小的 DFA:(('a'|'b')+('a'|'b') 'c'' 3'+ '0'..'9' ('a'..'z' | 'A'..'Z')+ '3'+ 'd' 'f')+ ((' a'|'b') 'c'*'3'+ '0'..'9' ('a'..'z' | 'A'..'Z')+)? 'f' 但我仍然得到最小的 DFA(针对位于此处的 Hopcroft 的最小化可视化工具进行测试:regexvisualizer.apphb.com)我的算法给出的状态数量与该工具生成的最小化 DFA 中的状态数量相同。 ..问题仍然是我如何证明我的算法与 Hopcroft 的算法相同,即。 e.产生最小的 DFA? 我上面展示的 DFA 是针对 ac*d|bc*d 的,但是 a(ce)*d|b(ce)*d 呢?在这种情况下,不会有“shift=itself”转换。 是的,你说得对 :) 这意味着它不像比较哈希那么简单。我想我们可以关闭这个。感谢大家!我会继续我的研究。【参考方案2】:

将您的原始 DFA 称为 M1。 简单来说,构造一个最小化的 DFA(称为 M2)意味着将其转换为包含最少状态数的 DFA。因此,M2 中的状态数将少于 M1 中的状态数。这里需要注意的重要一点是 M1 和 M2 必须是等价的,这意味着它们必须定义相同的正则语言。构建一个最小化的 DFA 不仅涉及寻找相同的状态,还涉及以下内容:

    删除“无法访问”状态: 这涉及从 DFA 的初始状态中删除任何输入字符串无法到达的状态。

    删除“死亡”或“陷阱”状态: 这涉及删除以自身终止的非接受状态。它们也被称为 TRAP 状态。

    删除“不可区分”状态: 这涉及删除对于任何输入字符串无法相互区分的状态。

还有一些流行的算法用于 DFA 最小化:

Moore's algorithm

Brzozowski's algorithm

Hopcroft's algorithm

这些算法可能值得一试!

【讨论】:

谢谢,故事是我想出了一些东西,可以将我的 DFA 最小化到某个状态,据我所知,它为我所拥有的输入生成了最小的 DFA,问题是:它会产生最小 DFA?或者它只是一堆代码,做了一些最小化,但结果不是最小可能的 DFA...请参阅我对原始问题的更新,它描述了我的算法。【参考方案3】:

鉴于您有将 NFA 确定为 DFA 的代码,将其最小化的最简单解决方案是 Brzozowski 算法。 您将需要实现一个函数来反转 NFA,但这相当简单。 (反转转换,交换开始和接受状态)

一旦你有一个确定和反向函数,Brzozowski 最小化被实现为:

minimize(nfa) = determinize(reverse(determinize(reverse(nfa)))) 

恕我直言,这是一个非常优雅的解决方案

【讨论】:

以上是关于DFA 最小化的主要内容,如果未能解决你的问题,请参考以下文章

DFA最小化,语法分析初步

DFA 最小化

DFA最小化,语法分析初步

DFA最小化,语法分析初步

形式语言与自动机六 DFA的最小化

DFA最小化,语法分析初步