6. 无限域函数,自动机与正则表达式
- 在 长度无界 的输入上定义函数,这种函数无法用一个大小有限的、由输入和输出构成的表格描述
- (前者)与语言的成员资格判定任务的等价性
- 确定性有穷自动机(可选):一个无界计算模型的简单案例
- (前者)与正则表达式的等价性
布尔电路的模型(或者说,NAND-CIRC编程语言)有一个非常明显的短板: 一个布尔电路只能计算一个 有限的 函数。事实上,由于每个门配有两个输入,大小为的电路至多能计算长度为的输入。
因此该模型无法捕捉到这样一种直观概念:算法可以视作对潜在的无穷函数进行的 统一处理 。
比方说,标准的小学乘法算法是一种 统一 算法,它可以对所有长度的数进行乘法运算的。然而,这种算法无法被表达为单一的电路,而是需要对每种输入配备一个不同的电路(或者说,NAND-CIRC语言)。(见图 6.1)
本章拓展了计算任务的定义,使其考虑配备 无界 定义域的函数。 其重点在于定义计算 哪些 任务,将 如何 计算的绝大部分留给之后的章节。其中将会认识到 图灵机 与其他在无界输入上进行计算的计算模型。 然而,这一章将认识到一个简单且受限的计算模型——确定性有穷自动机(DFAs)。
本章将会讨论以任意长度字符串作为输入的函数,其中主要关注 布尔 函数这种特例,其输出为单个位。 除此之外仍然有无数多个输入长度无界的函数。因此这一的函数不能被任何一个单一的布尔电路计算。 这个章节的第二部分将会讨论 有穷自动机 ,这种计算模型可以计算一个输入长度无界的函数。 确定性有穷自动机不像Python或其他通用编程语言一样强大。但它可以作为这些更加通用的计算模型的一个引子。 本章将会展示一个美妙的结果——能被有穷自动机计算的函数与能被 正则表达式 计算的函数精确地一致。 然而,读者仍然可以自由跳过自动机的部分,直接转向第七章中对于 图灵机 的讨论。
6.1 输入长度无界的函数
直到现在,我们考虑的计算任务都将某些长度为的字符串映射为某个长度为的字符串。
然而,一般情况下的计算任务都会涉及到长度无界的输入 例如,接下来的Python函数会计算一个函数 其中 为 当且仅当中的数量为奇数。
(换言之,对每个 ,) 虽然简单,却无法被一个布尔电路计算。相反,对每个,都需要通过不同的电路计算(函数在的限制)(e.g. 见图 6.2)。
def XOR(X):
'''接受一个0与1的列表X
当1的个数为奇数时输出1
否则输出0'''
result = 0
for i in range(len(X)):
result = (result + X[i]) % 2
return result
计算位异或的NAND电路与NAND-CIRC程序。值得注意的是的电路仅仅只是重复了四次计算位异或的电路。
这本书的前面部分研究了 有限 函数的计算。这样一种函数总是能通过列举所有的输入所对应的个函数值来表示。本章考虑像这样输入长度无界的函数。
尽管能用有限多个符号来描述(事实上在上面已经做过了),它却能接受无穷多种可能的输入,因此无法把它所有的函数值都写下来。 这对其他蕴含着其他重要计算任务的函数也是同理,包括加法,乘法,排序,在图上寻找路径,由点拟合曲线,等等。
为了和有限情况作区分,有时将函数(或)称为 无限的 。 然而,这不意味着可以接收一个无限长的输入。 它仅仅表明可以接收任意长的输入,因此无法简单地把在一个表上把不同输入下的全部输出都写下来。
{{idea}}{idea:61}[思路6.1] 函数指明了一个将输入映射到的计算任务.
如前所述,不失一般性的前提下,我们可以把注意力限制在输入和输出为二进制串的函数。因为其他的对象,像数字、列表、矩阵、照片、视频、以及别的种种,都可以用二进制串编码。
如前所述,有必要区分 规范 和 实现 这两个概念 。例如,考虑以下函数。
在数学上,这是一个良定义的函数。对每个都会有一个非即的函数值。然而,截至目前,尚未已知能计算该函数的Python程序。孪生素数猜想主张对每个都有一个使得均为素数。
如果该猜想成立,那么(译者注:此处应指)很容易计算—— def T(x): return 1
是一个奏效的程序。
然而,自1849年起,数学家们对该猜想的证明均无功而返。
这说明,不论知不知道函数的 实现 ,上面的定义提供的都是它的 规范 。
6.1.1 改变输入和输出
许多有趣的函数都接受不止一个输入,例如函数:
接受一个二进制表示的整数对,并输出积的二进制表示. 然而,因为一对字符串能被表达为一个单一的字符串,所以像这样的函数,可以被视为从到的映射。 一般不考虑底层细节,比如把一对整数精确地表达为串的方式,因为近乎所有的选择对我们的目标而言都是等价的。
我们想计算的另一个函数是
以一个单个位作为输出。以一个单个位为输出的函数成为 布尔函数 。 布尔函数是计算理论的中心,因此将在这本书中经常性地被讨论。 需要注意的是,即使布尔函数只有一个单一位用于输出,其输入可以是任意长度的。 因此它们仍然无法通过一个由函数值组成的有限表格描述,因此仍然是一个无限函数。
“布尔化”函数 。有时从一个非布尔函数中构造一个布尔函数的变体是非常方便的。 例如,下列函数是的一个布尔函数变体:
如果能够通过例如Python,C,JAVA等任何一门编程语言计算,也可以计算,反之亦然。
解答6.1
解答6.1
对每个函数 可以定义。
其输入满足 ,而输出为的第i位(如果且)。
如果,则当且仅当时为,通过这一点可以计算的长度。 从出发计算是十分直接的。 另一方面,给定一个计算的Python函数,可以通过如下方法计算。
def F(x):
res = []
i = 0
while BF(x,i,1):
res.append(BF(x,i,0))
i += 1
return res
6.1.2 形式语言
对每个布尔函数,可以定义集合 。这样的集合被称为 语言 。 这个名字源于 形式语言理论 ,像Noam Chomsky这样的语言学家致力于该理论。。 一个 形式语言 是(更一般地说,其中是一个有限的字母表1)。 一个语言上的 成员资格问题 或 判定问题 ,是断定对于给定的,是否有。 如果能够计算函数,也就能够判定语言的成员资格,反之亦然。 因此,许多像Sipser,1997这样的教材都将计算一个布尔函数的任务称为“判定一个语言” 本书主要用 函数 的记号来描述计算任务,这种方法更容易推广到不止一位输出的计算任务。 然而,因为语言的术语在文献中更加流行,有时也会提到它们。
6.1.3 函数的限制
如果是一个布尔函数而,则在输入长度为上的限制记作,是一个有限函数使得对每个均有。 这就是说是定义在上的有限函数,但在这些输入上与保持一致。 因为是一个有限函数,所以它可以被一个布尔电路计算。以下定理表明了这一点。
特别地,定理6.1表明甚至对于前面描述过的函数,这样的电路族也存在,即使尚未已知的程序可以对其进行计算。 这实际上并不令人惊讶:对每个特定的,要么是常0函数要么是常1函数,其中任何一者都可以用一个简单的布尔电路计算。 因为计算的电路族一定存在,用Python或其他任何编程语言计算的难度源于这样一个事实——我们不知道对每个特定的,电路族中的应该是什么。
6.2 确定性有穷自动机(可选)
我们目前所有的计算模型——布尔电路和无分支程序——都只对 有限 函数有效。
在第七章中,将会介绍 图灵机 ,这是输入长度无界函数的中心计算模型。 然而,本节将会介绍一个更加基本的模型—— 确定性有穷自动机 (DFA)
自动机可以视作通往图灵机的一个优秀的垫脚石,尽管它们在这本书的后面部分并不会大量地被用到,所以读者可以自由跳过到第七章.
DFA在能力上与 正则表达式 是等价的:正则表达式是识别模式的一个强力工具,在实践中广泛应用。 本书对自动机的处理是相对简略的。有大量的资源可以帮助你更加熟悉DFAs。 详细地说,第一章中Sisper的著作Sipser, 1997包含对这个内容的绝佳的说明。 这里有许多的在线自动机模拟器网站,也有将自动机和正则表达式互化的翻译器。 (例如此处和此处).
从高视角上看,一个 算法 是通过以下步骤的组合从输入计算输出的方法:
- 从输入读入一位
- 更新 状态 (工作记忆)
- 停止并产生一个输出
例如,回忆以下计算函数的Python程序
def XOR(X):
'''接受一个0与1的列表X
当1的个数为奇数时输出1
否则输出0'''
result = 0
for i in range(len(X)):
result = (result + X[i]) % 2
return result
每一步中,程序读入一个位X[i]
并且根据它更新自己的result
状态(在X[i]
为1时翻转result
,否则保持原样)。当它遍历完输入后,程序输出result
。
在计算机科学中,这样一个程序称为 单遍常数内存算法 ,因为它只遍历一次输入,而它的工作记忆是有限的。
(事实上,在这个案例中,result
非即)
这样一个算法称为 确定性有穷自动机 或 DFA (DFAs的另一个名字是 有限自动机 )。
我们可以把这样一种算法视作一个拥有个状态的“机器”,其中为常数。
这样一种机器从某个初始状态开始,然后从输入中一次读取一个位
只要这个机器读入了一个位,它就会根据和先前的状态转换到一个新的状态。
机器的输出决定于最终状态。每个单遍常数内存算法都和这样一个机器一致。
如果这个算法使用了位内存,那么其内存中的内容就能用一个长度为的串表达。因此对于这样一个算法的任意一个执行点,其都在至多个状态之中。
我们可以通过一个条规则的列表来指明一个拥有个状态的DFA2。 每条规则都有这样的形式:“如果DFA位于状态,读入的输入位为,则新状态为”。 在计算的最后,会有一个具有形式“如果最终状态为下列中的一者 … 则输出,否则输出”的规则。 举例而言,上述的Python程序可以用一个两个状态的自动机来计算:
- 初始化为状态。
- 对每个状态和读取的输入位,如果则将状态转移为,否则停留在状态。
- 最终当且仅当时输出。
我们也可以用一个带标号的个顶点的 图 来描述个状态的DFA。随每个状态和位,我们添加一条带有标号的从到的有向边,使得若DFA位于状态且读入,则DFA转移到状态。(如果状态不变,则这个边是一个指向原状态的圈;相似地,如果在和两种情况下都转移为状态,则图上会有两条平行的边)同时也会标明在最后使自动机输出的状态集。这个集合称为 接受状态 集。
图 6.3给出了XOR自动机的图形表示
形式化地讲,一个DFA由 (1) 条规则构成的表格,该表格用 转移函数 表示。将状态和位映射到状态。DFA将会在输入下从状态转移到;和 (2) 接受状态集
确定性有穷自动机可以通过几种等价的方法定义。
特别地,Sisper在Sipser,1997将DFA定义为五元组,其中为状态集,为字母表,为转移函数,是初始状态,为接受状态集。
该书中状态集总是如下形式而初状态总是,但这对这些模型的计算能力没有影响。 因此,我们将注意力局限在字母表与相等的情况。
解答6.2
解答6.2
当要求构造一个DFA时,可以首先通过更加一般的、形式化的方式,来构造一个单遍常数内存算法,这通常是有效的。(例如使用伪代码或者一个python程序)。一旦得到了这样一个算法,就可以机械式地将其翻译为一个DFA。 以下是计算的一个简单Python程序:
def F(X):
'''当且仅当X是零个或多个[0,1,0]的拼接时返回1'''
if len(X) % 3 != 0:
return False
ultimate = 0
penultimate = 1
antepenultimate = 0
for idx, b in enumerate(X):
antepenultimate = penultimate
penultimate = ultimate
ultimate = b
if idx % 3 == 2 and ((antepenultimate, penultimate, ultimate) != (0,1,0)):
return False
return True
既然我们维护了三个布尔变量,工作记忆就可以是种配置中的一个,因此上述程序可以直接翻译为一个状态DFA。 尽管这对解决问题没有必要,通过检查结果DFA,会发现可以通过合并一些状态得到一个状态自动机,该自动机在图 6.4中描述。图 6.5中描述了在一个特定输入上这个DFA的运行。
对自动机的剖析(有限vs无界)
既然我们已在考虑输入长度无界的计算任务,将算法中拥有 固定长度 的组件,和大小随输入增长的组件区分开,是非常关键的任务。对于DFAs而言,要分类的是下列部分:
固定大小组件: 给定一个DFA ,下列量是固定的,与输入大小无关:
- 中 状态 数。
- 转移函数 (有种输入,因此可以用一个行的表格描述,每一项都是中的一个数字)。
- 接收状态集。该集合可以用一个中的串描述,以指明哪些状态位于中而哪些没有。
以上这些意味着,可以通过有限多个符号完全地描述一个自动机。这是我们要求的任何一种“算法”的概念都拥有的一个共同性质:我们应当能够写下如何从输入生成输出的完整规范。
无界大小组件: 以下关于DFA的量不以任何常数作为上界。需要强调的是,对于任何给定的输入,它们仍然是有限的。
- 提供给DFA的输入的大小。输入长度总是有限的,但是不能预先设定上界。
- DFA执行的步数可以随输入长度而增长。事实上,DFA进行单次便利,因此对于一个输入,它精确地执行步。
图 6.4中DFA的执行过程。状态数和转移函数的大小是有界的,但是输入可以是任意长的。
如果DFA位于状态且读取值,则其转移到状态。在执行的最后,当且仅当最终状态位于时DFA接受该输入。
DFA可计算函数
如果有一个可以计算,就称一个函数是 DFA可计算的 。 在第四章中,我们发现每个有限函数都可以被某些布尔电路计算,因此,在此刻,你可能会希望每个函数都可以被 某些 DFA计算。 然而,有很多并 不是 这种情况。我们马上就会发现一些简单的,却无法被DFA计算的无限函数。但对于初学者,我们先证明这样的函数是存在的。
证明
证明
每个DFA都能用一个表示转移函数和接收状态集的串描述,而每个DFA 都计算 某些 函数。 因此可以定义如下函数 其中是对于所有输入,其均输出的常函数(也是中的一个函数)。 因此根据定义,每个中的函数都可以被 某些 自动机计算,而是从到的满射,这就意味着可数。(见节 2.4.2)
因为 所有 布尔函数的集合是不可数的,所以有如下推论:
证明
证明
如果每个布尔函数都可以被一些DFA计算,那么就与集合(所有布尔函数的集合)相等。但根据定理2.12,后者不可数,又与定理6.4相矛盾。
6.3 正则表达式
搜索 一段文本是计算中的一个常见任务。从本质上说, 搜索问题 非常简单。
我们有一个串集(例如硬盘上的文件,或数据库中的学生记录),而用户想要找到一个所有被某些模式 匹配 的构成的子集。
(例如,所有名称以串.txt
结尾的文件)
在最一般的情况下,我们允许用户通过指定一个(可计算的) 函数 来指明模式,其中与的模式匹配相一致。
这就是说,用户提供一个用像 Python 这样的编程语言编写的 程序 ,而系统返回所有使的。
举例而言,我们可以搜索所有包含串important document
的文本文件,或是(让与一个基于神经网络的分类器相一致)所有包含猫的图片。
然而,我们希望系统不会为了尝试求程序的值,而因此陷入死循环!
因此,典型的搜索文件和数据库的系统 不 允许用户用功能齐全的编程语言来指定模式。
相反,这样的系统使用 受限计算模型 。这种模型一方面 足够丰富 ,可以捕捉许多实践中需要的查询(例如,所有以.txt
结尾的文件名,或者所有形如(617)xxx-xxxx
的电话号码),但另一方面受到的 限制 又足够大,使大型文件中的查询变得非常高效,并避免其陷入死循环。
这种计算模型中最流行的一种是正则表达式。如果你使用过一个高级的文本编辑器,一个命令行终端,或者进行过任何种类的、对文本文件的大批量操作,那么你很有可能对正则表达式有所耳闻。
在字母表上定义的 正则表达式 由上的元素通过连接操作,操作(与 或 一致)和操作(与重复零到多次一致)组合而成。 举例而言,接下来的正则表达式在字母表上定义,并与所有使每个数位重复至少两次的串所构成的集合一致:
下列正则表达式定义在字母表上,并与所有这样的串形成的集合一致——该串由两个序列连接:第一个序列由至少一个-的字母形成;第二个序列由至少一个数位形成(无前导零)。
形式化地说,正则表达式由以下递归定义所定义:
在能从上下文中推断出来时,我们也会忽略括号。我们也使用或运算和连接运算左结合的惯例,并且给运算最高的优先级,然后是连接,最后是或。 因此,举例来说,我们写的是而不是。
每个正则表达式都与一个函数一致,其中若 匹配 正则表达式,则。 举例说,若 则 而 (你知道为什么吗)
上述的定义本身并不是什么难事,但很麻烦。所以你应该在此处停下并再看一次上述定义,直到你理解为什么该定义与我们对正则表达式的直观概念是相一致的。这不仅对理解正则表达式本身(在许多应用中经常使用)很重要,对更好地理解一般的递归定义也一样。
若一个布尔函数在输出时,所有的输入串都能够被某些正则表达式匹配,就说这个布尔函数是“正则的”。
令而使得当且仅当是一个或多个-组成的序列接上一个或多个数位组成的序列(无前导零) 则就是一个正则函数,因为,其中
即式6.1
举例而言,如果要验证,注意到匹配,匹配,匹配, 匹配。其中这些式子又可以被归结为一些更简单的表达式。例如匹配,因为和被表达式所匹配。
正则表达式可以在任意有限字母表上定义。但是和之前一样,我们主要关注 二进制情况 ,其中。绝大部分(如果不是所有的话)关于正则表达式的理论和实践的真知灼见都可以从研究二进制情况得到。
6.3.1 匹配正则表达式的算法
除非能计算以下问题,否则正则表达式在搜索方面并不会很有用:给定一个正则表达式,串是否被匹配。幸运的是,这样一个算法存在。 准确地说,存在一个算法(你可以想成“Python程序”,尽管稍后就会用 图灵机 来形式化算法的概念),该算法输入一个正则表达式和串,当且仅当匹配时输出(即,输出)
实际上,定义6.7已经指明了一个 计算 的递归算法。准确地说,操作——连接,或,星号3——可以被视作这样一个过程:对测试某个表达式是否匹配的任务,将其归约到测试的某个子表达式是否匹配的某个子串。因为这些子表达式总是比原式短,所以这个判定是否匹配的递归算法最终会在最基础的表达式上停止:与空串或者当个符号一致。
以上代码假定已经编写了一个过程,其当且仅当匹配空串时输出。
一个关键的观察结果为,在对正则表达式的递归定义中,无论是由一个还是两个表达式组成的,这两个正则表达式都比 小 最终(当其长度为)时,它们一定和单个字母的非递归情形一致。 相应地,算法6.10中的递归调用总是和一个更短的表达式或者(在表达式具有形式的情况下)一个更短的输入串相一致。 因此,当输入具有形式时,通过在上做递归,可以证明算法6.10的正确性。 归纳奠基是或为单独的一个字母,或。 在表达式具有形式或时,用更短的表达式做递归调用 在表达式具有形式时,在一个更短的字符串与同样的表达式,或更短的表达式与一个字符串上做递归调用,其中的长度小于等于。
解答6.3
解答6.3
可以通过以下观察结果给出这样一个递归算法
- 具有形式 或的表达式总是匹配空串
- 具有形式 ,其中是一个字母,不匹配空串
- 正则表达式不匹配空串
- 具有形式的表达式当且仅当或匹配空串时才匹配
- 具有形式的表达式当且仅当和都匹配空串时才匹配
根据以上的观察结果,可以给出下列算法来判断是否匹配空串
算法 6.2 (算法6.11). 匹配空串5
6.4 高效匹配正则表达式(可选)
算法6.10并不高效 举例而言,给定一个包含连接或“*”操作的表达式和一个长度为的串,它需要次递归调用。因此,在最劣情况下,算法6.10花费的时间是输入串长度的 指数 级别。 幸运的是,有快得多的算法可以在 线性 时间(即)内匹配正则表达式。 鉴于还没提到时间和空间复杂度的话题,我们将像在编程入门课程和白板编程面试中做的那样,不给出计算模型,而使用高级术语描述这个算法,其中使用的运行时间的概念是口语化的。 我们将会在第13章中介绍时间复杂度的形式化定义
定理6.12中术语所隐含的常数取决于表达式 因此,另一个描述定理6.12的方法是对于每个表达式,都会有一个常数和一个算法使得在位输入上计算最多需要步 因为在实践中,通常希望对一个短的正则表达式和大的文档计算,所以这是有意义的。 定理6.12告诉我们,可以在运行时间随文档大小线性增大的情况下计算,即使运行时间可能更依赖于正则表达式的大小
我们通过给出一个高效的递归算法来证明定理6.12。该算法将判定是否匹配串的任务归约到判定相关表达式是否匹配。 该算法使得表达式的运行时间拥有形式解得。
正则表达式的限制:定理6.12背后的算法,其中心定义是正则表达式的 限制 的概念 其思想为:对每个正则表达式和字母,有可能定义一个正则表达式使得匹配当且仅当匹配匹配串。 例如,如果是正则表达式(即出现一次或多次),那么与等价而为。(你能发现是为什么吗)
算法6.13计算给定正则表达式和字母的限制。 该算法总会结束,因为其递归调用时传递的表达式总比输入的表达式小。 其正确性可以通过对正则表达式的长度进行归纳证明,归纳奠基是为,,或一个单独的字母时。
通过限制的概念,可以定义如下匹配正则表达式的递归算法
根据限制的定义,对于每个和,表达式匹配当且仅当匹配。 因此对每个和,和算法6.14确实给出了正确的结果。 剩下的唯一任务就是分析其 运行时间 。 需要注意的是,算法6.14在归纳奠基时使用已解答练习 6.3中的过程。 然而,因为这个过程的运行时间只依赖于,与原输入的长度无关,所以没有问题。
简单起见,我们将注意力限制在字母表与相等的情况。 定义为,给定最大符号数,输入定义在上的符号数不超过最大符号数的正则表达式,算法6.13所能进行的最大操作次数。 可以发现的值是关于的多项式。然而这对我们的定理并不重要,因为我们只关心计算时运行时间对长度的依赖而不关心其对长度的依赖。
算法6.14是输入表达式和串的递归算法。其计算过程为在最多运行后,以某些表达式和长度为的串为输入调用自身。 它将在步运行后结束,此时它到达一个长度为的串。 因此,对长度为的输入,用算法6.13计算的运行时间满足以下递归方程:
(在归纳奠基时,是某个只与有关的常数。)
为了对式6.2有直观印象,我们展开一层递归,将写作
如此继续,可以发现,其中是这么做时会遇到的最长的表达式的长度。 因此,如下声明足以说明算法6.14在运行时间是:
对上述声明的证明
对上述声明的证明
对于一个定义在上的正则表达式和,我们用来指代表达式,其通过将限制在上,再是,以此类推得到。 令。 通过说明对每个,集合是有限的,因此也一样,其为中的最大长度,从而证明该声明。
我们通过在的结构上做归纳证明这一点。如果是符号,空串,或者空集,则可以直截了当地说明能含有的最多的表达式就是只有这个表达式本身,和。对其余情况,我们分为两类:(i) 和 (ii) ,其中是更小的表达式(因此根据归纳假设和有限).
在情况 (i) 中,若则要么等于要么在时为空集合。因为在集合中,所以中不同表达式的个数最多为。
在情况 (ii) 中,若,则在串上的所有限制要么具有形式,要么具有形式,其中为使得成立的串,其中 匹配空串。
因为 和 ,所以具有形式的可能不同的表达式的数量最多有个。这就完成了对该声明的证明。
最重要的是,在一个正则表达式上运行算法6.14时,会遇到的所有表达式都在有限集中,不论输入多大。因此算法6.14的运行时间满足等式,其中是依赖于的常数。 最终解得,O记号中隐含的常数可以(且将会)依赖于,并且,重要的是,不依赖于输入的长度。
6.4.1 用DFAs匹配正则表达式
定理6.12非常令人印象深刻,但是我们可以做得更好。 准确的说,不管有多长,都可以通过维护一个常数大小的内存并进行对的 单次遍历 来计算。 也就是说,这个算法将会从输入的开头扫描到结尾,然后判定是否被匹配。 在常见情况下,我们会尝试在巨大的文件或文档中匹配简短的正则表达式,这些文件或文档甚至没法整个装在电脑的内存里,此时这一特点尤为重要。 当然,如前所述,一个单遍常数内存算法仅仅就是一个确定性有穷自动机。 就像在定理6.17中将要看到的那样,一个函数能被正则表达式计算 当且仅当 它能被一个DFA计算。 我们从证明“仅当”开始:
算法6.16 匹配正则表达式
证明
证明
算法6.16判定给定的串是否被正则表达式所匹配。
对每个正则表达式,这个算法都有恒定数量的布尔变量(更准确地说,对每个有一个变量和。该算法利用了一个事实:对每个,都在中。) 其对输入串进行单次遍历。因此与一个DFA一致。
我们通过归纳输入长度来证明其正确性。 准确地说,我们将论证,在读入之前,对每个,变量与相等。
因为初始对每个,让所以的情况成立 对的情况,归纳法证明其成立。归纳假设表明对每个,都有。而根据集合的定义,对每个,和,位于中而。
6.4.2 正则表达式和自动机的等价性
回忆 以下,若存在某个正则表达式,布尔函数与相等,则称其为 正则的 。(等价地,若存在某个正则表达式,语言满足当且仅当时匹配,则称其为 正则的 )。下述定理是自动机理论的核心:
证明
证明
既然定理6.15已经证明了“仅当”方向,现在只需要证明“当”方向。 令为一个状态DFA,其计算函数,需要证明是正则的。
对每个,令为这样的函数:当且仅当DFA 从状态出发,读入输入后会到达状态,则其将映射到。 现在将要证明对每个都正则。这将证明该定理。因为根据定义6.2,等于对所有取或,其中。 因此一旦能够为每个具有形式的函数写出一个正则表达式,(通过使用操作)也就可以得到的正则表达式。
为了给出函数的正则表达式,现在从定义函数开始:对每个和,当且仅当自动机从出发接受输入后到达且 *所有的中间状态都在集合中 。(见图 6.7)
这就是说,尽管可能会在之外,当且仅当在输入(从出发)时自动机运行过程中永不进入之外的状态并在结束。 当时就是空集,因此当且仅当自动机在输入时直接从转移到而不经过任何的中间状态。 当时所有的状态都在中,因此。
现在通过归纳来证明这个定理,说明对所有和,正则。
对于 归纳奠基 ,对所有的,都正则,因为它可以被表示为表达式,,,或中的一个。
准确地说,若,则当且仅当为空串。 若,则当且仅当为单个字母且。
因此在这种情况中,与四个正则表达式,,和中的一个相一致,并取决于从转移到时读取的是或,还是仅为两个符号中的一者,或者都不是。
归纳步骤 :刚刚已经说明了归纳奠基,现在通过归纳法来证明一般情况。 归纳假设为对每个,都有正则表达式计算。 需要证明的是对每个,正则。 如果自动机从到时访问了中间状态,则其访问了第个状态零次或多次。
如果一个路径标号为,使得自动机从到,并且过程中不需要访问第个状态,则被正则表达式匹配; 如果一个路径标号为,使得使得自动机从到,并且过程中需要访问第个状态次,则可以将该路径视为:
- 首先,从到,期间访问的中间状态均位于。
- 然后,回到自身次,期间访问的中间状态均位于。
- 最后,从到,期间访问的中间状态均位于。
因此在该情况下,字符串被正则表达式匹配。(又见图 6.8) 因此可以使用以下正则表达式计算:
归纳步骤证明完毕,进而定理得证明。
若对于每个,均有与相一致的正则表达式,则可以得到一个与相一致的正则表达式。关键的观察结果在于,一个可能经过的状态均在中的,从到的路径,要么完全不通过——这种情况被所捕捉;要么从到,然后回到零或多次,最终从到——这种情况被所捕捉。
6.4.3 正则表达式的闭包性质
若和分别是被和计算的正则函数,则表达式计算函数 其定义为。 另一个说法是,正则函数族 在或运算下封闭 。 这就是说,如果和正则,则也一样。 定理6.17的重要推论是这个集合也在非运算下封闭
因为,引理6.18表明正则函数族在与操作下也同样封闭。进一步说,因为或,非,与是通用的基础运算,这个集合在与非,异或,和其它有限函数的运算下也封闭。 这就是说,我们有如下推论
6.5 正则表达式的限制与泵引理
正则表达式的高效匹配使其分外实用。通常来说,操作系统和文本编辑器都限制其搜索接口,不允许任意指明一个函数,并采用正则表达式,其原因就在此处。 然而,这种高效是有代价的。如我们所见,正则表达式无法计算所有函数。实际上,有很多简单(而且有用!)的函数无法被正则表达式计算。以下是一个样例:
引理6.20是如下结果的一个推论,该结果也被称为 泵引理 :
为了证明“泵引理”,我们观察一个串,正则表达式能够匹配它,并且比大得多。在这种情况下,的一部分一定会被具有形式的子表达式匹配,而这是唯一允许表达式匹配比其长的串的操作。如果我们考虑“最左”的、具有该形式的子表达式,并定义是被其匹配的串,我们就得到了泵引理需要的部分。
证明
证明
通过归纳表达式的长度可以形式化地证明该引理。
像所有的归纳证明一样,该证明会比较长,但在结尾给出符合我们直觉结果——我们一定在某处使用了闭包运算。阅读该证明,特别地,去理解以下的形式化证明如何与上面的直观思路相一致,是更好地熟悉该种归纳证明的好方法。
归纳假设为对于一个长度为的表达式,符合引理要求的条件。
归纳奠基 为当表达式为当个字母或者或。 在这些情况中引理显然成立,因为,而不可能有长度大于的串被该表达式匹配。
我们现在证明 归纳步骤 。令为有个符号的正则表达式,让且串满足。 既然有多于一个符号,则其具有下列形式之一: (a) :; (b) :; (c) 。在所有情况中,子表达式与的符号数都少于,因此符合归纳假设。
在情况 (a) 中,每个被匹配的串都被与中的一者匹配。若匹配,则根据归纳假设以及,有,其中与使得对每个,(因此也一样)匹配。当匹配时同理。
在情况 (b) 中,若被匹配,则有,其中匹配而匹配。 我们现在分类讨论。 若则根据归纳假设有满足,使得,且对每个有匹配。 如果我们令,则,且对于每个有匹配。 否则,若,又,则必定有。 因此根据归纳假设有使得,且对每个有匹配。 而我们现在令,则有。而另一方面对每个,表达式匹配。
在情况 **(c)**中,若被匹配,则,其中对每个,是一个被匹配的非空串。 若,我们可以用与上述连接运算情况相同的方法。 否则,注意到若是空串,且则且对每个,被匹配。
通过泵引理,我们可以轻易地证明引理6.20(即“括号匹配”函数的非正则性):
对于一个确定的函数,在说明该函数 不能 被正则表达式计算的方面,泵引理是一个有效的工具。 然而,这并 不是 正则性的“充分必要”条件:存在一个非正则的函数,其满足泵引理的条件。 为了理解泵引理,遵循定理6.21中量词的顺序是很关键的。 特别地,定理6.21所描述的数字取决于所选的正则表达式(上述证明选择了表达式所用符号数的两倍)。 所以,为了使用泵引理来排除计算某个函数的正则表达式的存在性,就需要能够选择一个合适的输入。它要能够任意地增大,并且满足F(w)=1。 如果你仔细思考泵引理后蕴含的直观,就会发现上述内容是很有意义的:足够大的才能强制性地要求使用闭包运算。
一个漫画,其内容是使用泵引理来证明不正则。泵引理宣称:如果正则,就一定会有一个数,使得对 所有 足够大的满足的, 存在 的一个划分满足特定的条件,使得对 所有 ,。你可以将一个基于泵引理的证明视作你和对手间的一场竞赛。每个 存在 量词都对应着你可以自由选择的对象(其基于先前选择的对象)。每个 全称 量词都对应着对手可以任意选择的满足条件的对象(并且也基于先前的选择)。一个有效的证明对应着无论对手做什么,你都可以取胜的策略。该策略通过构造一个矛盾来取胜。其是对的一个选择,使得成立,同时又使得泵引理的结论有效。
解答 6.4
解答 6.4
此处采用泵引理。 为了使用反证法,假设有一个正则表达式计算,令为泵引理(定理6.21)中的数。考虑串。因为全部由零组成的串的反转仍为全部由零组成的串,所以。 现在,根据选择引理,如果被计算,则可以写下使得,且对每个有。特别地,一定成立,但这就导致了矛盾,因为,所以其两部分并不一样长,所以并不是另一者的反转。
另一个基于泵引理的证明见图 6.10,这是一个关于函数非正规性证明的漫画,其中当且仅当存在使得(即,为一个连续零串拼接上一个同等长度的连续一串)。
6.6 回答正则表达式的语义问题
正则表达式有着除搜索之外的其他应用。 例如,在编程语言的 语法分析器 、 编译器 和 解释器 的设计中,正则表达式通常用于定义 词元 (例如一个有效的变量名,或者关键字)。 正则表达式还有别的应用:例如,近年来,互联网从固定的拓扑结构演化为“软件定义的网络”。 这样一个网络由可编程交换机进行路由,这些交换机实现了一些 策略 ,例如“如果包被SSL验证,则把它转发到A,否则转发到B”。 为了表示这样的策略,我们需要一种语言,它一方面足够丰富,可以捕捉我们需要实现的策略;另一方面又被充分地限制,从而可以在网络高速的要求下快速地执行它们,并能够回答像“C能否查看从A到B的包”这样的问题。
NetKAT网络编程语言通过正则表达式的一个变体来精确地实现这一点。 在这些应用中,我们不仅仅能够回答表达式能够匹配,同时也回答关于正则表达式的 语义问题 ,例如“表达式和是否计算同一个函数” 以及 “是否存在串被匹配?”
接下来的定理说明我们可以回答后者:
证明
证明
如果一个正则表达式计算的是常零函数,我们就定义其是“空的”。给定一个正则表达式,通过以下规则,我们可以判定是否为空:
- 若具有形式或,则其非空
- 若非空,则对所有的,均非空
- 若非空则非空
- 若与均非空,则非空。
- 为空。
通过这些规则,可以直接得出一个判定空性的递归算法。
通过定理6.23,我们可以得到判定两个正则表达式是否 等价 的算法。这意味着它们计算相同的函数。
证明
证明
我们从定理6.23中证明定理 6.24。(这两个定理实际上是等价的:我们很容易从定理 6.24中证明定理6.23,因为测试表达式空性和判定其与的等价性是一样的。)
对给定的两个表达式与,目标是计算表达式使得当且仅当。可以发现,与等价当且仅当为空。
我们从这样一个观察结果出发:对每个位 ,当且仅当
因此我们需要构造这样一个,其对所有的,均有
为了构造这个表达式,我们会说明对于任意一对和,我们可以构造表达式与,其分别计算和。(计算表达式是很直接的,只需使用运算)
特别地,根据引理6.18,正则函数在否运算下封闭。这意味着对每个正则表达式,均有表达式使得对所有均有。
于是,对于所有的两个表达式与,表达式 计算表达式的与运算。
给出了这两个变换,可以发现对所有的正则表达式与,都可以找到一个表达式满足式6.3,使得为空当且仅当与等价。
- 使用 无限 函数对输入长度任意的计算任务建模。
- 这样一种函数输入一个任意长(但仍然有限!)的串,而且不能被一个由输入输出构成的有限表格描述。
- 被称为 布尔函数 的一类特殊函数,其输出为单个位。计算该函数等价于判定一个 语言 。
- 确定性有穷自动机 (DFAs)是计算(无限)布尔函数的一个简单模型。
- 有一些函数无法被DFAs计算。
- DFAs可计算的函数族与正则表达式能识别的语言族相同。
6.7 习题
6.8 参考文献
正则表达式与有穷自动机的练习是一个优美的话题,本文中我们对其浅尝辄止。 (Sipser, 1997)(Hopcroft, Motwani, Ullman, 2014)(Kozen, 1997)中对该话题涉及更多。 这些文章也讨论了像 非确定有穷自动机 (NFA),以及上下文无关文法与下推自动机的关系。
图 6.4中的自动机由FSM simulator生成,作者为Ivan Zuzak和Vedrana Jankovic。
我们对于定理6.12的证明与Myhill-Nerode定理联系紧密。Myhill-Nerode定理的一个方向可以被陈述为:如果是一个正则表达式,则存在最多有限个串,使得对每个,有
1: 译者注:中的元素称为 字母 ,原著中提到其元素时使用的术语是字母表符号alphabet symbol
,翻译时为了简洁使用字母这一个更加简单的术语
2: 译者注:更准确地说,是条,但之后考虑的均为,因此
3: 译者注:准确的说法是闭包
4: 译者注:事实上,以上过程仅仅证明了算法6.10是会结束的,但是并没有证明正确性。但上面的过程确实给出了证明其正确性的骨架,因此剩下的工作繁而不难
5: 译者注:该算法并未要求输入串。此处应为作者笔误
6: 译者注:此处应为作者笔误,正确语句应当如下:且有一个非空子串被匹配,其中为的子串。