📜  多带不确定性图灵机模拟器

📅  最后修改于: 2021-05-20 06:17:40             🧑  作者: Mango

本文讨论了计算机科学(CS)的理论和实践问题。它回顾了图灵机(TM),这是自动机的基本类别,并提出了针对TM的广泛变体的模拟器:不确定性的多磁带。通过计算树的广度优先搜索(BFS)模拟不确定性。

该模拟器是用Python 3编写的,并结合了面向对象和函数式编程技术,充分利用了该编程语言的强大功能和表达能力。

组织如下。首先,以非正式的方式介绍了TM,从而突出了其在理论CS中的许多应用。然后,给出了基本模型和多带式变体的正式定义。最后,介绍了模拟器的设计和实现,并提供了其使用和执行的示例。

介绍

TM是Alan Turing在1936年设计的抽象自动机,用于研究计算的极限。 TM能够遵循简单的规则来计算函数。

TM是具有三个组成部分的原始计算模型:

  • 存储器:输入输出磁带,分为多个存储符号的离散单元。磁带上的单元格最少,但是右侧没有限制,因此可以存储的字符串长度没有限制。
  • 一个控制单元,具有一组有限的状态和一个指向当前单元的磁带头,并且可以在计算过程中向左或向右移动。
  • 存储在有限控制中的程序,用于控制机器的计算。

TM的操作包括三个阶段:

  1. 初始化。长度为N的输入字符串加载到磁带的前N个单元上。无限多个单元格的其余部分包含一个称为空白的特殊符号。机器切换到启动状态。
  2. 计算。每个计算步骤涉及:
    • 读取当前单元格中的符号(磁带头正在扫描的符号)。
    • 遵循程序定义的规则,以结合当前状态和读取的符号。这些规则称为过渡或移动,包括:(a)在当前单元格中写入新符号,(b)切换到新状态,以及(c)可选地将头一个单元格向左或向右移动。
  3. 定案。当没有关于当前状态和符号的规则时,计算将停止。如果机器处于最终状态,则TM接受输入字符串。如果当前状态为非最终状态,则TM拒绝输入字符串。请注意,并非所有TM都达到此阶段,因为TM可能永不停止在给定输入上进入无限循环。

TM在理论计算机科学中有许多应用,并且与形式语言理论密切相关。 TM是接受Chomsky语言层次结构的顶级类的语言识别器:由不受限制的语法生成的语言的类型0。它们也是语言转换器:给定一种语言的输入字符串,TM可以计算相同或不同语言的输出字符串。此功能允许TM计算其输入和输出被编码为形式语言的字符串的函数,例如,二进制数字被视为字母{0,1}上的一组字符串。

Church-Turing指出,TM能够计算出算法可以表达的任何函数。它的含义是,TM实际上是通用计算机,它们是抽象的数学设备,不受物理设备的时间和空间的限制。正如许多计算机科学家所相信的那样,如果这一论点是正确的,那么图灵发现的事实是,有些功能无法通过TM进行计算,这意味着某些功能无法通过计算机进行过去,现在或将来的算法计算。

在研究计算复杂性以及CS和数学的主要开放问题之一:P vs NP问题中,TMs也非常重要。 TM是一种方便的,独立于硬件的模型,用于根据计算过程中执行的步数(时间复杂度)或扫描的单元数(空间复杂度)来分析算法的计算复杂性。

正式定义:基本模型

图灵机(TM)是一个7元组 M=(Q, \Gamma, \Sigma, \delta, q_{0}, q_{acc}, B) 在哪里:

  • Q是一组有限的非空状态。
  • \Gamma是称为磁带字母表的一组有限的非空符号。
  • \Sigma \subseteq \Gamma-\{B\}是输入字母。
  • \delta:Q\times\Gamma \mapsto 2^{Q\times\Gamma\times\{L, R, S\}}是将状态符号对映射到三元状态,符号,头部方向(左,右或停留)的子集的过渡或下一移动函数。
  • q_{0}是开始状态。
  • q_{acc}是最终的接受状态。
  • B \in \Gamma-\Sigma是空白符号。

