# 2 有穷自动机
# 2.1 概述
# 2.1.1 什么是有穷自动机?
有穷自动机(finite automa) 是一个形式系统,它只记忆有限量的信息,这些信息通过它的 状态(states) 表示。当有 输入(input) 给到有穷自动机的时候,它会相应地改变自己的状态。告诉有穷自动机该如何根据输入改变自身状态的规则称为 转移(transition) 。
# 2.1.2 为什么要学习有穷自动机?
- 有穷自动机在电路与通信协议的设计和验证方面被广泛使用;
- 适用于许多的文本处理应用场景;
- 是编译器的一个重要组成部分;
- 描述了事件的简单模式;
- ......
# 2.1.3 一个关于网球的例子
网球的比赛胜利规则是这样的:
- 至少要得4分;
- 还需要比对手多两分;
用 s
表示 server wins point
, 用 o
表示 opponent wins point
,我们可以用如下的状态转换图来描述整个比赛输赢状况的变化过程。
这个状态转换图也称为 自动机(automaton) ,其中,双环表示 终止状态(final state) ,也就是比赛结束的时候。
给定一系列的输入,称为 输入字符串(input string) ,从初始状态开始,根据每个输入符号转移到下一状态。如果读入一个特定的字符串能够让状态从初始状态变为终止状态,我们称这个输入被 接受(accepted) 了。
# 2.1.4 一个自动机的语言
称能够被一个自动机 接受的字符串的集合为 的 语言(language) ,记为 。比如说 能够决出胜出者的字符串的集合。
对于同样的一组状态和转移来说,指定不同的终止状态,就会有不同的自动机,也就能够表示不同的语言了。
直观来讲,有穷自动机的基本要素有:
- 有限个状态;
- 状态间的转移;
- 一个初始状态和一个或者多个终止状态。
# 2.2 确定性有穷自动机
# 2.2.1 回忆基本概念
字母表(alphabet) 是任意有限的符号的集合。
- 比如说
ASCII
、Unicode
、{0, 1}
( 二元字母表(binary alphabet) ) 、{a, b, c}
、{s, o}
、 一个协议的信号集合。
- 比如说
一个字母表 上的 字符串(string) 是一个序列,其中每个元素都是 的成员。
- 在这个系列中,我们约定书写字符串的时候不需要空格、逗号和引号,直接书写即可。比如说
abc
或者01101
。
- 在这个系列中,我们约定书写字符串的时候不需要空格、逗号和引号,直接书写即可。比如说
所有字母表 上的字符串的集合。
比如说
有些微妙的是,
0
作为一个字符串还是作为单个符号的写法看起来是一样的,具体什么类型是由上下文语境所决定的,其他的符号也是如此。到底是单个符号,还是长度为1的字符串,类型由上下文决定。
一个字符串的 长度(length) 是字符串上存放符号的位置的个数。
表示 空串(empty string) ,即长度为0的字符串。
某个字母表 上的一个 语言(language) 是 的子集。
# 2.2.2 定义
有了上面的这些基本概念,我们就可以定义确定性有穷自动机了。
定义2.1
确定性有穷自动机(Deterministic Finite Automa, DFA) 是一种定义语言的形式化系统,记为五元组 ,其中:
是一个有限的 状态(state) 集合;
是一个 输入字母表(input alphabet) ;
是一个 转移函数(transfer function) ;
转移函数接受两个参数:一个状态和一个输入符号。
在状态 处接受符号 后DFA将走向的下一个状态。
是 初始状态(start state) , ;
是 终止状态(final state) 的集合 , 。
- 终止状态也称为 接受状态(accepting states) 。
其中需要注意的是 是一个全函数,也就是说总是存在下一个状态。如果没有转移的话,我们增加一个 死状态(dead state) 表示没有转移的情况。
例如:
不过为了表达简单,我们一般不把死状态画出来。
# 2.2.3 DFA的表示
# 常用方法:图表示
通常我们会用一张 状态转换图 来表示DFA,就像我们最开始的网球的例子一样,其中:
用 结点(node) 表示状态;
用 弧(arc) 表示转移函数;
- 从状态 到状态 的弧上面标有所有的能够让 转移到 的输入符号。
用标有
Start
的箭头指向起始状态;用 双圆(double circle) 表示终止状态。
比如说下面是一个可以识别英语中 ing
的 DFA 。
# 另一种方法:转移表
另一种可选的表示DFA的方式是 转移表(transition table) ,其中:
- 行头表示状态,列头表示输入符号;
- 用箭头指向初始状态,用星号标注终止状态;
- 每一个表象都是转移函数的结果,比如说在 行, 列的表项 表示 。
比如说下面是一个DFA的转移表以及等价的状态转换图的例子:
在习惯上,我们会用 这样的字母来表示字符串,用 这样的字母来表示单个输入符号。
# 2.2.4 扩展的转移函数
我们通过将 扩展到一个状态和一个字符串的方式(原本是一个状态和一个字符),来描述一组输入对于DFA的影响。
一个直观的感觉是,对于某个状态 ,扩展的 会从这个状态开始,根据一组输入 ,沿着状态转换图上面的某条路径,从 走到 ,其中该路径上的每条弧分别对应着标记 。
定义2.2
递归定义 扩展的转移函数(extended ) 为:
其中, 是一个字符串, 是一个输入符号, 表示这两者的拼接(在拼接过程中将 视作是长度为1的字符串)。
虽然在上面的定义中,我们使用了 这个符号,但是习惯上,我们并不区分 与 ,通常都用 表示,至于其到底是转移函数还是扩展的转移函数,由具体语境下的输入类型决定。这样的话后续很多的表达会方便很多。
比如说对于下面这个例子:
0 | 1 | |
---|---|---|
A | A | B |
B | A | C |
C | C | C |
我们有:
# 2.2.5 DFA的语言
各种各样的自动机可以用来定义语言,如果 是一个自动机,我们用 代表它的语言。对于一个DFA 来说, 是从初始状态走到终止状态的所有路径的标签字符串的集合。
形式化定义如下:
定义2.3
对于一个DFA ,定义 能够 接受(accept) 的语言集合为:
下面我们举一个例子:
我们可以发现, 101
是一个能够被这个DFA所接受的字符串。其实,这个DFA描述的是由 0 和 1 组成的所有的不含连续1的字符串组成的语言,形式化的表述就是:
下面,我们来证明一下上面的结论是正确的,也就是证明两个集合的等价性。
考虑 是我们给出的DFA对应的语言, 是由01组成的不含连续1的语言。下面我们只需要证明两个集合相等,通常,我们会通过下面的方式来证明集合相等:
也就是要证明
先证 ,也就是要证明如果 被给定的DFA接受,那么 中没有连续的1。我们可以通过对 的长度进行归纳来证明。
这里有一个很重要的技巧:扩展我们的归纳假设,使其比我们想要证明的结论更加详细。
归纳假设:
如果 ,那么 没有连续的两个 并且不以 结尾;
如果 ,那么 没有连续的两个 并且以单个 结尾。
基础情况: ,即 时。
- 1成立,因为 里面没有 。
- 2显然成立,因为 。
这里有一个重要的概念,对于命题 , 为假时 永远为真。
归纳步骤:
假设1和2对于长度比 短的字符串成立,记 ,其中 是 的最后一个符号, 是最后一个符号之前的字符串,则归纳假设对于 成立。
下面我们只需要证明1和2两个结论对于 也成立即可。
如果 ,则 一定是 或者 且 一定是 (看DFA),由归纳假设, 中没有连续的两个 ,从而 也没有两个连续的1,并且不以 结尾;
如果 ,则 且 (看DFA),根据归纳假设 中没有连续的两个1并且不以 结尾,从而 中也没有两个连续的1,并且以单个 结尾。
于是,只要一个字符串被给定的DFA接受,那么它一定没有连续的两个 ,即 。
下面证明 ,也就是证明,如果 没有连续的两个 ,那么它会被之前给出的DFA接受。
反证法。假设 没有被DFA所接受。
由于在每一个状态的每一个输入符号,都有一个独特的转移,因此每个进入 的字符串 都会进到一个唯一的状态。 不被DFA所接受的唯一可能就是走到状态 。
即 ,那么 ,其中 ,因为到达C只有经过B这一条路。
而 又能够推出 ,因为到达 只有从 读取 这一条路。
于是 ,这与 不含连续的两个 矛盾。
# 2.2.6 正则语言
定义2.4
如果一个语言 可以被某个 DFA 恰好接受(即 ),称这个语言是 正则(regular) 的。
注意, 必须恰好接受 ,即接受且只接受 中的字符串。
有一些语言不是正则的。直观上感觉,正则语言不太能够有任意多的状态,因为DFA的状态是有限的。
举一个不是正则的语言的例子, 。
其实就是连续的 个 和 个 组成的字符串的集合( )。下面我们来证明一下这个语言确实不是正则的。
反证法,假设存在一个有 个状态的DFA接受这个语言,考虑输入字符串 的状态变化:
考虑前 次移动,产生了 种状态,而整个DFA只有 种状态,根据鸽笼原理,至少有一个状态出现了两次,假设这个状态是 ,且 。
于是有:
于是,我们发现 ,即 也可以被该 DFA 接受,但 ,矛盾。
再看一个不是正则语言的例子: ,也就是所有匹配的括号串组成的语言,这也不是正则语言。
不过也有很多语言是正则的,它们会在许多语境中出现并且有很多有用的性质。比如说,表示浮点数的字符串就是一个正则语言。
再比如说: ,其中,计算 时将 视作二进制数。
这个DFA还是比较好构建的,因为任意一个数模23只会有23种有限的情况,我们可以用23个状态来表示, 记为 ,唯一的初始状态和终止状态就是0。
如果字符串 二进制含义下表示整数 ,假设 ,那么:
- 表示 ,则 ;
- 表示 ,则 。
这样的话,我们就能构建出整个DFA了。比如说 , 。
另一个例子: ,其中 是 的逆,即顺序相反的字符串。
这个语言依旧是正则的,不过它的DFA就不太容易构建了,不过之后我们会学到一个定理,这个定理告诉我们一个正则语言的逆依旧是正则语言。
# 2.3 非确定性有穷自动机
# 2.3.1 非确定性
非确定性有穷自动机(Nondeterministic Finite Automaton, NFA) 可以同一时刻处在多个状态中。当有一个输入符号的时候,可以从一个状态转移到任意状态的集合。
NFA从一个初始状态开始,如果任意一个选择序列能够到达一个终止状态,我们就认为NFA可以接受这个字符串。
直观来讲,虽然转移函数给出了多个目标状态,NFA总能够猜到正确的路。
比如说棋盘上的移动,将棋盘上的格子视为状态,输入是 组成的字符串,其中 表示移动到相邻的红格子中, 表示移动到相邻的黑格子中。初始状态是左上角的格子,终止状态是右下角的格子。
根据定义,我们可以给出这个自动机的转移表, rbb
就是一个这个自动机能够接受的字符串,因为:
# 2.3.2 定义
定义2.5
非确定性有穷自动机(Nondeterministic Finite Automa, NFA) 定义为五元组 ,除了 以外同DFA的定义2.1。
- 转移函数 返回一个状态的集合,即 ,其中 是一个符号。
定义2.6
递归定义NFA的 扩展的转移函数 为:
简记为 。
定义2.7
称一个字符串 被一个 NFA 接受 ,如果
一个NFA的语言就是这个NFA所能接受的所有字符串的集合,记为 。
# 2.3.3 DFA与NFA的等价性
NFA虽然看似给了更多的自由度,但其实它的表达能力和DFA是等价的。
定理 2.1
DFA 和 NFA 在语言的表达能力上是等价的。
能够用NFA表达的语言,一定存在某个DFA,能够表达出相同的语言;
能够用DFA表达的语言,一定存在某个NFA,能够表达出相同的语言。
首先,一个DFA能够被转化成一个接受相同语言的NFA,这个结论是显然的,因为我们可以将DFA看成是一种特殊的NFA,NFA是更具一般性的DFA。
给定某个DFA,构造一个具有相同状态集合、初始状态、终止状态集合以及字母表的NFA。用 表示DFA的转移函数, 表示NFA的转移函数。如果 ,则令 。
这样的话NFA的转移函数总是会返回一个单元集,其元素就是DFA在相同输入下的下一个状态,也就是说NFA退化成了一个DFA。
因此,给定一个DFA,一定能够通过上述方式构造出一个接受相同语言的NFA。
上面的结论是很容易理解的,稍微让人吃惊一些的是,对于任意的NFA,也存在一个DFA接受相同的语言。
证明的方法称为 子集构造(subset construction) 。
考虑一个NFA,具有状态集 ,字母表 ,转移函数 ,初始状态 ,终止状态集 。
下面构建一个等价的DFA ,具有状态集 ,字母表 , 初始状态 ,终止状态集
这里需要注意的关键点是,DFA的状态集中的元素是NFA中状态的集合。也就是说,作为一个DFA的状态, 应当视为一个DFA状态集中的一个元素,只不过这个元素恰好是 的子集罢了。
定义转移函数 为:
这里 的返回值对于NFA来说是一些状态的集合,不过对于DFA来说只不过是一个状态而已。
下面,我们通过之前的棋盘的例子来理解一下子集构造的过程。
上图中左侧是一个NFA,右侧是通过子集构造生成的对应的DFA。
下面我们只需要证明子集构造生成的DFA和原本的NFA接受相同的语言即可。
其实如果你自己在纸上走了一遍上面的例子,就会发现等价性的证明其实很容易了。
我们通过对 归纳来证明 恒成立即可。
基础情况: , 。
归纳:假设结论对于比 更短的字符串成了。令 ,根据 与 的定义有
根据归纳假设,对于 有 ,从而
于是,我们构造出来的DFA与原来的NFA是等价的,因为它们接受相同的语言。
# 2.4 带空转移的NFA
# 2.4.1 定义
我们可以在NFA的基础上再允许某个状态在没有输入字符的时候就转移到另一个状态,也就是说转移函数可以接受 输入。
这些 空转移( -Transitions ) 是自动完成的,不需要看输入字符串,这样的NFA称为 带空转移的NFA ,记为 - NFA。
这在某些时候会提供表达上的方便,不过并没有带来表达能力的提升, - NFA 依旧只能接受正则语言。
一个简单的例子如下:
定义2.8
定义状态 的 状态闭包(Closure of States) 为从状态 开始通过接受一个或者若干个 能够到达的状态集合,记为 。
定义状态集合 的闭包为
例如,在下面的 - NFA 中:
有 , 。
下面我们来定义一下扩展的 ,直观上, 应当是从状态 通过标记为 的路径的所能到达的状态的集合。
定义2.9
定义 - NFA 的扩展转移函数为:
沿用前一个例子,我们有:
一个 - NFA的语言就是所有使得 包含至少一个终止状态的字符串 的集合。
# 2.4.2 NFA与带空转移的NFA的等价性
定理 2.2
NFA 和 - NFA 在语言的表达能力上是等价的。
能够用NFA表达的语言,一定存在某个 - NFA,能够表达出相同的语言;
能够用 - NFA表达的语言,一定存在某个NFA,能够表达出相同的语言。
首先,每一个 NFA 就是一个 - NFA ,只不过没有空转移罢了。
下面,只需要证明给定一个 - NFA,我们都能够构造一个 NFA,与 - NFA接收相同的语言。
我们通过将空转移与下一次输入的真正转移组合在一起来实现这种构造,从而将空转移消除,如下图所示:
考虑 - NFA ,构造其原本的 NFA ,其中:
我们可以通过对 归纳证明 ,从而说明能被 - NFA接受当且仅当能被原本的NFA接受。
这里由于我们构造的时候是先取闭包,后转移,最后还剩一次取闭包的操作,因此在 之前还需要取一次闭包。
下面是一个 - NFA 及其等价的 NFA 的例子:
# 2.5 总结
DFA、NFA与 - NFA能够接受的语言的集合是相同的:正则语言。
NFA设计起来更加容易,可能会比DFA的状态数成指数级地少得多。
只有DFA可以被真正实现,因为计算机世界是一个确定的世界。