# 5 下推自动机

下推自动机(Pushdown Automata, PDA) 是一种在语言的定义能力上和 CFGCFG 等价的自动机。不过只有非确定性下推自动机才能定义所有的上下文无关语言。但是确定性下推自动机可以为解析器建模,并且大多数的程序设计语言都有确定性的下推自动机,因为确定的才是可实现的。

# 5.1 下推自动机

# 5.1.1 直观感受

想象一个 ε\varepsilon - NFA ,它具有额外的能够操作一个栈的能力,这就是PDA了。PDA上的移动由下面三个因素定义:

  1. 它的NFA的当前状态;

  2. 当前输入符号(或者 ε\varepsilon );

  3. 当前的栈顶元素。

大体是长成这样的:

pda

PDA是非确定性的,也就是说它的下一个移动可以有不止一个选择,在每一个选择中,PDA可以:

  1. 改变状态;

  2. 将栈顶元素替换成0个或者多个符号。

    • 替换成0个符号就相当于是出栈(pop)操作;

    • 替换成多个符号就相当于一个出栈(pop)加上一系列入栈(push)操作。

# 5.1.2 形式化定义

定义5.1

定义一个 下推自动机(Pushdown Automata) 为一个七元组 P=(Q,Σ,Γ,δ,q0,Z0,F)P = (Q, \Sigma, \Gamma, \delta, q_0, Z_0, F) ,其中:

  • QQ 是一个有限的 状态(state) 的集合;

  • Σ\Sigma 是一个 输入字母表(input alphabet)

  • Γ\Gamma 一个 栈字母表(stack alphabet)

  • δ\delta 是一个 转移函数(transition function)

    • 转移函数接收3个参数:

      • 一个状态 qQq \in Q

      • 一个输入 aΣa \in \Sigma 或者 a=εa = \varepsilon

      • 一个栈符号 ZΓZ \in \Gamma

    • δ(q,a,Z)={(p,α)pQ,αΓ}\delta(q, a, Z) = \{(p, \alpha) | p \in Q, \alpha \in \Gamma^*\} ,可能是空集;

    • 如果 (p,α)δ(q,a,Z)(p, \alpha) \in \delta(q, a, Z) ,那么PDA可以在状态 qq ,接收输入 aa ,并且观察到栈顶元素为 ZZ 的时候:

      • 状态变为 pp

      • 从输入的首部接收一个字符 aaaa 可能是 ε\varepsilon

      • 将栈顶的元素 ZZ 替换成序列 α\alpha

  • q0q_0 是一个 起始状态(start state)q0Qq_0 \in Q

  • Z0Z_0 是一个 起始符号(start symbol)Z0ΓZ_0 \in \Gamma

  • FF 是一个 终止状态(final states) 的集合 , FQF \subseteq Q

和上一讲类似,为了表示和说明方便,我们引入一下字母使用上的约定,这不是必须的,但本教程会遵守这些约定以尽量表意简明。

  • a,b,c,...a, b, c, ... 是输入符号;

    • 有时候我们会允许 ε\varepsilon 作为输入符号;
  • ...,X,Y,Z..., X, Y, Z 是栈符号;

  • ...,w,x,y,z..., w, x, y, z 是输入符号形成的字符串;

  • α,β,...\alpha, \beta, ... 是栈符号形成的字符串。

