6. 无限域函数,自动机与正则表达式

学习目标

  • 长度无界 的输入上定义函数,这种函数无法用一个大小有限的、由输入和输出构成的表格描述
  • (前者)与语言的成员资格判定任务的等价性
  • 确定性有穷自动机(可选): 一个无界计算模型的简单案例
  • (前者)与正则表达式的等价性

Quote

“算法以有限回答无穷”

—Stephen Kleene

布尔电路的模型(或者说,NAND-CIRC编程语言)有一个非常明显的短板: 一个布尔电路只能计算一个 有限的 函数 事实上,由于每个门配有两个输入,大小为的电路至多能计算长度为的输入.

因此该模型无法捕捉到这样一种直观概念: 算法可以视作对潜在的无穷函数进行的 统一处理 .

比方说,标准的小学乘法算法是一种 统一 算法,它可以对所有长度的数进行乘法运算的. 然而,这种算法无法被表达为单一的电路,而是需要对每种输入配备一个不同的电路(或者说,NAND-CIRC语言). (见图 6.1)

multiplicationschoolfig

图 6.1. 一旦知道如何计算多位数乘法,就可以对所有位数这么做. 但如果你想用布尔电路或者NAND-CIRC程序来描述乘法,对所有长度为的输入,你都需要一个不同的程序/电路

