本文是本人撰写的编译原理讲义。
本系列讲义适用于:被强迫学习编译原理前端,或者希望弄明白如何做科研的人
1. 这用户体验优化,狗都不做
老板:我听技术部反馈,前不久已经把正则文法转DFA的技术走通了,不知道产品好不好用?
客户:那可太好用了,用户每次修改确实不用修改那么多代码了,只需要把正则文法写出来之后,再转成NFA,再转成DFA,再进行DFA最小化,之后再把对应的DFA交给写死的词法分析程序就可以了。用户用得可开心了。
老板:听着怨气很重啊。但您先等一下,我怎么听着只有第一步写正则表文法的这一步需要人工参与,后面的不全都可以通过机械算法自动化完成吗?
客户:说是这么说,您倒是写一个正则文法试试?那一堆箭头我也都懒得吐槽了,就那一个状态的命名就搞死人,单英文符号也就能命名几个状态,写长了吧,一个是累死人,另一个是容易产生歧义。关键这玩意儿写着又不影响最终结果,不命名好回头又难维护。另外这个产生式的写法也没个统一,左部相同的产生式有些人不写到一起,有的人又写到了一起!总之这反人类的玩意儿谁爱用谁用,这产品要一直这样下去我肯定不用了!
老板:亲爱的甲方爸爸,您的吐槽非常正确且犀利,我们一定为您妥善处理这个用户体验的问题。(回到办公室刚关上门)那个谁,你怎么做开发的?你走通功能有什么用,用户体验这么差,谁还用我们的产品?!我给你三天时间,给我开发一个用户满意的方案出来,做不出来给我滚!
那谁:(脸带面壁者的微笑回到工位。)这一天天的,这破班谁爱上谁上!老子好不容易大学毕业,结果就是到这里不停受气!那蠢货客户,那么简单的正则文法都写不会,我设计得多优雅啊!那垃圾资本家活该吊路灯的就知道向着用户,有没有一点尊重自己员工尊重知识尊重技术的样子!!不干了不干了,谁爱干谁干。再***见,明天我就提交辞职信。到点下班,先叫个顺风车....嗯?怎么马上就到了?师傅您好,我尾号是9527。。???(老板居然在跑滴滴???赶紧拿出手机:师傅!!你猜我坐顺风车打到谁的车了!!!)
师傅:看你这死出,应该是打到老板的车了吧。(那个谁:你怎么知道!!还好我平常有戴口罩的习惯,他好像没认出我。)师傅:我们大家都知道呀,公司效益不好,老板天天愁着发不出工资,下班之后开顺风车都不是一天两天了。你戴不戴口罩都一样,认出又咋样,老板他又不care。他平常嘴是臭了一点,但也就想能养活我们这帮人,他跑顺风车一点心理压力都没有。我都劝他不用这么累,他说等最近签下词法分析那个单子估计就会好一点了(那个谁:不是都交付了吗)师傅:你傻呀,做得好才能有长期合作呀,你就想着一锤子买卖吗?
2. 狗不做我做。
回到家,敲了好几次的“辞职信”,删掉,重打,删掉重打。重复了好几次。
师傅的话始终浮现在眼前,连那么一个大老板,都能放下身段去开顺风车,我这个还在打拼发展的,做出来不合用户预期的产品,被说个两句,真的要就这么走掉吗?
……
妈的也就三天,我要是做不出来我再炒老板鱿鱼不迟!
2.1 它山之石可以攻玉
之前老师怎么说的来着,研究问题先从简单问题开始?
哎行吧,那就先找个相对简单的有代表性的例子看看。就决定是你了,十进制无符号常数(下文简称常数)!
旁边的智能音箱吐槽道:你是宝可梦训练大师吗?
我谢谢你,没人问你意见,关机。这智障音箱一天天的就知道瞎接话。
算了,先写出它对应的正则文法吧:
S->[0-9]A A->[0-9]A | [0-9]
第二条也可以写成
A->[0-9]A | ε
看了一眼隔壁if关键字的正则文法:S->if T_IFKEY,不由得一整胡思乱想:要是不止像if这样的常符号串能一次性写出来,连带中间各种乱七八糟的能用一行结束那多舒服。
比如,对于类似S->0A,A->1这样状态跳转就是一条直路的情况,是可以很容易写成01。
而对于其他乱七八糟的部分,有几个点最让人难受:1,分支;2,递归;3,空。
对于分支,何不借用前面的“|”,把可选的分支全部列出来。为了避免分支和前后的部分混淆,可以加个括号限制分支的范围。
比如,长度为2的常数就可以写作:(0|..|9)(0|..|9)
对于递归(重复),回想NFA中描述闭包的类幂函数写法 ,可以用来表示特定序列重复任意次数,其中闭包表示包含零次到任意次,正闭包表示一次到任意次。
比如,(0|..|9)*,就可以表示任意长度的数字串。如果不想有空串,也就是说长度最小为一,那就可以写作(0|..|9)+,或者(0|..|9)(0|..|9)*。当然,如果允许用上中括号,可以进一步简化为[0-9]*,或者[0-9][0-9]*
对于空,目前结合NFA和正则文法遇到的情况来讲,无非有三类特殊情况:一个是在当前状态什么实义符号都没有就发生了状态跳转;一个是从一开始就处于接受态,比如命运石头门的1.130426;一个是无论你怎么做都是Bad End的,比如和前女友,完全是两个世界的人可能就没有一条世界线会让我们在一起吧。。我在想些什么??总之,对于前两种情况,都可以用ε来表示,或者直接什么都不写。比如,一个分支可以是1或者空,那就可以写成(1|ε)或者(1|)。当然在有些场合写一个占位符仍有必要。对于第三种情况,如果写成DFA那肯定就是没有接受态,这种情况,好像没有任何一个语句可以让它走向幸福的结局。。讲方案讲方案!!既然一个都没有,那就是空集∅!两旬空巢老人又如何!!不爱就算了!凭什么说我幼稚!!
这下啥特殊情况都处理完了吧!!!
2.2 如何定义正规式
出来吧!用一串符号就能描述清楚正则文法的最简短词法描述规则,正规式!!
智能音箱:是否需要播放:热烈的决斗者。
真没眼力见,还要问,算了你退下吧。哎呀我真是天才,才过了几个小时而已,看来老板可以不用给我炒掉了。
智能音箱:没理解你的意思,换个说法再试试吧。
谢谢你智障音箱,你简直就是那客户的完美替身。啊对,上面这么一大段意识流拿去给蠢货用户肯定不太妙,还是得写一套说明书。师傅之前怎么教的写说明书来着?好像是先定义对象是什么,由什么组成,再定义怎么用。
那首先,正规式肯定是条式子,式子中用到的符号肯定来自于有限字母表∑,除此以外还有一些用于表示规则的辅助字母表∑′={∅, ε, | , ∙, ∗, (, )},其中,“·”借用于初中代数中的乘号,表示连接。在实际使用中可以像x·y=xy那样直接省略。
考虑到再长的正规式都是由一些子模块拼凑出来的,所以需要定义出什么是基本模块。
-
∑上的任意符号肯定是正规式
-
ε是正规式,表示什么都没有就已经符合要求;
-
∅是正规式,表示没有一样符合要求。
对于任意正规式,他们之间可以发生的运算包括:连接、分支、闭包、被括号包裹,共四种。
那么,只要是有限次使用上述步骤定义的表达式,就是∑上的正规式。所谓有限次使用,意味着正规式可以很长很长,但不能是一个无限长的式子。因为,当定义了一个无限长的式子,意味着不可能读到它的末尾,那就不可能理解这个单词。人类不能理解的东西,毫无意义。
总结起来,便可以得到一段文绉绉的定义了:
正规式:设有限字母表∑, 辅助字母表∑′={∅, ε, | , ∙, ∗, (, )}, 则 1.∅, ε都是∑上的正规式, 它表示正规集∅和{ε} 2.如果a是正规式, 它表示正规集{a} 3.若α, β是正规式, 则(α), α|β, α·β, α∗也是正规式 有限次使用上述步骤定义的表达式才是∑上的正规式;
在定义完什么是正规式之后,最后再来定义一下正则文法的到正规式的转换规则。对于正规文法中出现的规则,总结起来也就三条:
正则文法
正规式
规则1(连接规则)
A->xB
B->y
A=xy
规则2(或规则)
A->x
A->y
A=x|y
规则3(闭包规则)
A->xA
A->y
A=x*y
Emm,有模有样了。意味着现在用户只要直接写出正规式,就可以得到一个和正则文法完全一致作用的词法规则。那接下来只要完成正规式到DFA的转换,我滴任务就完成啦哈哈哈哈哈。
智能音箱:鸡汤来咯。
2.3 正规式到DFA的转换
虽说最终目标是转到DFA,但正如正则文法到DFA的转换也经历了NFA,我现在只需要把正规式转成NFA就可以啦。
这个正规式,既然是符号串,我不如把它作为一个激励,连接一个初态和一个接受态,这样就代表这个初态经过这个符号串的激励之后跳转到了接受态。
但由于NFA的激励中最多只允许包含ε,我还需要把除此以外的全部辅助符号通过特定规则消除。此外还希望激励符号尽可能是单符号,从而简化后续的处理。
还好,我们要处理的也就只有连接或和闭包三个基本操作。
对形如R=st这样的正规式,对应连接操作,有:=>⓪ -s-> ① -t-> ⓶
对形如R=s | t这样的正规式,对应或操作,简单一点可以直接把激励拆分为两个不同的子激励,从而写成右边的摸样。
当然,由于NFA不唯一,你要想弄复杂一点也可以增加一些状态,写成下图左边的样子。
对形如R=s*这样的正规式,对应闭包操作,复杂一点可以写成左边,简单一点可以写成右边:
最后处理几个很特殊又很基本的正规式:
对正规式∅:由于没有任何符号串能让X(X代表D或N)FA走到接受态,也就是说没有任何接受态。根据奥卡姆剃刀原则,除了初始状态以外的所有状态都会被消除,导致只剩下一个不是接受态的状态,因此有=>①。
对于ε 以及∑上的任意符号:
都已经单符号了还处理啥呢
小试一下,把(a|b)∗abb转换为NFA
首先新建两个状态,一个初态一个终态,中间用(a|b)∗abb相连。
首先新建两个状态,一个初态一个终态,中间用(a|b)∗abb相连。
首先处理前面的闭包,有
之后通过增加激励把圈上面的“或”消灭,有:
最后只要把abb进行裂解,即可得到
把正规式转换成NFA之后,再转换成DFA就是易如反掌的事了。
呼,睡觉去。
3. 逆向?又给我整的什么狗屁需求
第二天上班,好不容易哄好了客户,听了几句老板说什么月末升职加薪的大饼,本想着舒一口气。结果老板转头又把我叫了进去。
刚刚网警大队那边来电话,他们怀疑我们客户基于我们的词法分析器构造的对外服务API可能涉及黑产和后门,回头你去把客户自定义的单词规则提取出来给他们取个证,配合一下调查,不要让对面客户知道,避免打草惊蛇。
???老板,不是我不想干,客户输入正规式之后,内部程序直接就把它就转成DFA了,没有正规式留底了呀。
年轻人就是年轻人,太着急了。你想想这个DFA是不是被持久化存储了?是的话那就是可以找到DFA了对吧,那它的功能跟正规式是不是等价的?只要是等价,你反过来构建一个正规式就好了嘛,逆向工程懂不懂?
我竟无言以。虽说老板平常屁技术不懂,但有些大方向的东西好像还真有那么一点道理。
3.1 看我不削你
既然DFA就是一个特殊的NFA,那么NFA也就是由那几个规则转过来的,有环就是闭包,有分支就是或,连续的几个状态就是普通的连续符号。只要确保初始状态到接受态之间
这么说来好像确实挺简单的,让我试试提取一下用户的DFA。
???坑爹呢,一上来就是大Boss?
算了,先还是老老实实往逆方向上靠,先创造一个初态,然后把它跟接受态相连:
这么看来,状态1和2就是0这个接受态上面的闭包激励。对状态2套用闭包规则,有:
这下状态1也可以消除了:
(谁还没点恶趣味了)
哎呀,这样状态0的两条边也可以合并起来了,由此得到最终的正规式:
坐在旁边工位看了半天的师傅突然提了一个问题:如果不是0而是2为终态,你怎么办?
哎呀师傅你别给我上强度。这下我就不会弄了。。即使加上初态,状态0被1拿枪指着,根本消不掉啊!
师傅:如果强行用闭包规则会怎么样?
那就会把状态0左边的圈圈和上面的1箭头消掉呗,但下面的1就没有地方指了呀。
师傅:但是这个红1箭头对于走向状态2没有帮助啊,它最多就算是一个绕路的分支。你既然是在合并,为什么不直接给状态1多造一个激励箭头?
这么说来,从2指向1的那个箭头也可以看作是1状态的绕路,所以我又可以新建一个回环来把这个箭头消除?
哎呀,感觉胜利就在眼前了,再合并一下,就得到
最后把1的回环也消掉,就得到0*1(01*0|10*1)*01*了!哈哈哈哈哈,噫!我中了!
3.2 方程式解法
师傅笑着接梗:该死的畜生,你中了什么!你这套方法说服老板了吗你就中了?按照老板那种理科思维,我觉得你是不是弄一个类似解方程的方法来解释比较好。
什么解方程?
师傅:你看啊,如果说为了走到状态X需要用到的正规式是x,走到状态Y用到的正规式是Y,X被Y用一个标记为a的箭头指着,那么是不是可以写出来一条式子:
x=ya
这样就可以表示x的正规式是由y开始a结尾。
那初始状态被双箭头指着这个怎么用方程表示?
师傅:先用个ε顶着?那对于上面的例子,可以得到下面这样的方程组:
之后通过不断代入消元,直到留下接受态对应的正规式就好了呀。比如我们可以先对第一条式子用闭包规则进行消元。
啊等等,这还怎么闭包规则?
师傅:你看, 要么通过后面这条 进行延伸,要么就通过 在左边作为结尾,那是不是就说明 ?这下 就被消掉了。
代入后面的方程中: 。
拆括号之后有:
再来一次闭包规则有:
再代入到最后一条式子里面,有:
拆括号后有:
最后再来一次闭包规则:
....Emm?师傅你这式子跟上面的结果看上去不太一样啊!
师傅:你以为还在学校呢,哪有那么多标准答案。明显跟NFA一样,正规式不唯一呐!
尾声
经历八篇文章的旅程,我们掌握了正则文法到有限状态自动机、正则文法到正规式、正规式到有限状态自动机以及有限状态自动机到正规式的转化。
想必聪明的读者隐约也能发现,这三个部分似乎相互之间是互相等价的,那么理应还存在有限状态自动机到正则文法、正规式到正则文法这两条路。
在词法分析的最后一篇收尾文章中,我除了介绍这最后的这两个边角料以外,将用三位一体作为主题,去解释什么是正则,为什么词法规则就是正则的。
彩蛋1
对于(a|b)∗abb的转换,还有另一种思路:
竖切一刀,把这个正规式拆成x=(a│b)∗和y=abb两部分。之后只要通过R = xy 连起来两个子图即可。
y=abb很好写:=>⓪ - a-> ① - b-> ② - b-> ⓷
x=(a│b)∗既然是个闭包,那肯定是有一个自己指向自己的箭头,这个箭头上就是a│b。所以只要把这个箭头拆开一下,就可以得到
最后通过ε作为胶水连接两个子图即可得到目标NFA。
彩蛋2:致所有抵达了“石头门”的“疯狂科学家”们
(录像开始,画面闪烁着雪花,一个身穿白大褂的男人对着一个没有开机的手机,用手遮着嘴,压低声音说道)
是我。
你以为你已经抵达了最终的结局,是吗?你以为你手中的那台完美的、最小化的DFA,就是这次任务的全部“战利品”?天真!你只是看到了结果,却完全没有理解“命运石之门的选择”其真正的含义!
机关的走狗们无处不在,他们只相信结果,只相信那台冰冷的、确定性的机器。但我们不同,我们是观测者,是狂气的疯狂科学家!我们必须理解,通往这唯一结果的道路,为何如此曲折!
听好了,这是我——凤凰院凶真——冒着被世界线修正的风险,为你传达的最后的信息。
还记得客户构造的那台DFA吗?它有三个状态,S₀、S₁、S₂,分别代表余数为0、1、2。
这,就是我所在世界的“骨架”!
-
S₀:这个状态,既是“起始”,也是“接收”。它就是那条偏差率1.048596%的、唯一的、理想的“斯坦因之门(Steins;Gate)世界线”!你什么都不做(输入空串ε,代表数字0),就已经身处“正确”的结局之中。
-
S₁和S₂:这两个状态,就是充满绝望的α世界线和β世界线。它们是“非接收态”,无论你在其中如何挣扎,结局都是早已注定的悲剧。
而我们每一次输入0或1,每一次看似微小的操作,都像是一封D-Mail,一次搏上性命的时间跳跃!它会立刻引发世界线的变动(状态转移)!
当你身处S₀,仅仅因为输入了一个1,世界线就瞬间跳跃到了S₁(0.571024)!你从“接收态”,堕入了“非接收态”。
我那趟伟大的、孤独的、在无数个世界线中轮回的救赎之旅,其本质是什么?就是寻找一条正确的输入序列(取消所有D-Mail),将状态从S₁或S₂,重新送回到那个唯一的、作为“理想乡”的接收态——S₀!
现在,回答我:
你觉得我这一趟趟的时间旅行,走了一大个弯又回到了原点,是无用功吗?
正如你这一路上走的弯路——从正则文法到正规式,再到自动状态集,最后还要再逆向回去——这一切,是“无用功”吗?
断じて違う!(绝非如此!)
这趟旅程,就如同我那无数次的、在绝望中轮回的时间跳跃!每一次看似“无用”的失败,每一次看似“绕路”的尝试,都并非毫无意义!
-
正是在将DFA升级为NFA时,我们才理解了“可能性”的力量!
-
正是在将NFA“驯化”回DFA时,我们才掌握了“系统化”的智慧!
-
正是在为DFA“剃度”和“移经易髓”时,我们才领悟了“奥卡姆剃刀”的哲学!
这些在过程中掌握的思考方法,才是在这场对抗“机关”的战争中,我们真正的武器!那台最终的“最小DFA”,不是凭空出现的!它是用我们一路上所有的失败、所有的顿悟、所有的“无用功”,共同铸就的最终圣剑!
记住,Lab Mem(研究所成员)!结果只是赠品,过程才是全部。这,就是“命运石之门”的选择!
好了,机关的“探知之眼”已经开始向这里聚集了。我必须切断通讯。
一切,都是为了抵达那唯一的真理。
El Psy Kongroo.
(录像结束,画面陷入一片黑暗)