下面我们来设计一个 PDA 去接收语言 {0n1nn1}\{0^n1^n | n\ge 1\}

  • 状态:

    • qq 是起始状态,表示我们到目前为止只看到了0;

    • pp 表示我们已经至少看到了一个1,并且只有在后续输入为1的时候才会前进;

    • ff 是终止状态,表示接受。

  • 栈符号:

    • Z0Z_0 是初始符号,也标识了栈的底部,这样我们就知道什么时候我们数到了相等数量的1和0;

    • XX 是一个标记符,用于对输入中见到的0计数。

  • 转移函数:

    • δ(q,0,Z0)={(q,XZ0)}\delta(q, 0, Z_0) = \{(q, XZ_0)\}

    • δ(q,0,X)={(q,XX)}\delta(q, 0, X) = \{(q, XX)\}

      • 上面两个规则保证了,每当从输入中读到一个0的时候,向栈中压入一个 XX
    • δ(q,1,X)={(p,ε)}\delta(q, 1, X) = \{(p, \varepsilon)\}

      • 当我们第一次读到一个1的时候,从栈中弹出一个 XX ,并进入状态 qq
    • δ(p,1,X)={(p,ε)}\delta(p, 1, X) = \{(p, \varepsilon)\}

      • 每读到一个新的1,弹出一个 XX
    • δ(p,ε,Z0)={(f,Z0)}\delta(p, \varepsilon, Z_0) = \{(f, Z_0)\}

      • 在栈底接收,此时1和0的个数相等。

下面是用这个PDA接收000111的例子:

pda-eg

这个PDA用状态转换图表示为:

pda-graph

# 5.1.3 PDA的运行

我们可以将刚刚见到的PDA的过程图用即时描述形式化一下。

定义5.2

一个 即时描述(Instantaneous Description, ID) 是一个三元组 (q,w,α)(q, w, \alpha) ,其中:

  1. qq 是当前状态;

  2. ww 是剩余的输入;

  3. α\alpha 是栈的内容,左侧为栈顶。

如果ID II 可以通过PDA的一步移动变成ID JJ ,我们写作 IJI \vdash J

定义5.3

定义 走向(Goes-To) 关系:

(w,α,(q,aw,Xα)(p,w,βα))(p,β)δ(q,a,X) (\forall w, \alpha, (q, aw, X\alpha) \vdash (p, w, \beta\alpha)) \Leftrightarrow (p, \beta) \in \delta(q, a, X)

\vdash 扩展到 \vdash^* ,表示0或多次,满足

  • 基础情况: III \vdash^* I

  • 归纳情况: 若 IJI \vdash^* JJKJ \vdash K ,则 IKI \vdash^* K

沿用上面000111的例子,我们可以将PDA的变化过程图方便地写成如下的ID走向序列:

(q,000111,Z0)(q,00111,XZ0)(q,0111,XXZ0)(q,111,XXXZ0)(p,11,XXZ0)(p,1,XZ0)(p,ε,Z0)(f,ε,Z0) (q, 000111, Z_0) \vdash (q, 00111, XZ_0) \vdash (q, 0111, XXZ_0) \vdash (q, 111, XXXZ_0)\\ \vdash (p, 11, XXZ_0) \vdash (p, 1, XZ_0) \vdash (p, \varepsilon, Z_0) \vdash (f, \varepsilon, Z_0)

那如果输入是0001111呢?

(q,000111,Z0)(q,00111,XZ0)(q,0111,XXZ0)(q,111,XXXZ0)(p,11,XXZ0)(p,1,XZ0)(p,1,Z0)(f,1,Z0) (q, 000111, Z_0) \vdash (q, 00111, XZ_0) \vdash (q, 0111, XXZ_0) \vdash (q, 111, XXXZ_0)\\ \vdash (p, 11, XXZ_0) \vdash (p, 1, XZ_0) \vdash (p, 1, Z_0) \vdash (f, 1, Z_0)

注意到ID没有办法再移动了,也就是说输入并没有被完全消耗完,因此0001111不被接受。

其实,和DFA一样,这里有一个隐藏的死状态(dead state),转移函数是一个全函数,我们定义其一部分,未定义的情况都默认走入死状态。

定理5.1

给定PDA PP ,如果 (q,x,α)(p,y,β)(q, x, \alpha) \vdash^* (p, y, \beta) ,则 wΣ,γΓ\forall w \in \Sigma^*, \gamma \in \Gamma^* ,有 (q,xw,αγ)(p,yw,βγ)(q, xw, \alpha\gamma)\vdash^*(p, yw, \beta\gamma)

这个定理很容易理解,增加后缀字符串和增加部分栈底元素是不会影响中间的走向序列的。

不过这个定理反之亦然么?答案是不是的。我们可以增加相同的部分栈底元素,这没什么影响。