本章拓展了计算任务的定义,使其考虑配备 无界 定义域的函数. 其重点在于定义计算 哪些 任务,将 如何 计算的绝大部分留给之后的章节. 其中将会认识到 图灵机 与其他在无界输入上进行计算的计算模型. 然而,这一章将认识到一个简单且受限的计算模型——确定性有穷自动机(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

xor5circprogfig

图 6.2. 计算位异或的NAND电路与NAND-CIRC程序. 值得注意的是的电路仅仅只是重复了四次计算位异或的电路. 这本书的前面部分研究了 有限 函数的计算. 这样一种函数总是能通过列举所有的输入所对应的个函数值来表示. 本章考虑像这样输入长度无界的函数.

尽管能用有限多个符号来描述(事实上在上面已经做过了),它却能接受无穷多种可能的输入,因此无法把它所有的函数值都写下来. 这对其他蕴含着其他重要计算任务的函数也是同理,包括加法,乘法,排序,在图上寻找路径,由点拟合曲线,等等.

为了和有限情况作区分,有时将函数(或称为 无限的 . 然而,这不意味着可以接收一个无限长的输入. 它仅仅表明可以接收任意长的输入,因此无法简单地把在一个表上把不同输入下的全部输出都写下来.

重要启示

重要提示 6.1. 函数指明了一个将输入映射到的计算任务.

如前所述,不失一般性的前提下,我们可以把注意力限制在输入和输出为二进制串的函数. 因为其他的对象,像数字、列表、矩阵、照片、视频、以及别的种种,都可以用二进制串编码.

如前所述,有必要区分 规范实现 这两个概念 . 例如,考虑以下函数.

在数学上,这是一个良定义的函数. 对每个都会有一个非的函数值. 然而,截至目前,尚未已知能计算该函数的Python程序. 孪生素数猜想主张对每个都有一个使得均为素数. 如果该猜想成立,那么(译者注:此处应指很容易计算—— def T(x): return 1是一个奏效的程序. 然而,自1849年起,数学家们对该猜想的证明均无功而返. 这说明,不论知不知道函数的 实现 ,上面的定义提供的都是它的 规范 .

6.1.1 改变输入和输出

许多有趣的函数都接受不止一个输入,例如函数:

接受一个二进制表示的整数对,并输出积的二进制表示. 然而,因为一对字符串能被表达为一个单一的字符串,所以像这样的函数,可以被视为从的映射. 一般不考虑底层细节,比如把一对整数精确地表达为串的方式,因为近乎所有的选择对我们的目标而言都是等价的.

我们想计算的另一个函数是

以一个单个位作为输出. 以一个单个位为输出的函数成为 布尔函数 . 布尔函数是计算理论的中心,因此将在这本书中经常性地被讨论. 需要注意的是,即使布尔函数只有一个单一位用于输出,其输入可以是任意长度的. 因此它们仍然无法通过一个由函数值组成的有限表格描述,因此仍然是一个无限函数.

“布尔化“函数 . 有时从一个非布尔函数中构造一个布尔函数的变体是非常方便的. 例如,下列函数是的一个布尔函数变体:

如果能够通过例如Python,C,JAVA等任何一门编程语言计算,也可以计算,反之亦然.

练习 6.1 (一般函数的布尔化).

说明对每个函数,都有一个布尔函数使得一个能够计算的Python程序可以被转移为一个计算的程序,反之亦然.

练习 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 (无限函数的电路族).

. 则有一个电路族使得对每个能够计算在输入长度为上的限制

定理 6.1的证明

这是布尔电路通用性的一个立即推论. 事实上,因为映射到,定理定理 4.8表明一定有一个布尔函数来计算它. 事实上,这个电路的大小为至多个门,其中为常数.

特别地,定理 6.1表明甚至对于前面描述过的函数,这样的电路族也存在,即使尚未已知的程序可以对其进行计算. 这实际上并不令人惊讶: 对每个特定的要么是常0函数要么是常1函数,其中任何一者都可以用一个简单的布尔电路计算. 因为计算的电路族一定存在,用Python或其他任何编程语言计算的难度源于这样一个事实——我们不知道对每个特定的,电路族中的应该是什么.

6.2 确定性有穷自动机(可选)

我们目前所有的计算模型——布尔电路和无分支程序——都只对 有限 函数有效.

第七章中,将会介绍 图灵机 ,这是输入长度无界函数的中心计算模型. 然而,本节将会介绍一个更加基本的模型—— 确定性有穷自动机 (DFA)

自动机可以视作通往图灵机的一个优秀的垫脚石,尽管它们在这本书的后面部分并不会大量地被用到,所以读者可以自由跳过到第七章.

DFA在能力上与 正则表达式 是等价的: 正则表达式是识别模式的一个强力工具,在实践中广泛应用. 本书对自动机的处理是相对简略的. 有大量的资源可以帮助你更加熟悉DFAs. 详细地说,第一章中Sisper的著作Sipser, 1997包含对这个内容的绝佳的说明. 这里有许多的在线自动机模拟器网站,也有将自动机和正则表达式互化的翻译器. (例如此处此处).

从高视角上看,一个 算法 是通过以下步骤的组合从输入计算输出的方法:

  1. 从输入读入一位
  2. 更新 状态 (工作记忆)
  3. 停止并产生一个输出

例如,回忆以下计算函数的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自动机的图形表示

xorautomatonfig

图 6.3. 一个计算XOR函数的有穷自动机. 其有两个状态 当它读入时,它从转移到

形式化地讲,一个DFA由 (1) 条规则构成的表格,该表格用 转移函数 表示. 将状态和位映射到状态 DFA将会在输入下从状态转移到(2) 接受状态集

定义 6.1 (确定性有穷自动机).

一个在上定义的个状态的确定性有穷自动机是一个对 其中 有限函数称为DFA的 转移函数 . 集合称为 接受状态 集.

为无限域上的布尔函数. 对于任意,定义且对任意,若有 则称计算函数

暂停一下

确保你没有混淆自动机的 转移函数 (定义 6.1中的与其所 计算 的函数(定义 6.1中的 前者是一个有限函数,指明了自动机所遵循的规则的表格; 后者是一个无限函数.

Info

备注 6.1 (其他教材中的定义).

确定性有穷自动机可以通过几种等价的方法定义.

特别地,Sisper在Sipser,1997将DFA定义为五元组,其中为状态集,为字母表,为转移函数,是初始状态,为接受状态集.

该书中状态集总是如下形式而初状态总是,但这对这些模型的计算能力没有影响. 因此,我们将注意力局限在字母表相等的情况.

Question

练习 6.2 (识别).

证明计算下列函数的DFA存在:

练习 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的运行.

DFA010afig

图 6.4. 一个仅在输入为零个或多个的拼接时输出的DFA. 状态既是初始状态又是唯一的接受状态. 表格表示了转移函数 它将当前状态和读到的符号映射到一个新状态.

对自动机的剖析(有限vs无界)

既然我们已在考虑输入长度无界的计算任务,将算法中拥有 固定长度 的组件,和大小随输入增长的组件区分开,是非常关键的任务. 对于DFAs而言,要分类的是下列部分:

固定大小组件: 给定一个DFA ,下列量是固定的,与输入大小无关:

  • 状态
  • 转移函数 (有种输入,因此可以用一个行的表格描述,每一项都是中的一个数字).
  • 接收状态集 该集合可以用一个中的串描述,以指明哪些状态位于中而哪些没有.

以上这些意味着,可以通过有限多个符号完全地描述一个自动机. 这是我们要求的任何一种“算法“的概念都拥有的一个共同性质: 我们应当能够写下如何从输入生成输出的完整规范.

无界大小组件: 以下关于DFA的量不以任何常数作为上界. 需要强调的是,对于任何给定的输入,它们仍然是有限的.

  • 提供给DFA的输入的大小. 输入长度总是有限的,但是不能预先设定上界.
  • DFA执行的步数可以随输入长度而增长. 事实上,DFA进行单次便利,因此对于一个输入,它精确地执行步.

DFA010executionfig

图 6.5. 图 6.4中DFA的执行过程. 状态数和转移函数的大小是有界的,但是输入可以是任意长的. 如果DFA位于状态且读取值,则其转移到状态 在执行的最后,当且仅当最终状态位于时DFA接受该输入.

DFA可计算函数

如果有一个可以计算,就称一个函数DFA可计算的 . 在第四章中,我们发现每个有限函数都可以被某些布尔电路计算,因此,在此刻,你可能会希望每个函数都可以被 某些 DFA计算. 然而,有很多并 不是 这种情况. 我们马上就会发现一些简单的,却无法被DFA计算的无限函数. 但对于初学者,我们先证明这样的函数是存在的.

定理 6.2 (DFA可计算的函数是可数的).

为全体使得存在一个DFA计算的布尔函数的集合. 则可数.

定理 6.2的证明思路

每个DFA都能用一个有限长度的串来描述,从而产生一个从的满射: 更准确地说,这个函数将一个描述自动机的串对应到计算的函数.

定理 6.2的证明

每个DFA都能用一个表示转移函数和接收状态集的串描述,而每个DFA 都计算 某些 函数 因此可以定义如下函数 其中是对于所有输入,其均输出的常函数(也是中的一个函数). 因此根据定义,每个中的函数都可以被 某些 自动机计算,而是从的满射,这就意味着可数. (见节 2.4.2)

因为 所有 布尔函数的集合是不可数的,所以有如下推论:

定理 6.3 (DFA不可计算函数的存在性).

存在一个布尔函数不能被 任何的 DFA计算.

定理 6.3的证明

如果每个布尔函数都可以被一些DFA计算,那么就与集合(所有布尔函数的集合)相等. 但根据定理2.12,后者不可数,又与定理 6.2相矛盾.

6.3 正则表达式

搜索 一段文本是计算中的一个常见任务. 从本质上说, 搜索问题 非常简单. 我们有一个串集(例如硬盘上的文件,或数据库中的学生记录),而用户想要找到一个所有被某些模式 匹配构成的子集. (例如,所有名称以串.txt结尾的文件) 在最一般的情况下,我们允许用户通过指定一个(可计算的) 函数 来指明模式,其中的模式匹配相一致. 这就是说,用户提供一个用像 Python 这样的编程语言编写的 程序 ,而系统返回所有使 举例而言,我们可以搜索所有包含串important document的文本文件,或是(让与一个基于神经网络的分类器相一致)所有包含猫的图片. 然而,我们希望系统不会为了尝试求程序的值,而因此陷入死循环! 因此,典型的搜索文件和数据库的系统 允许用户用功能齐全的编程语言来指定模式. 相反,这样的系统使用 受限计算模型 . 这种模型一方面 足够丰富 ,可以捕捉许多实践中需要的查询(例如,所有以.txt结尾的文件名,或者所有形如(617)xxx-xxxx的电话号码),但另一方面受到的 限制 又足够大,使大型文件中的查询变得非常高效,并避免其陷入死循环.

这种计算模型中最流行的一种是正则表达式. 如果你使用过一个高级的文本编辑器,一个命令行终端,或者进行过任何种类的、对文本文件的大批量操作,那么你很有可能对正则表达式有所耳闻.

在字母表上定义的 正则表达式上的元素通过连接操作,操作(与 一致)和操作(与重复零到多次一致)组合而成. 举例而言,接下来的正则表达式在字母表上定义,并与所有使每个数位重复至少两次的串所构成的集合一致:

下列正则表达式定义在字母表上,并与所有这样的串形成的集合一致——该串由两个序列连接: 第一个序列由至少一个-的字母形成; 第二个序列由至少一个数位形成(无前导零).

形式化地说,正则表达式由以下递归定义所定义:

定义 6.2 (正则表达式). 字母表上定义的 正则表达式 上的一个串,并具有下列形式之一

  1. ,其中
  2. ,其中为正则表达式
  3. 其中为正则表达式(当不会混淆时,通常省略括号并写为 )
  4. 其中为正则表达式 最终还有两个“边界条件“: and 这些正则表达式分别与不接受任何串和只接受空串一致.

在能从上下文中推断出来时,我们也会忽略括号. 我们也使用或运算和连接运算左结合的惯例,并且给运算最高的优先级,然后是连接,最后是或. 因此,举例来说,我们写的是而不是

每个正则表达式都与一个函数一致,其中若 匹配 正则表达式,则 举例说,若(你知道为什么吗)

暂停一下

的形式化定义是那种写比掌握麻烦的类型. 因此第一时间自己搞清楚其定义,再检查其是否与下列的定义相符,可能会更加简单.

定义 6.3 (匹配正则表达式).为字母表上的正则表达式 函数 定义如下:

  1. ,则当且仅当
  2. ,则,其中为或运算符.
  3. ,则当且仅当存在使得的连接,且时,
  4. ,则当且仅当存在使得的连接,且对每个,均有
  5. 最终, 对边界条件 是常函数, 而 只在输入空串时输出 对一个串,若上的正则表达式使,就说 匹配

暂停一下

上述的定义本身并不是什么难事,但很麻烦. 所以你应该在此处停下并再看一次上述定义,直到你理解为什么该定义与我们对正则表达式的直观概念是相一致的. 这不仅对理解正则表达式本身(在许多应用中经常使用)很重要,对更好地理解一般的递归定义也一样.

若一个布尔函数在输出时,所有的输入串都能够被某些正则表达式匹配,就说这个布尔函数是“正则的“.

定义 6.4 (定义6.8). 正则函数/语言 令为一个有限集,而为一个布尔函数. 若存在某个正则表达式,就称正则 的. 类似的,对每个形式语言,称是正则的当且仅当存在某个正则表达式使得当且仅当匹配

样例 6.1 (一个正则函数).使得当且仅当是一个或多个-组成的序列接上一个或多个数位组成的序列(无前导零) 则就是一个正则函数,因为,其中

(6.1)

举例而言,如果要验证,注意到匹配匹配匹配匹配 其中这些式子又可以被归结为一些更简单的表达式. 例如匹配,因为被表达式所匹配.

正则表达式可以在任意有限字母表上定义. 但是和之前一样,我们主要关注 二进制情况 ,其中 绝大部分(如果不是所有的话)关于正则表达式的理论和实践的真知灼见都可以从研究二进制情况得到.

6.3.1 匹配正则表达式的算法

除非能计算以下问题,否则正则表达式在搜索方面并不会很有用: 给定一个正则表达式,串是否被匹配. 幸运的是,这样一个算法存在. 准确地说,存在一个算法(你可以想成“Python程序“,尽管稍后就会用 图灵机 来形式化算法的概念),该算法输入一个正则表达式和串,当且仅当匹配时输出(即,输出

实际上,定义 6.3已经指明了一个 计算 的递归算法. 准确地说,操作——连接,或,星号3——可以被视作这样一个过程: 对测试某个表达式是否匹配的任务,将其归约到测试的某个子表达式是否匹配的某个子串. 因为这些子表达式总是比原式短,所以这个判定是否匹配的递归算法最终会在最基础的表达式上停止: 与空串或者当个符号一致.

算法 6.1 (正则表达式匹配).

以上代码假定已经编写了一个过程,其当且仅当匹配空串时输出

一个关键的观察结果为,在对正则表达式的递归定义中,无论是由一个还是两个表达式组成的,这两个正则表达式都比 最终(当其长度为时,它们一定和单个字母的非递归情形一致. 相应地,算法 6.1中的递归调用总是和一个更短的表达式或者(在表达式具有形式的情况下)一个更短的输入串相一致. 因此,当输入具有形式时,通过在上做递归,可以证明算法 6.1的正确性. 归纳奠基是为单独的一个字母, 在表达式具有形式时,用更短的表达式做递归调用 在表达式具有形式时,在一个更短的字符串与同样的表达式,或更短的表达式与一个字符串上做递归调用,其中的长度小于等于

4

练习 6.3 (匹配空串).

给出一个匹配空串的算法. 该算法输入为正则表达式,且满足当且仅当时输出

练习 6.3的解答

可以通过以下观察结果给出这样一个递归算法

  1. 具有形式 的表达式总是匹配空串
  2. 具有形式 ,其中是一个字母,不匹配空串
  3. 正则表达式不匹配空串
  4. 具有形式的表达式当且仅当匹配空串时才匹配
  5. 具有形式的表达式当且仅当都匹配空串时才匹配

根据以上的观察结果,可以给出下列算法来判断是否匹配空串

算法 6.2 (匹配空串).5

6.4 高效匹配正则表达式(可选)

算法 6.1并不高效 举例而言,给定一个包含连接或“*“操作的表达式和一个长度为的串,它需要次递归调用. 因此,在最劣情况下,算法 6.1花费的时间是输入串长度的 指数 级别. 幸运的是,有快得多的算法可以在 线性 时间(即内匹配正则表达式. 鉴于还没提到时间和空间复杂度的话题,我们将像在编程入门课程和白板编程面试中做的那样,不给出计算模型,而使用高级术语描述这个算法,其中使用的运行时间的概念是口语化的. 我们将会在第13章中介绍时间复杂度的形式化定义

定理 6.4 (在线性时间内匹配正则表达式).

给定一个正则表达式,则存在时间的算法计算

定理 6.4术语所隐含的常数取决于表达式 因此,另一个描述定理 6.4的方法是对于每个表达式,都会有一个常数和一个算法使得在位输入上计算最多需要步 因为在实践中,通常希望对一个短的正则表达式和大的文档计算,所以这是有意义的. 定理 6.4告诉我们,可以在运行时间随文档大小线性增大的情况下计算,即使运行时间可能更依赖于正则表达式的大小

我们通过给出一个高效的递归算法来证明定理 6.4. 该算法将判定是否匹配串的任务归约到判定相关表达式是否匹配 该算法使得表达式的运行时间拥有形式解得

正则表达式的限制: 定理 6.4背后的算法,其中心定义是正则表达式的 限制 的概念 其思想为: 对每个正则表达式和字母,有可能定义一个正则表达式使得匹配当且仅当匹配匹配串 例如,如果是正则表达式(即出现一次或多次),那么等价而 (你能发现是为什么吗)

算法 6.3计算给定正则表达式和字母的限制 该算法总会结束,因为其递归调用时传递的表达式总比输入的表达式小. 其正确性可以通过对正则表达式的长度进行归纳证明,归纳奠基是,或一个单独的字母时.

算法 6.3 (限制正则表达式).

通过限制的概念,可以定义如下匹配正则表达式的递归算法

算法 6.4 (在线性时间内匹配正则表达式).

根据限制的定义,对于每个,表达式匹配当且仅当匹配 因此对每个算法 6.4确实给出了正确的结果. 剩下的唯一任务就是分析其 运行时间 . 需要注意的是,算法 6.4在归纳奠基时使用练习 6.3中的过程. 然而,因为这个过程的运行时间只依赖于,与原输入的长度无关,所以没有问题.

简单起见,我们将注意力限制在字母表相等的情况. 定义为,给定最大符号数,输入定义在上的符号数不超过最大符号数的正则表达式,算法 6.3所能进行的最大操作次数. 可以发现的值是关于的多项式. 然而这对我们的定理并不重要,因为我们只关心计算时运行时间对长度的依赖而不关心其对长度的依赖.

算法 6.4是输入表达式和串的递归算法. 其计算过程为在最多运行后,以某些表达式和长度为的串为输入调用自身. 它将在步运行后结束,此时它到达一个长度为的串. 因此,对长度为的输入,用算法 6.3计算的运行时间满足以下递归方程:

(在归纳奠基时,是某个只与有关的常数. )

为了对(6.2)有直观印象,我们展开一层递归,将写作

如此继续,可以发现,其中是这么做时会遇到的最长的表达式的长度. 因此,如下声明足以说明算法 6.4在运行时间是

声明

是定义在上的正则表达式,则有使得对符号序列,再定义(即,将限制在上,然后是,以此类推),则

对上述声明的证明

对于一个定义在上的正则表达式,我们用来指代表达式,其通过将限制在上,再是,以此类推得到. 令 通过说明对每个,集合是有限的,因此也一样,其为的最大长度,从而证明该声明.

我们通过在的结构上做归纳证明这一点. 如果是符号,空串,或者空集,则可以直截了当地说明能含有的最多的表达式就是只有这个表达式本身, 对其余情况,我们分为两类: (i) (ii) ,其中是更小的表达式(因此根据归纳假设有限).

在情况 (i) 中,若要么等于要么在时为空集合. 因为在集合中,所以中不同表达式的个数最多为

在情况 (ii) 中,若,则在串上的所有限制要么具有形式,要么具有形式,其中为使得成立的串,其中 匹配空串.

因为 ,所以具有形式的可能不同的表达式的数量最多有个. 这就完成了对该声明的证明.

最重要的是,在一个正则表达式上运行算法 6.4时,会遇到的所有表达式都在有限集中,不论输入多大. 因此算法 6.4的运行时间满足等式,其中是依赖于的常数. 最终解得,O记号中隐含的常数可以(且将会)依赖于,并且,重要的是,不依赖于输入的长度.

6.4.1 用DFAs匹配正则表达式

定理 6.4非常令人印象深刻,但是我们可以做得更好. 准确的说,不管有多长,都可以通过维护一个常数大小的内存并进行对单次遍历 来计算 也就是说,这个算法将会从输入的开头扫描到结尾,然后判定是否被匹配. 在常见情况下,我们会尝试在巨大的文件或文档中匹配简短的正则表达式,这些文件或文档甚至没法整个装在电脑的内存里,此时这一特点尤为重要. 当然,如前所述,一个单遍常数内存算法仅仅就是一个确定性有穷自动机. 就像在定理 6.6中将要看到的那样,一个函数能被正则表达式计算 当且仅当 它能被一个DFA计算. 我们从证明“仅当“开始:

定理 6.5 (匹配正则表达式的DFA).

为正则表达式. 则有输入的计算算法,其对进行单次遍历并维护一个常数大小的内存.

定理 6.5的证明思路

算法 6.5给出了一个匹配正则表达式的单遍常数内存算法来检查正则表达式是否匹配一个串. 其思路在于使用“记忆化搜索“的方法,将算法 6.4这一个递归算法用动态规划的算法替代. 如果你还没有上过算法课,你可能不知道这些技巧,这没有关系; 尽管这个更高效的算法对正则表达式的实践应用十分关键,对这本书却并不是很重要.

算法 6.5 (匹配正则表达式).

定理 6.5的证明

算法 6.5判定给定的串是否被正则表达式所匹配.

对每个正则表达式,这个算法都有恒定数量的布尔变量(更准确地说,对每个有一个变量 该算法利用了一个事实: 对每个都在中. ) 其对输入串进行单次遍历. 因此与一个DFA一致.

我们通过归纳输入长度来证明其正确性. 准确地说,我们将论证,在读入之前,对每个,变量相等.

因为初始对每个,让所以的情况成立 对的情况,归纳法证明其成立. 归纳假设表明对每个,都有 而根据集合的定义,对每个位于中而

6.4.2 正则表达式和自动机的等价性

回忆 以下,若存在某个正则表达式,布尔函数相等,则称其为 正则的 . (等价地,若存在某个正则表达式,语言满足当且仅当匹配,则称其为 正则的 ). 下述定理是自动机理论的核心:

定理 6.6 (DFA与正则表达式的等价性).

正则当且仅当存在DFA计算

定理 6.6的证明思路

一个方向由定理 6.5证明,其说明对每个正则表达式,函数可以被一个DFA计算(见样例图 6.6). 在另一个方向上,我们说明给定一个DFA,对每个都可以找到这样一个正则表达式: 当且仅当DFA从状态出发,在读取后最终会到达时,该正则表达式才匹配串

automatonfig

图 6.6. 计算函数的确定性有穷自动机.

dfatoreg1fig

图 6.7. 给定一个状态DFA,对于每个和数,定义函数,其输入为 当且仅当DFA从状态出发,在给定输入为的情况下,最后会到达状态,且过程中仅通过了中间状态,则函数值为

定理 6.6的证明

既然定理 6.5已经证明了“仅当“方向,现在只需要证明“当“方向. 令为一个状态DFA,其计算函数,需要证明是正则的.

对每个,令为这样的函数: 当且仅当DFA 从状态出发,读入输入后会到达状态,则其将映射到 现在将要证明对每个都正则. 这将证明该定理. 因为根据定义 6.1等于对所有取或,其中 因此一旦能够为每个具有形式的函数写出一个正则表达式,(通过使用操作)也就可以得到的正则表达式.

为了给出函数的正则表达式,现在从定义函数开始: 对每个当且仅当自动机从出发接受输入后到达且 *所有的中间状态都在集合中 . (见图 6.7)

这就是说,尽管可能会在之外,当且仅当在输入(从出发)时自动机运行过程中永不进入之外的状态并在结束. 当就是空集,因此当且仅当自动机在输入时直接从转移到而不经过任何的中间状态. 当时所有的状态都在中,因此

现在通过归纳来证明这个定理,说明对所有正则.

对于 归纳奠基 ,对所有的都正则,因为它可以被表示为表达式中的一个.

准确地说,若,则当且仅当为空串. 若,则当且仅当为单个字母

因此在这种情况中,与四个正则表达式中的一个相一致,并取决于转移到时读取的是,还是仅为两个符号中的一者,或者都不是.

归纳步骤 : 刚刚已经说明了归纳奠基,现在通过归纳法来证明一般情况. 归纳假设为对每个,都有正则表达式计算 需要证明的是对每个正则. 如果自动机从时访问了中间状态,则其访问了第个状态零次或多次.

如果一个路径标号为,使得自动机从,并且过程中不需要访问第个状态,则被正则表达式匹配; 如果一个路径标号为,使得使得自动机从,并且过程中需要访问第个状态次,则可以将该路径视为:

  • 首先,从,期间访问的中间状态均位于
  • 然后,回到自身次,期间访问的中间状态均位于
  • 最后,从,期间访问的中间状态均位于

因此在该情况下,字符串被正则表达式匹配. (又见图 6.8) 因此可以使用以下正则表达式计算

归纳步骤证明完毕,进而定理得证明.

dfatoreginductivefig

图 6.8. 若对于每个,均有与相一致的正则表达式,则可以得到一个与相一致的正则表达式 关键的观察结果在于,一个可能经过的状态均在中的,从的路径,要么完全不通过——这种情况被所捕捉; 要么从,然后回到零或多次,最终从——这种情况被所捕捉.

6.4.3 正则表达式的闭包性质

分别是被计算的正则函数,则表达式计算函数 其定义为 另一个说法是,正则函数族 在或运算下封闭 . 这就是说,如果正则,则也一样. 定理 6.6的重要推论是这个集合也在非运算下封闭

引理 6.1 (正则表达式在补运算下封闭).

正则,则函数也正则,其中对每个

引理 6.1的证明

如果正则,则根据定理 6.4,其可被DFA 计算. 然后可构造一个DFA ,其进行的计算相同,但是翻转了接受状态集. DFA 将计算 根据定理 6.6,这表明 也是正则的.

因为引理 6.1表明正则函数族在与操作下也同样封闭. 进一步说,因为或,非,与是通用的基础运算,这个集合在与非,异或,和其它有限函数的运算下也封闭. 这就是说,我们有如下推论

定理 6.7 (正则表达式的闭包性质).

为任意有限布尔函数,令为正则函数,则函数正则.

定理 6.7的证明

这是正则函数在或运算和非运算(因此也有与运算)下的封闭性,与定理 4.7——其声明每个都可以被一个布尔电路计算(其只不过是与、或、非运算的结合)——结合的直接结果.

6.5 正则表达式的限制与泵引理

正则表达式的高效匹配使其分外实用. 通常来说,操作系统和文本编辑器都限制其搜索接口,不允许任意指明一个函数,并采用正则表达式,其原因就在此处. 然而,这种高效是有代价的. 如我们所见,正则表达式无法计算所有函数. 实际上,有很多简单(而且有用! )的函数无法被正则表达式计算. 以下是一个样例:

引理 6.2 (匹配括号).

,而为这样一个函数: 给定一个括号串,其输出当且仅当对于每一个左括号,都有一个右括号与其配对. 则没有定义在上的正则表示能够计算

引理 6.2是如下结果的一个推论,该结果也被称为泵引理 :

定理 6.8 (泵引理).为定义在字母表上的正则表达式,则有这样一个数字,使得对于每个,其中使得,有串使得,并满足以下条件:

  1. 对每个,有

pumpinglemmafig

图 6.9. 为了证明“泵引理“,我们观察一个串,正则表达式能够匹配它,并且大得多. 在这种情况下,的一部分一定会被具有形式的子表达式匹配,而这是唯一允许表达式匹配比其长的串的操作. 如果我们考虑“最左“的、具有该形式的子表达式,并定义是被其匹配的串,我们就得到了泵引理需要的部分.

定理 6.8的证明思路

证明思路如下. 令为表达式中使用的字母数的两倍,串满足,则串存在的唯一方法是中含有操作(即,闭包操作),且有一个非空子串匹配,其中的子串. 6我们可以重复任意多次,而所得的串仍然被匹配. 又见图 6.9

暂停一下

泵引理声明起来比较麻烦,但是记忆它的一个方法是,泵引理实际上只说了这句话: “如果一个被正则表达式的串足够长,那么它的一个子串一定是被运算符所匹配的” .

定理 6.8的证明

通过归纳表达式的长度可以形式化地证明该引理.

像所有的归纳证明一样,该证明会比较长,但在结尾给出符合我们直觉结果——我们一定在某处使用了闭包运算. 阅读该证明,特别地,去理解以下的形式化证明如何与上面的直观思路相一致,是更好地熟悉该种归纳证明的好方法.

归纳假设为对于一个长度为的表达式,符合引理要求的条件.

归纳奠基 为当表达式为当个字母或者 在这些情况中引理显然成立,因为,而不可能有长度大于的串被该表达式匹配.

我们现在证明 归纳步骤 . 令为有个符号的正则表达式,让且串满足 既然有多于一个符号,则其具有下列形式之一: (a) : (b) : (c) 在所有情况中,子表达式的符号数都少于,因此符合归纳假设.

在情况 (a) 中,每个被匹配的串都被中的一者匹配. 若匹配,则根据归纳假设以及,有,其中使得对每个(因此也一样)匹配匹配时同理.

在情况 (b) 中,若匹配,则有,其中匹配匹配 我们现在分类讨论. 若则根据归纳假设有满足使得,且对每个匹配 如果我们令,则,且对于每个匹配 否则,若,又,则必定有 因此根据归纳假设有使得且对每个匹配 而我们现在令,则有 而另一方面对每个,表达式匹配

在情况 (c) 中,若匹配,则,其中对每个是一个被匹配的非空串. 若,我们可以用与上述连接运算情况相同的方法. 否则,注意到若是空串,且对每个匹配.

Info

备注 6.2 (递归定义与归纳证明).

当一个对象是 递归定义的 (像是正则表达式),则通过 归纳 证明这种对象的性质是自然的. 也就是说,我们我们想要证明所有这种类型的对象都具有性质,则我们可以很自然地采取这样的归纳步骤: 若等具有性质,则通过结合它们产生的对象也一样.

通过泵引理,我们可以轻易地证明引理 6.2(即“括号匹配“函数的非正则性):

引理 6.2的证明

为了使用反证法,我们假设有一个表达式使得定理 6.8中的数,而(即,个左括号跟着个右括号). 则若如定理 6.8中那样写出表明完全由左括号组成. 因此中的左括号比右括号更多. 因此 但根据泵引理,与假设矛盾.

对于一个确定的函数,在说明该函数 不能 被正则表达式计算的方面,泵引理是一个有效的工具. 然而,这并 不是 正则性的“充分必要“条件: 存在一个非正则的函数,其满足泵引理的条件. 为了理解泵引理,遵循定理 6.8中量词的顺序是很关键的. 特别地,定理 6.8所描述的数字取决于所选的正则表达式(上述证明选择了表达式所用符号数的两倍). 所以,为了使用泵引理来排除计算某个函数的正则表达式的存在性,就需要能够选择一个合适的输入 它要能够任意地增大,并且满足F(w)=1. 如果你仔细思考泵引理后蕴含的直观,就会发现上述内容是很有意义的: 足够大的才能强制性地要求使用闭包运算.

pumpinglemmaprooffig 图 6.10. 一个漫画,其内容是使用泵引理来证明不正则. 泵引理宣称: 如果正则,就一定会有一个数,使得对 所有 足够大的满足存在 的一个划分满足特定的条件,使得对 所有 你可以将一个基于泵引理的证明视作你和对手间的一场竞赛. 每个 存在 量词都对应着你可以自由选择的对象(其基于先前选择的对象). 每个 全称 量词都对应着对手可以任意选择的满足条件的对象(并且也基于先前的选择). 一个有效的证明对应着无论对手做什么,你都可以取胜的策略. 该策略通过构造一个矛盾来取胜. 其是对的一个选择,使得成立,同时又使得泵引理的结论有效.

练习 6.4 (回文非正则).

证明对于定义在字母表上的函数非正则: 当且仅当,其中代表“反转”: 串 ( 回文 函数定义时一般不需要一个显式的分隔符,但带有分隔符的版本更加简洁,因为我们在此处使用它. 这并没有什么影响,因为分隔符可以很容易地用一个特殊的二进制串编码).

练习 6.4的解答

此处采用泵引理. 为了使用反证法,假设有一个正则表达式计算,令为泵引理(定理 6.8)中的数. 考虑串 因为全部由零组成的串的反转仍为全部由零组成的串,所以 现在,根据选择引理,如果计算,则可以写下使得且对每个 特别地,一定成立,但这就导致了矛盾,因为,所以其两部分并不一样长,所以并不是另一者的反转.

另一个基于泵引理的证明见图 6.10,这是一个关于函数非正规性证明的漫画,其中当且仅当存在使得(即,为一个连续零串拼接上一个同等长度的连续一串).

6.6 回答正则表达式的语义问题

正则表达式有着除搜索之外的其他应用. 例如,在编程语言的 语法分析器编译器解释器 的设计中,正则表达式通常用于定义 词元 (例如一个有效的变量名,或者关键字). 正则表达式还有别的应用: 例如,近年来,互联网从固定的拓扑结构演化为“软件定义的网络“. 这样一个网络由可编程交换机进行路由,这些交换机实现了一些 策略 ,例如“如果包被SSL验证,则把它转发到A,否则转发到B“. 为了表示这样的策略,我们需要一种语言,它一方面足够丰富,可以捕捉我们需要实现的策略; 另一方面又被充分地限制,从而可以在网络高速的要求下快速地执行它们,并能够回答像“C能否查看从A到B的包“这样的问题.

NetKAT网络编程语言通过正则表达式的一个变体来精确地实现这一点. 在这些应用中,我们不仅仅能够回答表达式能够匹配,同时也回答关于正则表达式的 语义问题 ,例如“表达式是否计算同一个函数“ 以及 “是否存在串匹配? “

接下来的定理说明我们可以回答后者:

定理 6.9 (正则语言的空性可计算).

存在一个算法,给定一个正则表达式,其输出当且仅当为常零函数.

定理 6.9的证明思路

思路为,我们可以直接从表达式的结构中观察到这一点. 计算常零函数的唯一可能是具有形式或者通过与其他表达式连接得到.

定理 6.9的证明

如果一个正则表达式计算的是常零函数,我们就定义其是“空的“. 给定一个正则表达式,通过以下规则,我们可以判定是否为空:

  • 具有形式,则其非空
  • 非空,则对所有的均非空
  • 非空则非空
  • 均非空,则非空.
  • 为空.

通过这些规则,可以直接得出一个判定空性的递归算法.

通过定理 6.9,我们可以得到判定两个正则表达式是否 等价 的算法. 这意味着它们计算相同的函数.

定理 6.10 (正则表达式的等价性可计算). 令函数输入(串表示的)一对正则表达式当且仅当 则存在一个算法计算

定理 6.10的证明思路

证明思路是,对于给定的一对正则表达式,我们寻找一个表达式使得当且仅当 因此为常零函数当且仅当等价,则我们可以由此通过测试的空性来判定的等价性.

定理 6.10的证明

我们从定理 6.9中证明定理 6.10. (这两个定理实际上是等价的: 我们很容易从定理 6.10中证明定理 6.9,因为测试表达式空性和判定其与的等价性是一样的. )

对给定的两个表达式,目标是计算表达式使得当且仅当 可以发现,等价当且仅当为空.

我们从这样一个观察结果出发: 对每个位 当且仅当

因此我们需要构造这样一个,其对所有的,均有

为了构造这个表达式,我们会说明对于任意一对,我们可以构造表达式,其分别计算 (计算表达式是很直接的,只需使用运算)

特别地,根据引理 6.1,正则函数在否运算下封闭. 这意味着对每个正则表达式,均有表达式使得对所有均有

于是,对于所有的两个表达式,表达式 计算表达式的与运算.

给出了这两个变换,可以发现对所有的正则表达式,都可以找到一个表达式满足(6.3),使得为空当且仅当等价.

本章回顾

  • 使用 无限 函数对输入长度任意的计算任务建模.
  • 这样一种函数输入一个任意长(但仍然有限! )的串,而且不能被一个由输入输出构成的有限表格描述.
  • 被称为 布尔函数 的一类特殊函数,其输出为单个位. 计算该函数等价于判定一个 语言
  • 确定性有穷自动机 (DFAs)是计算(无限)布尔函数的一个简单模型.
  • 有一些函数无法被DFAs计算.
  • DFAs可计算的函数族与正则表达式能识别的语言族相同.

6.7 习题

习题 6.1 (正则函数的闭性质).

假设均正则. 对于下列每一个函数的定义,要么证明总正则; 要么给出一对正则的作为反例,使得非正则.

  1. 其中的反转: for

  2. WWWW

习题 6.2.

下列是两个从映射到的函数,其中一个能被正则表达式计算,另一者不能. 对能被计算的那一者,写出确实能够计算其的表达式; 对于不能被计算的那一者,使用泵引理证明其不能.

  • 整除,否则

  • 当且仅当否则

习题 6.3 (非正则性).

  1. 证明下列函数非正则. 对每个当且仅当具有形式,其中

  2. 证明下列函数非正则. 对每个当且仅当,其中

6.8 参考文献

正则表达式与有穷自动机的练习是一个优美的话题,本文中我们对其浅尝辄止. (Sipser, 1997)(Hopcroft, Motwani, Ullman, 2014)(Kozen, 1997)中对该话题涉及更多. 这些文章也讨论了像 非确定有穷自动机 (NFA),以及上下文无关文法与下推自动机的关系.

图 6.4中的自动机由FSM simulator生成,作者为Ivan Zuzak和Vedrana Jankovic.

我们对于定理 6.4的证明与Myhill-Nerode定理联系紧密. Myhill-Nerode定理的一个方向可以被陈述为: 如果是一个正则表达式,则存在最多有限个串,使得对每个,有


1: 译者注: 中的元素称为 字母 ,原著中提到其元素时使用的术语是字母表符号alphabet symbol,翻译时为了简洁使用字母这一个更加简单的术语

2: 译者注: 更准确地说,是条,但之后考虑的均为,因此

3: 译者注: 准确的说法是闭包

4: 译者注: 事实上,以上过程仅仅证明了算法 6.1是会结束的,但是并没有证明正确性. 但上面的过程确实给出了证明其正确性的骨架,因此剩下的工作繁而不难

5: 译者注: 该算法并未要求输入串 此处应为作者笔误

6: 译者注: 此处应为作者笔误,正确语句应当如下: 且有一个非空子串匹配,其中的子串.