在计算的每个步骤中,可以通过即时描述(ID)描述TM。 ID是三元组\alpha q \beta在哪里q \in Q是实际状态\alpha \in \Gamma^{*}是机器正在扫描的单元格左侧单元格中包含的字符串,并且\beta \in \Gamma^{*}是当前单元格和磁带头右边其他单元格中包含的字符串,直到开始无限次空白序列的单元格为止。

二进制关系\vdash_{M}关联两个ID,并针对所有ID定义如下p, q \in QX, Y, Z \in \Gamma\alpha, \beta \in \Gamma^{*}

  • \alpha p X \beta \vdash_{M} \alpha Y q \beta iff (q, Y, R) \in \delta(p, X)
  • \alpha p \vdash_{M} \alpha X q iff (q, X, R) \in \delta(p, B)
  • \alpha X p Y \beta \vdash_{M} \alpha q X Z \beta iff (q, Z, L) \in \delta(p, Y)
  • \alpha X p \vdash_{M} \alpha q X Y iff (q, Y, L) \in \delta(p, B)
  • \alpha p X \beta \vdash_{M} \alpha q Y \beta iff (q, Y, S) \in \delta(p, X)
  • \alpha p \vdash_{M} \alpha q X iff (q, X, S) \in \delta(p, B)

\vdash_{M}^{*}是…的及物和反身封闭\vdash_{M} ,即在ID之间应用零个或多个转换。然后, M定义为: L(M) = \{ w \in \Sigma^{*} | q_{0}w \vdash_{M}^{*} \alpha q_{acc} \beta; \alpha, \beta \in \Gamma^{*} \}

如果适用于所有州q \in Q和磁带符号X \in \Gamma\delta(q, X) TM最多具有确定性。如果存在多个选择,那么TM是不确定的。

确定性TM的ID序列是线性的。对于不确定的TM,它形成一个计算树。可以将非确定性视为机器创建并行执行的自身副本。我们的模拟器将使用这种有用的类比。

乍一看,我们可以认为非确定性TM比确定性TM更强大,因为它能够“猜测”正确的路径。但是,事实并非如此:DTM只是NDTM的特殊情况,任何NDTM都可以转换为DTM。因此,它们具有相同的计算能力。

实际上,已经提出了TM的几种变体:带有双向无限磁带,具有多个磁道,没有停留选项等。有趣的是,所有这些变体都具有与基本模型相同的计算能力。他们承认同一类语言。

在下一部分中,我们将介绍一个非常有用的变体:多带不确定性TM。

正式定义:Multitape TM

Multitape TM具有多个带有独立磁头的输入输出带。该变体不会增加原始磁带的计算能力,但是正如我们将看到的那样,它可以简化使用辅助磁带构建TM的过程。

K-tape TM是7元组M=(Q, \Gamma, \Sigma, \delta, q_{0}, q_{acc}, B)其中所有元素都与基本TM中的元素相同,但过渡函数是映射\delta:Q\times\Gamma^{k} \mapsto 2^{Q\times(\Gamma\times\{L, R, S\})^{k}} 。它将状态读取符号对映射到新状态对-写入符号+方向对的子集。

例如,下面的2磁带TM计算第一磁带中以一元符号存储的数字的总和。第一个磁带包含因素:代表自然数的1序列由0分隔。机器将所有1写入磁带2,计算所有因素的总和。