但不能删除相同的部分栈底元素,因为有可能这部分栈底元素是先被弹出又被压入的。虽然从结果上来看是没关系的,但是如果把这些栈底元素去掉了,在过程中,弹出这些栈底元素的步骤就会走入死状态,从而破坏了等价性。

不过字符串的相同后缀并没有这个问题就是了。

定理5.2

给定PDA PP ,若 (q,xw,α)(p,yw,β)(q, xw, \alpha) \vdash^* (p, yw, \beta) ,则有 (q,x,α)(p,y,β)(q, x, \alpha) \vdash^* (p, y, \beta)

# 5.1.4 PDA的语言

定义PDA的语言的一种通常的方式是根据 终止状态(final state)

定义5.4

PP 是一个PDA,定义

L(P)={wαΓ,fF,(q0,w,Z0)(f,ε,α)} L(P) = \{w | \exists \alpha\in\Gamma^*, f\in F, (q_0, w, Z_0) \vdash^* (f, \varepsilon, \alpha)\}

另一种定义PDA的语言的方式是根据 空栈(empty stack)

定义5.5

PP 是一个PDA,定义

N(P)={wqQ,(q0,w,Z0)(q,ε,ε)} N(P) = \{w | \exists q \in Q, (q_0, w, Z_0) \vdash^* (q, \varepsilon, \varepsilon)\}

定理5.3

L(P)L(P)N(P)N(P) 在定义语言的能力上是等价的。

这两种语言的定义在表达能力上是等价的:

  1. L=L(P)L = L(P) ,则存在另一个PDA PP' 使得 L=N(P)L = N(P')

  2. L=N(P)L = N(P) ,则存在另一个PDA PP'' 使得 L=L(P)L = L(P'')