正式地,让M=(\{q_0, q_1\}, \{0, 1, \#\}, \{0, 1\}, \delta, q_0, q_1, \#)在哪里\delta定义如下:

  • 跳过所有0: \delta(q_0, (0, \#)) = \{(q_0, (0, R), (\#, S))\}
  • 将1复制到磁带2: \delta(q_0, (1, \#)) = \{(q_0, (1, R), (1, R))\}
  • 达到空白时停止: \delta(q_0, (\#, \#)) = \{(q_1, (\#, S), (\#, S))\}

停顿问题

TM对于某些输入可能不会停止运行。例如,考虑TM M=(\{q_{0}, q_{acc}\}, \{\#\}, \emptyset, \delta, q_{0}, q_{acc}, \#)\delta(q_{0}, \#)=\{(q_{0}, \#, S)\}

暂停问题指出,无法确定任意TM是否会在给定的输入字符串上暂停。这个问题具有深远的意义,因为它表明存在TM无法计算的问题,如果Church-Turing命题是正确的,则意味着没有算法可以解决该问题。

对于TM模拟器来说,这是个坏消息,因为这意味着模拟器可能会陷入无限循环。

我们不能完全避免这个问题,但是我们可以解决它的受限形式。考虑非确定性TM的情况,如果计算树的分支进入无限循环并永远增长,直到其他分支达到最终状态。在这种情况下,模拟器应停止接受输入字符串。但是,如果我们以深度优先搜索(DFS)的方式遍历树,则当模拟器进入无限分支之一时,它将陷入困境。为了避免这种情况,模拟器将通过广度优先搜索(BFS)遍历计算树。 BFS是一种图遍历策略,它在探究分支的所有子级之前,先探究其所有子级。

Python的多磁带NDTM模拟器

在本节中,我们将介绍用于不确定性TM的模拟器,该模拟器具有多个用Python编写的磁带。

该模拟器包括两个类:Tape类和NDTM类。

磁带实例包含当前扫描的单元列表和磁带头索引,并提供以下操作:

  • readSymbol():返回由头部扫描的符号;如果头部在最后扫描的单元格中,则返回空白
  • writeSymbol():将头部扫描的符号替换为另一个符号。如果头部位于最后扫描的单元格中,则将符号附加到符号列表的末尾。
  • moveHead():将头部向左(-1),向右(1)或无位置(0)的位置移动一个位置。
  • clone():创建磁带的副本或副本。这对于模拟不确定性非常有用

NDTM实例具有以下属性:

  • 起始状态和最终状态。
  • 当前状态。
  • 磁带列表(磁带对象)。
  • 过渡字典。

转换函数是用字典实现的,该字典的键是元组(状态,read_symbols),其值是元组列表(new_state,moves)。例如,以以前出现的一元符号添加数字的TM将表示为:

{('q0', ('1', '#')): [('q0', (('1', 'R'), ('1', 'R')))],
 ('q0', ('0', '#')): [('q0', (('0', 'R'), ('#', 'S')))],
 ('q0', ('#', '#')): [('q1', (('#', 'S'), ('#', 'S')))]}

请注意,由于Python的通用数据结构(如字典,元组和列表), Python表示形式与转换函数的数学定义非常相似。 dict的一个子类,标准集合模块中的defaultdict,用于减轻初始化的负担。

NDTM对象包含用于读取磁带中当前符号元组,添加,获取和执行过渡以及制作当前TM副本的方法。

NDTM的主要方法是accepts()。它的参数是输入字符串,如果计算树的任何分支到达接受状态,则返回NDTM对象;如果没有分支则返回NDTM对象。它通过广度优先搜索(BFS)遍历计算树,以允许在任何分支到达接受状态时停止计算。 BFS使用队列来跟踪挂起的分支。来自collections模块的Python deque用于在队列操作中获得O(1)性能。算法如下:

Add the TM instance to the queue
While queue is not empty:
   Fetch the first TM from the queue
   If there is no transition for the current state and read symbols:
      If the TM is in a final state: return TM
   Else:
      If the transition is nondeterministic:
         Create replicas of the TM and add them to the queue
      Execute the transition in the current TM and add it to the queue

最后,NDTM类具有一些方法,可将TM表示形式打印为瞬时描述的集合,并从文件中解析TM规范。像往常一样,此输入/输出功能是模拟器中最繁琐的部分。

规范文件具有以下语法

% HEADER: mandatory
start_state final_state blank number_of_tapes
% TRANSITIONS
state read_symbols new_state write_symbol, move write_symbol, move ...

以“%”开头的行被视为注释。例如,以一元表示法添加数字的TM具有以下规范文件:

% HEADER
q0 q1 # 2
% TRANSITIONS
q0 1, # q0 1, R 1, R
q0 0, # q0 0, R #, S
q0 #, # q1 #, S #, S

状态和符号可以是任何不包含空格或逗号的字符串。

可以从Python会话运行模拟器,以探索输出配置。例如,如果先前的文件以名称“ 2sum.tm”保存:

from NDTM import NDTM
tm = NDTM.parse('2sum.tm')
print(tm.accepts('11011101'))

输出显示模拟器在磁带#1中产生了1的总和:

Output :
q1: ['1', '1', '0', '1', '1', '1', '0', '1']['#']
q1: ['1', '1', '1', '1', '1', '1']['#']

输出显示两个磁带的内容,磁头的位置(每个磁带的第二个列表)以及TM的最终状态。

模拟器的源代码

除去输入/输出代码和注释,该模拟器少于100行代码。它证明了Python的强大功能和经济性。它是面向对象的,但也使用诸如列表推导之类的功能构造。

####
# NDTM.py: a nondeterministic Turing Machine Simulator
# Author: David Gil del Rosal (dgilros@yahoo.com)
#### from collections import defaultdict, deque
  
class Tape:
    # Constructor. Sets the blank symbol, the
    # string to load and the position of the tape head
    def __init__(self, blank, string ='', head = 0):
        self.blank = blank
        self.loadString(string, head)
      
    # Loads a new string and sets the tape head    
    def loadString(self, string, head):
        self.symbols = list(string)
        self.head = head
          
    # Returns the symbol on the current cell, or the blank
    # if the head is on the start of the infinite blanks
    def readSymbol(self):
        if self.head < len(self.symbols):
            return self.symbols[self.head]
        else:
            return self.blank
          
    # Writes a symbol in the current cell, extending
    # the list if necessary
    def writeSymbol(self, symbol):
        if self.head < len(self.symbols):
            self.symbols[self.head] = symbol
        else:
            self.symbols.append(symbol)
              
    # Moves the head left (-1), stay (0) or right (1)
    def moveHead(self, direction):
        if direction == 'L': inc = -1
        elif direction == 'R': inc = 1
        else: inc = 0
        self.head+= inc
          
    # Creates a new tape with the same attributes than this
    def clone(self):
        return Tape(self.blank, self.symbols, self.head)
      
    # String representation of the tape
    def __str__(self):
        return str(self.symbols[:self.head]) + \
               str(self.symbols[self.head:])
      
  
class NDTM:
    # Constructor. Sets the start and final states and
    # inits the TM tapes
    def __init__(self, start, final, blank ='#', ntapes = 1):
        self.start = self.state = start
        self.final = final
        self.tapes = [Tape(blank) for _ in range(ntapes)]
        self.trans = defaultdict(list)
  
    # Puts the TM in the start state and loads an input
    # string into the first tape
    def restart(self, string):
        self.state = self.start
        self.tapes[0].loadString(string, 0)
        for tape in self.tapes[1:]:
            tape.loadString('', 0)
                      
    # Returns a tuple with the current symbols read
    def readSymbols(self):
        return tuple(tape.readSymbol() for tape in self.tapes)
  
    # Add an entry to the transaction table
    def addTrans(self, state, read_sym, new_state, moves):
        self.trans[(state, read_sym)].append((new_state, moves))
      
    # Returns the transaction that corresponds to the
    # current state & read symbols, or None if there is not
    def getTrans(self):
        key = (self.state, self.readSymbols())
        return self.trans[key] if key in self.trans else None
          
    # Executes a transaction updating the state and the
    # tapes. Returns the TM object to allow chaining    
    def execTrans(self, trans):
        self.state, moves = trans
        for tape, move in zip(self.tapes, moves):
            symbol, direction = move
            tape.writeSymbol(symbol)
            tape.moveHead(direction)
        return self
      
    # Returns a copy of the current TM
    def clone(self):
        tm = NDTM(self.start, self.final)
        tm.state = self.state
        tm.tapes = [tape.clone() for tape in self.tapes]
        tm.trans = self.trans        # shallow copy
        return tm
          
    # Simulates the TM computation. Returns the TM that
    # accepted the input string if any, or None.
    def accepts(self, string):
        self.restart(string)
        queue = deque([self])
        while len(queue) > 0:
            tm = queue.popleft()
            transitions = tm.getTrans()
            if transitions is None:
                # there are not transactions. Exit
                # if the TM is in the final state
                if tm.state == tm.final: return tm
            else:
                # If the transaction is not deterministic
                # add replicas of the TM to the queue
                for trans in transitions[1:]:
                    queue.append(tm.clone().execTrans(trans))
                # execute the current transition
                queue.append(tm.execTrans(transitions[0]))
        return None
      
    def __str__(self):
        out = ''
        for tape in self.tapes:
            out+= self.state + ': ' + str(tape)  + '\n'
        return out
      
    # Simple parser that builds a TM from a text file
    @staticmethod
    def parse(filename):
        tm = None
        with open(filename) as file:
            for line in file:
                spec = line.strip()
                if len(spec) == 0 or spec[0] == '%': continue
                if tm is None:
                    start, final, blank, ntapes = spec.split()
                    ntapes = int(ntapes)
                    tm = NDTM(start, final, blank, ntapes)
                else:
                    fields = line.split()
                    state = fields[0]
                    symbols = tuple(fields[1].split(', '))
                    new_st = fields[2]
                    moves = tuple(tuple(m.split(', '))
                                  for m in fields[3:])
                    tm.addTrans(state, symbols, new_st, moves)
        return tm
      
if __name__ == '__main__':
    # Example TM that performs unary complement
    tm = NDTM('q0', 'q1', '#')
    tm.addTrans('q0', ('0', ), 'q0', (('1', 'R'), ))
    tm.addTrans('q0', ('1', ), 'q0', (('0', 'R'), ))
    tm.addTrans('q0', ('#', ), 'q1', (('#', 'S'), ))
    acc_tm = tm.accepts('11011101')
    if acc_tm: print(acc_tm)
    else: print('NOT ACCEPTED')    

一个不平凡的例子

作为最后一个示例,我们介绍了可识别非上下文无关语言的3-tape TM的规范\{ ww | w \in (0+1)^{*} \}

TM不确定地复制磁带#2中字符串的前半部分和磁带#3中后半部分的内容。然后,继续检查两个部分是否匹配。

% 3-tape NDTM that recognizes L={ ww | w in {0, 1}* }
q0 q4 # 3
% TRANSITIONS
% put left endmarkers on tapes #2 and #3
q0 0, #, # q1 0, S $, R $, R
q0 1, #, # q1 1, S $, R $, R
% first half of string: copy symbols on tape #2
q1 0, #, # q1 0, R 0, R #, S
q1 1, #, # q1 1, R 1, R #, S
% guess second half of string: copy symbols on tape #3
q1 0, #, # q2 0, R #, S 0, R
q1 1, #, # q2 1, R #, S 1, R
q2 0, #, # q2 0, R #, S 0, R
q2 1, #, # q2 1, R #, S 1, R
% reached end of input string: switch to compare state
q2 #, #, # q3 #, S #, L #, L
% compare strings on tapes #2 and #3
q3 #, 0, 0 q3 #, S 0, L 0, L
q3 #, 1, 1 q3 #, S 1, L 1, L
% if both strings are equal switch to final state
q3 #, $, $ q4 #, S $, S $, S

用法示例。将上面的文件另存为“ 3ww.tm”,然后运行以下代码:

from NDTM import NDTM
tm = NDTM.parse('3ww.tm')
print(tm.accepts('11001100'))

产生的输出符合预期:TM达到了最终状态,并且输入字符串的两半的内容位于磁带#2和#3中。

Output :
q4: ['1', '1', '0', '0', '1', '1', '0', '0']['#']
q4: []['$', '1', '1', '0', '0', '#']
q4: []['$', '1', '1', '0', '0', '#'] 

一个有趣的练习是尝试将此TM转换为单磁带非确定性TM甚至是单磁带确定性TM。这是完全有可能的,但是规范会繁琐得多。这是拥有多个磁带的实用程序:没有更多的计算能力,但是更加简单。