先证 L(P)L(P) 能推出 N(P)N(P')

直观的想法是 PP' 会模拟 PP ,如果 PP 接收,则 PP' 会清空它的栈。 PP' 必须避免意外地清空自己的栈,所以它需要一个特殊的栈底标记来捕捉那种 PP 清空了自己的栈但是并没有接受的情况。

构造 PP' 用于和 PP 同样的状态、符号和转移,再加上:

  1. 栈符号 X0X_0PP' 的起始符号),用来保卫栈底;

  2. 新的起始状态 ss 和擦除(erase)状态 ee

  3. δ(s,ε,X0)={(q0,Z0X0)}\delta(s, \varepsilon, X_0) = \{(q_0, Z_0X_0)\} ,用于启动 PP

  4. 对于 PP 的任意终止状态 ff ,将 (e,ε)(e, \varepsilon) 添加到 δ(f,ε,X)\delta(f, \varepsilon, X) 中去,其中 XX 是任意的栈符号,包括 X0X_0

  5. 对于任意 XXδ(e,ε,X)={(e,ε)}\delta(e, \varepsilon, X) = \{(e, \varepsilon)\}

这里新增加的 X0X_0 就是用来保护原本 PP 可能在过程中空栈的情况,中间过程都不变,增加转移,让原本的终止状态能够走到新的空栈状态中去。

图示如下:

l2n

再证 N(P)N(P) 可以推出 L(P)L(P'')

直观的想法是 PP'' 模拟 PP ,不过 PP'' 有一种特殊的底部标记用来捕捉 PP 空栈的情况,然后把这种情况设置为接受状态。

构造 PP'' 拥有和 PP 相同的状态,符号和转移,加上:

  1. 栈符号 X0X_0 (起始符号),用于保护栈底;

  2. 新的起始状态 ss 和终止状态 ff

  3. δ(s,ε,X0)={(q0,Z0X0)}\delta(s, \varepsilon, X_0) = \{(q_0, Z_0X_0)\} ,用于启动 PP

  4. 对于 PP 中的任意状态 qqδ(q,ε,X0)={(f,ε)}\delta(q, \varepsilon, X_0) = \{(f, \varepsilon)\}

再一次强调,这里新增 X0X_0 的作用,和之前一样,都是为了保护栈底,辅助判断。

n2l

下面看一个例子,设计一个PDA,能够处理 if-else 语句,当 else 的数量超过前缀 if 的数量时停止。

N(P)N(P) 的版本:

if-n

L(P)L(P) 的版本:

if-l

# 5.1.5 确定性下推自动机与非确定性下推自动机

我们上面所讨论的下推自动机都是 非确定性下推自动机(Nondeterminstic Pushdown Automata, NPDA) ,习惯上,当我们不加定语地说下推自动机的时候,指的都是非确定性下推自动机。

下面我们来看一看 确定性下推自动机(Determinstic Pushdown Automata, DPDA) 。为了变得确定,对于任意状态 qq ,输入符号 aa 以及栈符号 XX ,最多只有一种移动的选择。

此外,在使用 ε\varepsilon 和实际选择之间,不应当存在选择关系。形式化表述,就是 δ(q,a,X)\delta(q, a, X)δ(q,ε,X)\delta(q, \varepsilon, X) 不可以同时非空。

与NFA和DFA等价不同的是,NPDA要比DPDA在表达能力上更强一些。

考虑回文串 WWRWW^R 。假设存在这样的一个DPDA,当输入走到 0m110m0^m110^m 的时候,由于我们需要确保这个字符串在11之后有和之前相同个数的0,DPDA的栈必须是空的。

假设在此之后,我们又遇到了输入 0n110n0^n110^n

  • 如果 n=mn = m ,我们应该接受它;

  • 如果 nmn \ne m ,我们应该拒绝它。

但是,之前的栈已经清空了,也就是 mm 的信息已经被忘记了,我们怎么可能判断 nnmm 是否相等呢?

不过 wcwRwcw^R 是可以用DPDA表示的,我们只需要在没遇到 cc 的时候压栈,在遇到 cc 之后弹栈即可。

定理5.4

如果 LL 是一个正则语言,存在一个DPDA PP,使得 L=L(P)L = L(P)

这个定理非常直观,因为DPDA本质上其实就是一个DFA加上一个栈,我们不使用这个栈,不就是一个DFA了嘛。

之前,DPDA是通过终止状态定义的,如果用空栈呢?在DFFA中, L(P)L(P)N(P)N(P) 并不是等价的。考虑 {0}\{0\}^* 这个语言,能够用 L(P)L(P) 表示,因为是正则语言,但并不能用 N(P)N(P) 表示。

因此,正则语言可以用 DPDA( L(P)L(P) ) 表示,进而可以用 NPDANPDA 表示;但是正则语言不一定能用 DPDA(N(P))DPDA(N(P)) 表示,不过 DPDA( N(P)N(P) ) 可以用 DPDA( L(P)L(P) ) 表示,证明过程和之前PDA的证法是一样的。

给定一个用终止状态定义的DPDA PPL=L(P)L = L(P) ,则 LL 有一个不歧义的文法。

不过,不歧义的文法不一定非得能够用一个DPDA来表示。

考虑 wwRww^RS0S01S1εS \to 0S0|1S1|\varepsilon 这个文法是无歧义的,但并不能用DPDA来表示(原因在之前已经讲过了)。

# 5.2 下推自动机与上下文无关文法的等价性

# 5.2.1 概述

我们已经讨论了正则语言的闭包性质,能够在正则表达式和有穷自动机的表达方式之间来回横跳是一件很有用的事情。类似的,上下文无关文法和下推自动机这两种表示方法对于我们后续讨论上下文无关语言的性质都很有用。

并且,由于PDA是具有算法性质的,通常通过构造PDA来论述一个语言是上下文无关语言会更容易。

比如说,看出一个PDA能够接受平衡括号是容易的,但是文法就不是那么容易看出来了,因为文法的推导自由度太大了。

定理5.5

CFG和PDA在定义语言的表达能力上是等价的。

要论证PDA和CFG的表达能力等价,我们只需要论证既能够将一个CFG转化成PDA,又能够将一个PDA转化成CFG就可以了。

# 5.2.2 CFG转化成PDA

对于CFG GG ,令 L=L(G)L = L(G) ,构造PDA PP 使得 N(P)=LN(P) = L

其中,PP 有:

  • 一个状态 qq

  • 输入符号是 GG 的终结符;

  • 栈符号是 GG 中的所有符号;

  • 起始符号是 GG 的起始符号。

直觉上, PP 的每一步应该代表某个 左句型(Left-Sentential Form, LSF) ,即最左推导中的一步。

如果 PP 的栈是 α\alphaPP 到目前为止消耗了输入中的 xx ,那么此刻的 PP 应该代表左句型 xαx\alpha

这样的话,到达空栈的时候,被消耗的输入就是 L(G)L(G) 中的一个字符串了。

具体地, PP 的转移函数如下:

  1. δ(q,a,a)={(q,ε)}\delta(q, a, a) = \{(q, \varepsilon)\}

    • 这个步骤并没有改变 PP 表示的左句型,但是将提供 aa 的责任从栈转移到输入上了。
  2. 如果 AαA \to \alphaGG 的一个产生式,则 (q,α)δ(q,ε,A)(q, \alpha) \in \delta(q, \varepsilon, A)

    • 猜测即将使用的 AA 的一个产生式,并且表示推导中的下一个左句型。

下面证明 N(P)=L(G)N(P) = L(G)

我们先证明

x,(q,wx,S)(q,x,α)Slmwα \forall x, (q, wx, S) \vdash^* (q, x, \alpha) \Leftrightarrow S \Rightarrow^*_{lm} w\alpha

先证充分性。对 PP 使用的步骤数进行归纳。

基础情况:0步,则 α=S,w=ε\alpha = S, w = \varepsilon ,并且 SlmSS \Rightarrow^*_{lm}S 永真。

归纳:考虑 PPnn 次移动, P:(q,wx,S)(q,x,α)P: (q, wx, S) \vdash^* (q, x, \alpha) ,并且假设结论对于 n1n - 1 次移动的情况都成立。

有两种情况需要考虑,取决于最后一次移动是用的第一条规则还是第二条规则。

  • 如果使用第一条规则,则移动序列一定形如 (q,yax,S)(q,ax,aα)(q,x,α)(q, yax, S) \vdash^* (q, ax, a\alpha) \vdash (q, x, \alpha) ,其中 ya=wya = w

    • 根据归纳假设,对于前 n1n - 1 步,有 SlmyaαS \Rightarrow^*_{lm} ya\alpha ,而 ya=wya = w ,则 SlmwαS \Rightarrow^*_{lm}w\alpha
  • 如果使用第二条规则,则移动序列一定形如 (q,wx,S)(q,x,Aβ)(q,x,γβ)(q, wx, S) \vdash^* (q, x, A\beta) \vdash (q, x, \gamma\beta) ,其中 AγA \to \gamma 是一个产生式,且 γβ=α\gamma\beta = \alpha

    • 根据归纳假设,对于前 n1n - 1 步,有 SlmwAβS \Rightarrow^*_{lm}wA\beta ,又 AγA\to \gamma ,从而 Slmwγβ=wαS \Rightarrow^*_{lm} w\gamma\beta = w\alpha

再证必要性,对最左推导中的步骤数进行归纳,想法是类似的,这里就不再重复了。

现在,我们已经有了对于任意的 xx(q,wx,S)(q,x,α)(q, wx, S) \vdash^* (q, x, \alpha) 当且仅当 SlmwαS \Rightarrow^*_{lm} w\alpha

特别地,令 x=α=εx = \alpha = \varepsilon ,那么 (q,w,S)(q,ε,ε)(q, w, S) \vdash^* (q, \varepsilon, \varepsilon) 当且仅当 SlmwS\Rightarrow^*_{lm} w ,也就是说 wN(P)w \in N(P) 当且仅当 wL(G)w\in L(G) ,即 N(P)=L(G)N(P) = L(G)

# 5.2.3 PDA转化成CFG

现在,假设 L=N(P)L = N(P) ,我们下面将会构造一个上下文无关文法 GG 使得 L=L(G)L = L(G)

直观来讲, GG 当中会存在变量 [pXq][pXq] 用于产生一系列输入,这些输入能够使得 PP 从状态 pp 开始,弹出栈符号 XX ,然后转到状态 qq

  • PP 在这个过程中栈顶不会走到 XX 下面。

pop-x

下面进行形式化的构造。

GG 中的变量都形如 [pXq][pXq] ,这个变量产生且仅产生使得 (p,w,X)(q,ε,ε)(p, w, X) \vdash^* (q,\varepsilon, \varepsilon) 的字符串。再次之外还有一个起始符号,我们将之后讨论。

对于 [pXq][pXq] 的每一个产生式都来源于 PP 中在状态 pp 栈顶元素 XX 的情况下的移动情况。

最简单的情况: (q,ε)δ(p,a,X)(q, \varepsilon) \in \delta(p, a, X)

  • 注意, aa 可以是一个输入符号,也可能是 ε\varepsilon

这样的话,对应的产生式就是 [pXq]a[pXq] \to a 。这里, [pXq][pXq] 产生 aa ,因为读取 aa 是弹出 XX 并从 pp 走到 qq 的一种方式。

下一种最简单的情况:对于某个状态 rr 和符号 YY(r,Y)δ(p,a,X)(r, Y) \in \delta(p, a, X)

在这种情况下, GG 有产生式 [pXq]a[rYq][pXq] \to a[rYq]

  • 我们可以我们可以擦除 XX 并从 pp 走到 qq ,通过先读一个 aa ,进入状态 rr 且用 YY 代替 XX ,然后再读取某个字符串 ww ,让 PPrr 走到 qq ,并移除 YY

x-y

第三种最简单的情况是:对于某个状态 rr 和符号 Y,ZY, Z ,有 (r,YZ)δ(p,a,X)(r, YZ) \in \delta(p, a, X)

现在,PPYZYZ 代替了 XX ,为了删除 XXPP 需要先删除 YY ,从状态 rr 走到某个状态 ss ,然后删除 ZZ ,从状态 ss 走到状态 qq

x-y-z

由于我们并不知道中间状态 ss 到底是什么(非确定性),我们必须对每一种可能的状态 ss ,生成一个产生式 [pXq]a[rYs][sZq][pXq] \to a[rYs][sZq]

这样, [pXq]auv[pXq] \Rightarrow^* auv 其中 [rYs]u[rYs] \Rightarrow^* u[sZq]v[sZq] \Rightarrow^* v

经过了三个简单情况的讨论,我们可以总结一下一般的情况了。

假设对于某个状态 rr 以及 k3k\ge 3 ,存在 (r,Y1Y2...Yk)δ(p,a,X)(r, Y_1Y_2...Y_k) \in \delta(p, a, X) ,生成

[pXq]a[rY1s1][s1Y2s2]...[sk2Yk1sk1][sk1Ykq] [pXq] \to a[rY_1s_1][s_1Y_2s_2]...[s_{k-2}Y_{k-1}s_{k-1}][s_{k-1}Y_kq]

最后我们来完成一下整个构造。我们可以证明 (q0,w,Z0)(p,ε,ε)(q_0, w, Z_0) \vdash^* (p, \varepsilon, \varepsilon) 当且仅当 [q0Z0p]w[q_0Z_0p]\Rightarrow^* w

  • 证明方法是两个简单的归纳,这里就不再证了。

不过,状态 pp 可以是任何一个状态,因为我们求的是 N(P)N(P)

最后,在 GG 中加入另一个变量 SS ,作为起始符号,并且对每一个状态 pp ,添加产生式 S[q0Z0p]S \to [q_0Z_0p]

最后,我们再回忆一下之前的那个例子,设计一个PDA,能够处理 if-else 语句,在 else 超过 if 的时候停下来。

if-else

用上面的方法,我们可以设计产生式如下:

S[pZp],[pZp]e,[pZp]i[pZp][pZp] S \to [pZp], [pZp] \to e, [pZp] \to i[pZp][pZp]

化简后即是 SeiSSS \to e | iSS