Python es una máquina de Turing

Construimos el puente riguroso entre programas concretos en Python y máquinas de Turing abstractas mediante una cadena de simulaciones: máquina de Turing monocinta → máquina de Turing multicinta → máquina de acceso aleatorio → ordenador moderno → programa en Python → máquina de Turing monocinta. Esta equivalencia justifica el uso de Python como modelo formal de computación a lo largo de la serie, y sitúa el resultado dentro de la tesis de Church–Turing.

47 min de lectura

Hemos perfeccionado el ordenador de propósito general para cálculos científicos. Ahora intentamos emplear el mismo dispositivo con pequeñas modificaciones en una gran variedad de situaciones diferentes para las cuales no fue diseñado en primer lugar. En estas circunstancias, si llegara a resultar que la lógica básica de una máquina diseñada para la solución numérica de ecuaciones diferenciales coincide con la lógica básica de una máquina destinada a hacer facturas para una tienda departamental, consideraría esto como la coincidencia más asombrosa que haya encontrado.

Howard Aiken1

La máquina de Turing, inventada por Alan Turing en 19372, es un modelo matemático sencillo que pretende representar la noción de algoritmo, y del que hablaremos en detalle en la Sección 2.2. Esta máquina es una de las herramientas más relevantes en las ciencias de la computación, pues su simplicidad permite demostrar resultados importantes, como la indecidibilidad del problema de parada (Proposición 3.24), a partir del cual podremos adentrarnos en la cuestión principal de nuestro trabajo: la incompletitud de las matemáticas. Sin embargo, esta potencia formal tiene una desventaja, que comprobaremos a partir de un simple ejemplo (Ejemplo 2.11): definir programas sencillos en este modelo es complejo y tedioso.

Este inconveniente es el que motiva este capítulo: en la Sección 2.3 probaremos la equivalencia entre los programas de ordenador (en nuestro caso, los programas en Python, que exploraremos en la Sección 2.1) y las máquinas de Turing. Esto nos permitirá ver un programa en Python como una representación de una máquina de Turing, eliminando el principal inconveniente y quedándonos con la potencia matemática ya comentada.

Es importante resaltar que los resultados que aquí aparecen son perfectamente generalizables a cualquier lenguaje de programación Turing-completo. Sin embargo, decidimos usar este lenguaje por su facilidad de comprensión y de uso.

Tanto el resultado de equivalencia que probamos en este capítulo como la observación anterior son especialmente interesantes, ya que guardan una fuerte relación con la tesis de Church–Turing, de la que hablaremos brevemente en la Sección 2.4.

2.1 #Programas en Python

Python3 es un lenguaje de programación de alto nivel, interpretado, orientado a objetos y open source, ampliamente usado en la actualidad para una gran variedad de aplicaciones. Una de sus principales características es su legibilidad y facilidad de uso: cualquier persona experimentada en otros lenguajes podrá comprender y redactar programas en Python con mucha sencillez.

Usando Python, podemos escribir una gran variedad de programas. Para el propósito de nuestro estudio, nos centraremos en una clase de programas específica: aquella que tiene únicamente una cadena de caracteres como entrada y una cadena de caracteres como salida. Los llamaremos programas SISO (del inglés, Single Input–Single Output; véase Definición 2.2)4. En breve, comprobaremos cómo esta clase no es restrictiva: estos tipos de programas son tan expresivos como cualquier otro programa. De hecho, en este trabajo usaremos los términos «programa» y «programa SISO» indistintamente, y en ambas ocasiones nos referiremos a un programa en Python de tipo SISO.

Definición2.1—Función SISO

Definimos una función SISO como una función de Python que acepta un único parámetro de tipo str como entrada y devuelve un str.

Definición2.2—Programa en Python de tipo SISO

Respecto a un sistema informático de referencia CC, definimos un programa en Python de tipo SISO como una cadena de caracteres PP tal que:

  • PP es código en Python sintácticamente correcto;
  • PP define al menos una función, y la primera de ellas es de tipo SISO. A esta función la llamaremos función main.

Observemos que en la definición anterior se especifica un sistema informático CC, pero no se define de forma explícita. Podemos pensar en CC como cualquier sistema informático, con cualquier configuración de software y hardware, y con tanta memoria como sea necesaria.

También podemos preguntarnos qué queremos decir cuando nos referimos a un programa como «sintácticamente correcto», dado que esto dependerá de varios factores, como la versión de Python instalada en CC, así como el sistema operativo, arquitectura, etc. Es posible que un programa se ejecute en un sistema CC y falle o incluso dé una salida diferente en otro sistema C′C'. Sin embargo, y como veremos más adelante, usaremos estos programas como representaciones de máquinas de Turing. Los programas que presentamos aquí son independientes de arquitectura y de memoria, y las demostraciones que figuran en este trabajo son independientes de todas estas características.

También necesitaremos definir cuál es la salida de un programa en Python.

Definición2.3—Salida de un programa

Sea PP un programa en Python respecto a un sistema informático CC. Sea MM la función main de PP, e II la entrada de PP, una cadena de caracteres. La salida de PP respecto a la entrada II, P(I)P(I), se produce usando CC para ejecutar PP con entrada II, y se define como:

  • Si MM devuelve una cadena de caracteres OO, entonces P(I)=OP(I) = O.
  • Si MM devuelve cualquier otro objeto en Python, entonces P(I)P(I) no está definido.
  • Si MM lanza una excepción,5 entonces P(I)P(I) no está definido.
  • Si MM no termina, entonces P(I)P(I) no está definido.

Nota. La definición anterior puede generalizarse fácilmente para nn entradas. Reemplazando II por I1,I2,…,InI_1, I_2, \ldots, I_n obtenemos la definición correspondiente para P(I1,I2,…,In)P(I_1, I_2, \ldots, I_n).

En otras palabras, la salida de un programa es la cadena de caracteres devuelta por su función main en caso de que lo haga, y no estará definida en caso contrario.

Al definir los programas en Python que usaremos en este trabajo, nos centramos en el uso de cadenas de caracteres (también llamadas strings) como entrada y salida, y evitamos el uso de otros objetos de Python que no sean strings. Inicialmente, podemos pensar que esto impone una limitación sobre los programas en Python que podemos escribir, pero esto realmente no es así. Para ello, basta observar que cualquier objeto en Python puede codificarse debidamente mediante una cadena de caracteres. Existen librerías nativas que permiten hacer esto, como pickle.6 Un ejemplo de esto se muestra en el Programa 2.1. La función function_with_encoded_object es una función main.

import pickle
from base64 import b64decode, b64encode
 
def function_with_encoded_object(encoded: str) -> str:
decoded = pickle.loads(b64decode(encoded.encode('ascii')))
return str(decoded)
 
adjacencies = {1: 2, 3: 4, 5: 3, 4: 2}
adjacencies_encoded = b64encode(pickle.dumps(adjacencies)).decode('ascii')
function_with_encoded_object(adjacencies_encoded)

pickle.dumps serializa el objeto a bytes, no a str. Usamos Base64 para representar esos bytes como una cadena ASCII y los recuperamos antes de llamar a pickle.loads. La función también devuelve una cadena, de modo que tanto la entrada como la salida cumplen la definición SISO.

En resumen, hemos visto cómo la condición de ser SISO para un programa en Python no es realmente una restricción. Sin embargo, esta clase de problemas nos resultará muy conveniente, como veremos más adelante.

2.2 #Máquinas de Turing

Las máquinas de Turing, primero descritas por Alan Turing en 19362, son dispositivos computacionales abstractos y simples creados para ayudar a investigar qué problemas pueden ser computados. Las «máquinas automáticas» —tal como Turing las llamó en 1936— fueron específicamente diseñadas para computar números reales. Estas máquinas no fueron nombradas «máquinas de Turing» hasta 1937, cuando Alonzo Church7 revisó la publicación original de Turing. Hoy se consideran uno de los modelos fundamentales de la teoría de la computación, pues son la representación matemática de un algoritmo8.

Antes de comprender cómo funcionan estas máquinas, haremos unas definiciones previas.

Definición2.4—Alfabeto, símbolo, palabra, longitud, palabra vacía

Un alfabeto AA es un conjunto finito. Sus elementos se llaman símbolos (o letras).

Una palabra sobre el alfabeto AA es una sucesión finita de elementos de AA. Es decir, uu es una palabra de AA si y solo si u=a1 a2 … anu = a_1\,a_2\,\ldots\,a_n, con ai∈Aa_i \in A para todo i=1,2,…,ni = 1, 2, \ldots, n.

El conjunto de todas las palabras sobre un alfabeto se denota A∗A^*.9

La longitud de una palabra uu es el número de símbolos que contiene, y se denota ∥u∥\|u\|. Para u=a1 a2 … anu = a_1\,a_2\,\ldots\,a_n, es ∥u∥=n\|u\| = n.

La palabra vacía es la palabra de longitud cero. Es la misma para todos los alfabetos, y se denota ϵ\epsilon.

La primera definición presentada por Turing en 19362 dista de la que se presenta en la Definición 2.5. Sin embargo, ambas son equivalentes.

Definición2.5—Máquina de Turing

Una máquina de Turing es una séptupla M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F), donde:

  • Q={q0,…}Q = \{q_0, \ldots\} es un conjunto (finito) de estados.
  • AA es un alfabeto de entrada.
  • BB es un alfabeto de símbolos de la cinta (o alfabeto de trabajo), que incluye a AA (es decir, A⊂BA \subset B).
  • δ:Q×B⟶(Q×B×{L,R,S})∪{∅}\delta : Q \times B \longrightarrow (Q \times B \times \{L, R, S\}) \cup \{\emptyset\} es la función de transición que asigna a cada estado q∈Qq \in Q y símbolo b∈Bb \in B un valor δ(q,b)\delta(q, b) que puede ser:
  • ∅\emptyset si no está definido, o
  • una tripleta (p,c,M)(p, c, M), donde p∈Qp \in Q, c∈Bc \in B, M∈{L,R,S}M \in \{L, R, S\}.
  • q0∈Qq_0 \in Q es el estado inicial.
  • ⊔∈B∖A\sqcup \in B \setminus A es el símbolo blanco.
  • F⊆QF \subseteq Q es el conjunto de estados finales.

La definición anterior es un tanto abstracta, y podemos visualizarla mejor en la Figura 2.1. En ella, vemos las tres partes «físicas» de la máquina: la unidad de control, el cabezal y la cinta. La cinta contiene casillas, cada una de las cuales puede almacenar un símbolo. Inicialmente, los símbolos son los del alfabeto de entrada, pero conforme la máquina va ejecutando instrucciones se puede usar el alfabeto de trabajo. Estos símbolos incluyen un símbolo blanco, denotado ⊔\sqcup, y se encuentran en todas las posiciones de la cinta que no codifican la palabra de entrada. El cabezal puede moverse por la cinta a la izquierda y a la derecha (o quedarse en el sitio), informando a la unidad de control de los símbolos que ve. El símbolo al que apunta el cabezal lo llamaremos símbolo escaneado. El cabezal puede reemplazar el símbolo escaneado por cualquier otro del alfabeto de trabajo, incluyendo el símbolo blanco. La unidad de control manda transiciones al cabezal, y cambia entre los diferentes estados en función de los símbolos leídos por el cabezal.

qn¡1qnq0q1q2t010t::::::entradaunidaddecontrolestadoactualcintacabezal
Figura 2.1. Configuración inicial de una máquina de Turing para la entrada 010010.

Es importante aclarar que la máquina de Turing realmente no tiene ninguna «unidad de control» o «cabezal»: es una descripción matemática. Lo que hemos hecho es proporcionar una especificación intuitiva de cómo se ejecuta el algoritmo que representa tal máquina.

Para describir la máquina en cada momento, necesitaremos el concepto de configuración.

Definición2.6—Configuración de una máquina de Turing

Especificaremos la configuración de una máquina de Turing mediante dos parámetros:

  • El estado q∈Qq \in Q en el que se encuentra la máquina.
  • La palabra w∈B∗w \in B^* presente en la cinta y la posición del cabezal de lectura, que representaremos recuadrando el símbolo donde se encuentra el cabezal. Dado que la cinta se extiende infinitamente a ambos lados, eliminamos los caracteres blancos a la izquierda y a la derecha hasta llegar a la posición del cabezal.

Representaremos una configuración mediante

q  :  wq\;:\;w

con uno de los símbolos de ww debidamente recuadrado.

Lo único que nos queda es precisar la noción de «paso de cálculo» que comentamos anteriormente. En cada paso, aplicamos una transición y cambiamos la configuración de la máquina. Esto es lo que denominamos proceso de cálculo, y formalizamos a continuación.

Definición2.7—Proceso de cálculo de una máquina de Turing

Dada una máquina de Turing M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F) y dada una entrada inicial u=u1 u2 … un∈A∗u = u_1\,u_2\,\ldots\,u_n \in A^*, definimos su configuración inicial como:

q0  :  u1 u2 ... unq_0\;:\;\boxed{u_1}\:u_2\:...\:u_n

A continuación, definimos un paso de cálculo para cada configuración de la máquina:

q  :  b−n ... b−2 b−1 b0 b1 b2 ... bmq\;:\;b_{-n}\:...\:b_{-2}\:b_{-1}\:\boxed{b_0}\:b_1\:b_2\:...\:b_m

con q∈Qq \in Q, b=b−n ... b−2 b−1 b0 b1 b2 ... bm∈B∗b = b_{-n}\:...\:b_{-2}\:b_{-1}\:b_0\:b_1\:b_2\:...\:b_m \in B^*, en función del valor de la función de transición δ(q,b0)\delta(q, b_0):

  • Si δ(q,b0)\delta(q, b_0) no está definido (δ(q,b0)=∅\delta(q, b_0) = \emptyset), la máquina para.
  • Si δ(q,b0)=(p,c,M)\delta(q, b_0) = (p, c, M) con p∈Qp \in Q, c∈Bc \in B, M∈{L,R,S}M \in \{L, R, S\}, la máquina reemplazará el símbolo b0b_0 presente en la posición de la cinta en la que se encuentra el cabezal por cc, moverá el cabezal una posición de acuerdo a MM (a la izquierda, derecha o no se moverá si es LL, RR o SS, respectivamente), y cambiará su estado a pp:
M=L⇒p  :  b−n ... b−2 b−1 c b1 b2 ... bmM=R⇒p  :  b−n ... b−2 b−1 c b1 b2 ... bmM=S⇒p  :  b−n ... b−2 b−1 c b1 b2 ... bm\begin{matrix} M=L & \Rightarrow & p\;:\;b_{-n}\:...\:b_{-2}\:\boxed{b_{-1}}\:c\:b_1\:b_2\:...\:b_m\\ M=R & \Rightarrow & p\;:\;b_{-n}\:...\:b_{-2}\:b_{-1}\:c\:\boxed{b_1}\:b_2\:...\:b_m\\ M=S & \Rightarrow & p\;:\;b_{-n}\:...\:b_{-2}\:b_{-1}\:\boxed{c}\:b_1\:b_2\:...\:b_m \end{matrix}

El proceso de cálculo se define como la sucesión de pasos de cálculo, comenzando por la configuración inicial.

Es posible que la máquina pare cuando llega a una configuración tal que la función de transición no está definida.10 Si esto no ocurre, la sucesión de pasos de cálculo será infinita y la máquina no parará nunca.

En definitiva, vemos que la máquina, en función del estado en el que se encuentre y el símbolo escaneado, ejecutará una cierta transición, definida en δ\delta, que le dirá a qué estado pasar, por qué símbolo reemplazar el escaneado y el movimiento que tiene que realizar sobre la cinta.

Una nota importante respecto a las transiciones no definidas. A la hora de describir una función de transición, definiremos únicamente las transiciones para los estados q∈Qq \in Q y símbolos b∈Bb \in B para los que están definidas. En caso contrario, podemos asumir que δ(q,b)=∅\delta(q, b) = \emptyset. Puedes ver esto en el Ejemplo 2.11, que comentaremos en detalle más adelante.

Introducimos una notación gráfica para la función de transición. La definiremos mediante un grafo dirigido: representaremos los estados como nodos (uno para cada estado) y las transiciones como arcos dirigidos.

El estado inicial se simbolizará, además de en la definición de la máquina, con una flecha apuntando al estado (no es un arco, sino que no proviene de ningún nodo):

q0

Para cada transición δ(q,b)=(p,c,M)\delta(q,b)=(p,c,M), insertaremos un arco dirigido conectando los nodos correspondientes a los estados qq y pp como sigue:

qpb;c;M

En caso de que tengamos varias funciones de transición entre ambos estados (δ(q,b)=(p,c,M)\delta(q,b)=(p,c,M) y δ(q,b′)=(p,c′,M′)\delta(q,b')=(p,c',M')), colocaremos varias líneas en el arco para simbolizarlo:

qpb;c;Mb0;c0;M0

Finalmente, también indicaremos los estados finales rodeando dos veces el nodo:

qF

Observemos cómo una máquina de Turing, dada una entrada, puede:

  • parar y aceptar,
  • parar y rechazar, o
  • no parar (ciclar).

La máquina para cuando llega a una configuración para la que no hay una transición definida. Cuando ocurre esto, podemos definir una salida con los símbolos que quedan en la cinta.

Definición2.8—Salida de una máquina de Turing

Para una máquina de Turing MM, definimos su salida como los contenidos de la cinta una vez que la máquina ha parado, omitiendo los símbolos blancos a ambos lados.

Definición2.9—Palabra aceptada por una máquina de Turing

Dada una máquina de Turing M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F) y dada una entrada inicial u∈A∗u \in A^*, decimos que MM acepta uu si MM para con entrada uu en un estado q∈Fq \in F. Si lo hace en un estado q∉Fq \notin F, decimos que MM rechaza uu.

Al haber definido qué palabras acepta una máquina determinada, introducimos el concepto de lenguaje. Un lenguaje no es más que un conjunto de palabras.

Definición2.10—Lenguaje aceptado por una máquina de Turing

Un lenguaje LL sobre un alfabeto AA es un conjunto de palabras sobre AA.

Si M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F) es una máquina de Turing, el lenguaje aceptado, denotado como L(M)L(M), es el conjunto de palabras aceptadas por MM, es decir,

u∈L(M)  ⟺  M acepta uu \in L(M) \iff M \text{ acepta } u

Concretemos todos los conceptos que hemos introducido mediante un ejemplo.

Ejemplo2.11

Sea M#a>#bM_{\#a>\#b} una máquina de Turing tal que:

M#a>#b=({q0,qa,qb,qR,qF},{a,b},{a,b,X,⊔},δ,q0,⊔,{qF})M_{\#a>\#b} = (\{q_0, q_a, q_b, q_R, q_F\}, \{a, b\}, \{a, b, X, \sqcup\}, \delta, q_0, \sqcup, \{q_F\})

con δ\delta, su función de transición, descrita mediante el grafo:

q0qRqbqaqFX;X;Rt;t;Ra;X;Rb;X;La;a;RX;X;Rb;b;RX;X;Ra;a;Lb;b;LX;X;Lb;X;Ra;X;Lt;t;S
Figura 2.2. La función de transición δ\delta de M#a>#bM_{\#a>\#b}.

Equivalentemente podemos definir la función de transición explícitamente:

δ(q0,a)=(qa,X,R)δ(q0,b)=(qb,X,R)δ(q0,X)=(q0,X,R)δ(qa,a)=(qa,a,R)δ(qa,b)=(qR,X,L)δ(qa,X)=(qa,X,R)δ(qa,⊔)=(qF,⊔,S)δ(qb,a)=(qR,X,L)δ(qb,b)=(qb,b,R)δ(qb,X)=(qb,X,R)δ(qR,a)=(qR,a,L)δ(qR,b)=(qR,b,L)δ(qR,X)=(qR,X,L)δ(qR,⊔)=(q0,⊔,R)\begin{matrix} \delta(q_0, a) = (q_a,X,R) & \delta(q_0, b) = (q_b,X,R) & \delta(q_0, X) = (q_0,X,R) & \\ \delta(q_a, a) = (q_a,a,R) & \delta(q_a, b) = (q_R,X,L) & \delta(q_a, X) = (q_a,X,R) & \delta(q_a, \sqcup) = (q_F,\sqcup,S) \\ \delta(q_b, a) = (q_R,X,L) & \delta(q_b, b) = (q_b,b,R) & \delta(q_b, X) = (q_b,X,R) & \\ \delta(q_R, a) = (q_R,a,L) & \delta(q_R, b) = (q_R,b,L) & \delta(q_R, X) = (q_R,X,L) & \delta(q_R, \sqcup) = (q_0,\sqcup,R) \\ \end{matrix}

Veamos qué hace esta máquina. Empieza al principio de la cinta, y escanea los símbolos uno a uno, de izquierda a derecha. Hay tres posibilidades, dependiendo de si vemos aa, bb o ⊔\sqcup:

  1. Si se lee una bb:
  2. Reemplazar la bb con una XX.
  3. Continuar escaneando hacia la derecha.
  4. Si encontramos una aa, reemplazarla con una XX. Habremos reemplazado el mismo número de aa que de bb, así que nos movemos al inicio de la cinta y reiniciamos el algoritmo.
  5. Si no encontramos una aa, tenemos una bb de más, así que rechazamos.
  6. Si se lee una aa:
  7. Reemplazar la aa con una XX.
  8. Continuar escaneando hacia la derecha.
  9. Si encontramos una bb, reemplazarla con una XX. Habremos reemplazado el mismo número de aa que de bb, así que nos movemos al inicio de la cinta y reiniciamos el algoritmo.
  10. Si no encontramos una bb, tenemos una aa de más, así que aceptamos.
  11. Si no encontramos aa o bb antes del ⊔\sqcup al final de la entrada: el número de aa reemplazados equivale al número de bb reemplazados, así que rechazamos.

El lenguaje aceptado por esta máquina es el conjunto de palabras de {a,b}∗\{a,b\}^* con un número de aa mayor que de bb:11

L(M#a>#b)={u∈{a,b}∗  :  #(u,a)>#(u,b)}L(M_{\#a>\#b}) = \left\{ u \in \{a,b\}^* \;:\; \#(u,a) > \#(u,b) \right\}

Veamos un ejemplo de palabra aceptada: abbaaabbaa. Los pasos que da la máquina de Turing configuración tras configuración serán los siguientes:

q0  :  a b b a a→qa  :  X b b a a→qR  :  X X b a a→qR  :  ⊔ X X b a a→q0  :  X X b a a→q0  :  X X b a a→q0  :  X X b a a→qb  :  X X X a a→qR  :  X X X X a→qR  :  X X X X a→qR  :  X X X X a→qR  :  ⊔ X X X X a→q0  :  X X X X a→q0  :  X X X X a→q0  :  X X X X a→q0  :  X X X X a→q0  :  X X X X a→qa  :  X X X X X ⊔→qF  :  X X X X X ⊔\begin{matrix} q_0\;:\;\boxed{a}\:b\:b\:a\:a & \rightarrow & q_a\;:\;X\:\boxed{b}\:b\:a\:a & \rightarrow & q_R\;:\;\boxed{X}\:X\:b\:a\:a & \rightarrow & q_R\;:\;\boxed{\sqcup}\:X\:X\:b\:a\:a \\ & \rightarrow & q_0\;:\;\boxed{X}\:X\:b\:a\:a & \rightarrow & q_0\;:\;X\:\boxed{X}\:b\:a\:a & \rightarrow & q_0\;:\;X\:X\:\boxed{b}\:a\:a \\ & \rightarrow & q_b\;:\;X\:X\:X\:\boxed{a}\:a & \rightarrow & q_R\;:\;X\:X\:\boxed{X}\:X\:a & \rightarrow & q_R\;:\;X\:\boxed{X}\:X\:X\:a \\ & \rightarrow & q_R\;:\;\boxed{X}\:X\:X\:X\:a & \rightarrow & q_R\;:\;\boxed{\sqcup}\:X\:X\:X\:X\:a & \rightarrow & q_0\;:\;\boxed{X}\:X\:X\:X\:a \\ & \rightarrow & q_0\;:\;X\:\boxed{X}\:X\:X\:a & \rightarrow & q_0\;:\;X\:X\:\boxed{X}\:X\:a & \rightarrow & q_0\;:\;X\:X\:X\:\boxed{X}\:a \\ & \rightarrow & q_0\;:\;X\:X\:X\:X\:\boxed{a} & \rightarrow & q_a\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} & \rightarrow & q_F\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} \end{matrix}

Como la máquina para en el estado qF∈Fq_F \in F, la palabra es aceptada.

Veamos un ejemplo de una palabra no aceptada, como es abbabb:

q0  :  a b b→qa  :  X b b→qR  :  X X b→qR  :  ⊔ X X b→q0  :  X X b→q0  :  X X b→q0  :  X X b→qb  :  X X X ⊔\begin{matrix} q_0\;:\;\boxed{a}\:b\:b & \rightarrow & q_a\;:\;X\:\boxed{b}\:b & \rightarrow & q_R\;:\;\boxed{X}\:X\:b & \rightarrow & q_R\;:\;\boxed{\sqcup}\:X\:X\:b \\ & \rightarrow & q_0\;:\;\boxed{X}\:X\:b & \rightarrow & q_0\;:\;X\:\boxed{X}\:b & \rightarrow & q_0\;:\;X\:X\:\boxed{b} \\ & \rightarrow & q_b\;:\;X\:X\:X\:\boxed{\sqcup} \end{matrix}

Como la máquina para en el estado qb∉Fq_b \notin F, la palabra no es aceptada.

La máquina también permite la entrada vacía (ϵ\epsilon, que definimos en la Definición 2.4), en cuyo caso la sucesión de configuraciones será:

q0  :  ⊔\begin{matrix} q_0\;:\;\boxed{\sqcup} \end{matrix}

La máquina para inmediatamente en q0∉Fq_0 \notin F, y ϵ\epsilon no es aceptada. Esto es esperado, pues ϵ\epsilon tiene exactamente el mismo número de aa que de bb.

Estos ejemplos quedan resumidos en la Tabla 2.1, junto con el comportamiento de la máquina para otras palabras. Observa cuándo cada palabra es aceptada, así como el número de transiciones tras los que la máquina para.

Una vez que hemos descrito con precisión qué es una máquina de Turing, procederemos a probar el resultado más importante de este capítulo (Teorema 2.23), que ya anticipamos en la introducción: la equivalencia entre las máquinas de Turing recién expuestas y los programas en Python que definimos en la Sección 2.1.

Entrada Config. inicial Config. al parar ¿Aceptada? N.º transiciones
abbaaabbaa q0  :  a b b a aq_0\;:\;\boxed{a}\:b\:b\:a\:a →\rightarrow qF  :  X X X X X ⊔q_F\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} sí 18
abbabb q0  :  a b bq_0\;:\;\boxed{a}\:b\:b →\rightarrow qb  :  X X X ⊔q_b\;:\;X\:X\:X\:\boxed{\sqcup} no 7
ϵ\epsilon q0  :  ⊔q_0\;:\;\boxed{\sqcup} →\rightarrow q0  :  ⊔q_0\;:\;\boxed{\sqcup} no 0
abababab q0  :  a b a bq_0\;:\;\boxed{a}\:b\:a\:b →\rightarrow q0  :  X X X X ⊔q_0\;:\;X\:X\:X\:X\:\boxed{\sqcup} no 16
ababbababb q0  :  a b a b bq_0\;:\;\boxed{a}\:b\:a\:b\:b →\rightarrow qb  :  X X X X X ⊔q_b\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} no 17
ababaababa q0  :  a b a b aq_0\;:\;\boxed{a}\:b\:a\:b\:a →\rightarrow qF  :  X X X X X ⊔q_F\;:\;X\:X\:X\:X\:X\:\boxed{\sqcup} sí 18
abaaababaaab q0  :  a b a a a bq_0\;:\;\boxed{a}\:b\:a\:a\:a\:b →\rightarrow qF  :  X X X X a X ⊔q_F\;:\;X\:X\:X\:X\:a\:X\:\boxed{\sqcup} sí 23
Tabla 2.1. Ejemplos del comportamiento de M#a>#bM_{\#a>\#b} con diversas entradas.

2.3 #Equivalencia

Nuestro objetivo ahora es comprender por qué las máquinas de Turing tienen la misma potencia de cálculo que los programas en Python. En otras palabras, queremos demostrar que los problemas que podemos resolver en ambos son los mismos.12

Para probar la equivalencia entre ambos modelos de cálculo, recurriremos al concepto de simulación. Es normal para diferentes modelos computacionales simularse unos a otros. Un ejemplo de esto es la máquina virtual de Java (JVM), que es un modelo abstracto capaz de ejecutar cualquier programa en Java13. Otro ejemplo es el sistema operativo Linux, que es capaz de ejecutar la JVM. Por lo tanto, un ordenador Linux es capaz de ejecutar cualquier programa en Java. Esto es posible mediante la cadena de simulaciones:

Linux⇝JVM⇝programa en Java\text{Linux} \leadsto \text{JVM} \leadsto \text{programa en Java}

Usamos la notación A⇝BA \leadsto B para indicar que AA simula a BB. El hecho de que AA simule BB quiere decir, sencillamente, que es posible realizar el procedimiento de cálculo de BB usando exclusivamente el procedimiento de cálculo de AA.

Procederemos a probar la equivalencia mostrada mediante la cadena de simulaciones que aparece en la Figura 2.6.

m¶aquinadeTuringmonocintam¶aquinadeTuringmulticintam¶aquinadeaccesoaleatorioprogramaenPythonordenadordevonNeumann
Figura 2.6. Cadena de simulaciones.

El trabajo que haremos en esta sección es arduo, y necesitaremos introducir diversos modelos computacionales para llegar a nuestro objetivo. Sin embargo, una vez probemos todas las simulaciones, sabremos que todas ellas son equivalentes, y podremos usar el modelo que queramos a conveniencia.

El primer modelo computacional que estudiaremos lo obtendremos añadiendo más cintas y cabezales a la máquina de Turing que definimos en la Sección 2.214.

Definición2.12—Máquina de Turing multicinta

Una máquina de Turing multicinta con kk cintas es una máquina de Turing convencional en la que tenemos kk cintas y kk cabezales de lectura. En este sentido, la configuración de una máquina de Turing multicinta se describe como:

q  :  w1  ,  w2  ,  ...  ,  wkq\;:\;w_1\;,\;w_2\;,\;...\;,\;w_k

donde cada una de las palabras de la cinta tiene uno de sus símbolos recuadrados, simbolizando la posición del cabezal en cada cinta.

La función δ:Q×Bk⟶(Q×(B×{L,R,S})k)∪{∅}\delta : Q \times B^k \longrightarrow \left(Q \times (B \times \{L, R, S\})^k\right) \cup \{\emptyset\} será de la forma:

δ(q,b1,b2,...,bk)=∅\delta(q, b_1, b_2, ..., b_k) = \emptyset

para las transiciones no definidas, y para las definidas:

δ(q,b1,b2,...,bk)=(p,c1,M1,c2,M2,...,ck,Mk),\delta(q, b_1, b_2, ..., b_k) = (p, c_1, M_1, c_2, M_2, ..., c_k, M_k),

con q,p∈Qq,p\in Q, bi,ci∈Bb_i, c_i \in B, Mi∈{L,R,S}M_i \in \{L, R, S\}, para todo i∈{1,2,...,k}i \in \{1, 2, ..., k\}.

El proceso de cálculo será análogo al de una máquina de Turing de una sola cinta, teniendo en cuenta que el símbolo bib_i es el leído por el cabezal ii-ésimo, que cic_i es el símbolo por el que se cambia bib_i en el cabezal ii-ésimo, y MiM_i es el movimiento que seguirá el cabezal ii-ésimo.

La entrada de la máquina se colocará en la primera cinta, con el cabezal en el primer símbolo de la palabra. El resto de cabezales apuntarán al símbolo vacío, dado que todas las cintas excepto la primera estarán llenas de símbolos vacíos.

Las palabras aceptadas por la máquina procederán de forma análoga al caso de una sola cinta, teniendo en cuenta si el estado donde la máquina para es final o no (está en FF).

qn¡1qnq0q1q2t010t::::::entradacinta1cabezal1ttttt::::::cinta2ttttt::::::cintak...cabezal2cabezalk...
Figura 2.7. Diagrama de una máquina de Turing multicinta en su configuración inicial.

Esta definición es intuitiva y esperable. Simplemente incorporamos más cintas y cabezales. De ahora en adelante, también usaremos el término «máquina de Turing de una sola cinta» para referirnos a la máquina de Turing que definimos en primer lugar.

Uno puede pensar que estas máquinas tienen más poder computacional que las de una sola cinta, pero, sin embargo, son equivalentes. Es evidente que una máquina multicinta simula una máquina de una sola cinta (la Definición 2.5 es la misma que la Definición 2.12 para k=1k=1). En la Proposición 2.13 vemos cómo el recíproco también es cierto.

Proposición2.13

Toda máquina de Turing multicinta puede ser simulada por una máquina de Turing de una sola cinta.

Demostración

Demostración.15 Sea M=(Q,A,B,δ,q0,⊔,F)M = (Q, A, B, \delta, q_0, \sqcup, F) una máquina de Turing con kk cintas. Vamos a crear una máquina M~=(Q~,A,B~,δ~,q0,[⊔k],F)\widetilde{M} = (\widetilde{Q}, A, \widetilde{B}, \widetilde{\delta}, q_0, [\sqcup^k], F) con una sola cinta de modo que L(M)=L(M~)L(M) = L(\widetilde{M}).

Para simular kk cintas y kk cabezales en un solo cabezal y una sola cinta, deberemos ampliar el alfabeto de trabajo B~\widetilde{B}. En primer lugar, vamos a introducir nuevos símbolos para poder saber la posición de cada uno de los cabezales. Definimos:

Bˇ={bˇ:b∈B}\widecheck{B} = \{\widecheck{b} : b \in B\}

Y, hecho esto, definimos B~\widetilde{B}, el alfabeto de M~\widetilde{M}, como:

B~={[b1 b2 ... bk]:b∈B∪Bˇ}∪{#}\widetilde{B} = \{[b_1\:b_2\:...\:b_k] : b \in B \cup \widecheck{B}\} \cup \{\#\}

Observemos que los símbolos de B~\widetilde{B} son bien un delimitador #\#, bien un grupo de caracteres de B∪BˇB \cup \widecheck{B}. De este modo, podemos simular kk cintas y kk cabezales en una sola cinta con un solo cabezal. Un ejemplo: si en MM tenemos la configuración:

q  :  0 1 0  ,  X X X  ,  Y Xq\;:\;0\:\boxed{1}\:0\;,\;X\:X\:\boxed{X}\;,\;\boxed{Y}\:X

Una de las posibles configuraciones en M~\widetilde{M} sería:

q  :  # [0 X Yˇ] [1ˇ X X] [0 Xˇ ⊔] #q\;:\;\fbox{\#}\:[0\:X\:\widecheck{Y}]\:[\widecheck{1}\:X\:X]\:[0\:\widecheck{X}\:\sqcup]\:\#
t010t......tXXXt......tYXtt......ttt01...0tt...tXXXt...ttt...ttttt...YXt...q:#[0XHY][H1XX][0HXt]#q:#[tXt][tXt][0HXt][H1tt][0tHY][ttX]#
Figura 2.8. Dos configuraciones de M~\widetilde{M} para una configuración de MM.

Observemos que varias configuraciones de M~\widetilde{M} son posibles, dado que las kk cintas son independientes (no importa cómo las «apilamos», y esto no tiene efecto en el proceso de cálculo). Esta característica de nuestra codificación se evidencia en la Figura 2.8. Sin embargo, esta independencia hace que esta peculiaridad no nos afecte: simplemente dejamos que la máquina vaya computando y vamos aplicando las transiciones y modificando las transiciones según se dicte.

Describir de forma exacta el conjunto de estados Q~\widetilde{Q} y la función de transición δ~\widetilde{\delta} es un proceso arduo. En esta demostración, nos limitaremos a explicar su comportamiento; la construcción completa se da en la Proposición 2.14 más abajo.

La máquina comienza con la configuración inicial para la entrada: u=u1 u2 ... un∈A∗u = u_1\:u_2\:...\:u_n \in A^*:

q0  :  # [u1ˇ ⊔ˇ ... ⊔ˇ⏟k−1] [u2 ⊔ ... ⊔⏟k−1] ... [un ⊔ ... ⊔⏟k−1] #q_0\;:\;\fbox{\#}\:[\widecheck{u_1}\:\underbrace{\widecheck{\sqcup} \:... \:\widecheck{\sqcup}}_{k-1}]\:[u_2\:\underbrace{{\sqcup} \:... \:{\sqcup}}_{k-1}]\:...\:[u_n\:\underbrace{{\sqcup} \:... \:{\sqcup}}_{k-1}]\:\#

A continuación, va aplicando los movimientos de δ\delta cinta a cinta y cabezal a cabezal. Llamaremos a estas cintas y cabezales «cintas virtuales» y «cabezales virtuales», respectivamente.

Para ello, comienza escaneando toda la cinta desde el delimitador #\# izquierdo, y almacena los valores de los cabezales virtuales en estados de Q~\widetilde{Q}. El conjunto Q~\widetilde{Q} es ampliado para poder efectuar esta acción de «memorización». A continuación, volvemos al principio de la cinta, y vamos efectuando cada una de las acciones de MM por cada cinta virtual. Si en algún momento al desplazar cada cabezal virtual nos lo coloca sobre un delimitador #\#, reemplazamos la posición de la cinta por el símbolo blanco de M~\widetilde{M}, [⊔k][\sqcup^k], y desplazamos todos los símbolos de la cinta virtual a la izquierda o derecha según sea necesario.

Una vez hecho esto, cambiamos el estado inicial por el estado correspondiente según δ\delta y así sucesivamente. El conjunto FF es el mismo, dado que vamos pasando por los mismos estados de QQ en Q~\widetilde{Q}.

Es fácil ver que L(M)=L(M~)L(M) = L(\widetilde{M}), pues hemos creado una máquina que imita a la perfección el proceso de cálculo de MM, y en caso de parar, para en un estado de QQ. Las palabras aceptadas serán aquellas en las que, una vez que la máquina haya parado, el estado esté en FF.

∎

Los detalles de esa construcción — el alfabeto de trabajo, los estados auxiliares y cada cláusula de δ~\widetilde{\delta} — son los siguientes.

Proposición2.14

Toda máquina de Turing multicinta puede ser simulada por una máquina de Turing de una sola cinta.

Demostración

Sea M=(Q,A,B,δ,q0,⊔,F)M=(Q, A, B, \delta, q_0, \sqcup, F) una máquina de Turing con kk cintas. Definiremos la máquina M~=(Q~,A,B~,δ~,q0,[⊔k],F)\widetilde{M}=(\widetilde{Q}, A, \widetilde{B}, \widetilde{\delta}, q_0, [\sqcup^k], F). Para ello, definimos en primer lugar Bˇ\widecheck{B} como:

Bˇ={bˇ:b∈B}\widecheck{B} = \{\widecheck{b} : b \in B\}

Para definir B~\widetilde{B}, observemos que un símbolo de un alfabeto no tiene por qué ser únicamente un carácter. Para representar un único símbolo, lo rodearemos de corchetes ([ ][\:]). Por tanto, el siguiente alfabeto tiene sentido:

B~={[u1 u2 ... uk]:ui∈B∪Bˇ}∪{#}\widetilde{B} = \{[u_1\:u_2\:...\:u_k]:u_i\in B\cup\widecheck{B}\}\cup\{\#\}

En B~\widetilde{B} insertamos, además de los símbolos que hemos comentado, un símbolo delimitador #\#.

El conjunto de estados Q~\widetilde{Q} lo hacemos más amplio que QQ. Para ello, numeraremos todos los estados de QQ:

Q={q0,q1,...,qm}Q=\{q_0,q_1,...,q_m\}

Y definimos el nuevo conjunto de estados como:

Q~=Q∪(⋃i=0m⋃j=1k{q~i,j, q~i,j(R), q~i,j(D), q~i,j(Ds), q~i,j(I), q~i,j(Is)})\widetilde{Q}=Q\cup\left(\bigcup_{i=0}^m\bigcup_{j=1}^k\left\{\widetilde{q}_{i,j},\:\widetilde{q}_{i,j}^{(R)},\:\widetilde{q}_{i,j}^{(D)},\:\widetilde{q}_{i,j}^{(Ds)},\:\widetilde{q}_{i,j}^{(I)},\:\widetilde{q}_{i,j}^{(Is)}\right\}\right)

Definimos como configuración inicial de la máquina con entrada u=u1 u2 ... un∈A∗u=u_1\:u_2\:...\:u_n\in A^* a:16

q0  :  # [u1ˇ ⊔ˇ ... ⊔ˇ⏟k−1] [u2 ⊔ ... ⊔⏟k−1] ... [un ⊔ ... ⊔⏟k−1] #q_0\;:\;\fbox{\#}\:[\widecheck{u_1}\:\underbrace{\widecheck{\sqcup} \:...\:\widecheck{\sqcup}}_{k-1}]\:[u_2\:\underbrace{{\sqcup}\:...\:{\sqcup}}_{k-1}]\:...\:[u_n\:\underbrace{{\sqcup}\:...\:{\sqcup}}_{k-1}]\:\#

Describiremos δ~\widetilde{\delta} poco a poco. En primer lugar, queremos partir del mismo estado que la máquina multicinta (esto nos permitirá que el lenguaje aceptado sea el mismo, como veremos más adelante). En el momento en el que la máquina se encuentre en este estado, nos movemos a la derecha y comenzaremos a buscar la posición en la cinta del primer cabezal. Usaremos el estado q~i,j\widetilde{q}_{i,j} para buscar el cabezal jj-ésimo, estando la máquina multicinta en el estado qiq_i. Añadimos para hacer esto las siguientes transiciones, para cada i=0,1,...,mi=0,1,...,m:

δ~(qi,#)=(q~i,1,#,R)\widetilde{\delta}(q_i, \#) = (\widetilde{q}_{i,1}, \#, R)

Para buscar el cabezal jj-ésimo, estando la máquina multicinta en el estado qiq_i, nos iremos moviendo a la derecha mientras que no hayamos encontrado un carácter de Bˇ\widecheck{B} en la posición jj-ésima. Para ello, añadimos para cada i=0,1,...,mi=0,1,...,m y j=1,2,...,kj=1,2,...,k:

δ~(q~i,j,[w1 w2 ... wl ... wk])=(q~i,j,[w1 w2 ... wl ... wk],R)     con     wj∈B,  wl∈B∪Bˇ para l≠j\widetilde{\delta}(\widetilde{q}_{i,j}, [w_1\:w_2\:...\:w_l\:...\:w_k]) = (\widetilde{q}_{i,j}, [w_1\:w_2\:...\:w_l\:...\:w_k], R)\;\;\text{ con }\;\;w_j\in B,\; w_l \in B\cup\widecheck{B} \text{ para } l \neq j

Nótese que, de forma explícita, hacemos que el símbolo wjw_j no pueda estar en Bˇ\widecheck{B} (de aquí en adelante, si no explicitamos, será wl∈B∪Bˇw_l\in B\cup\widecheck{B} para cada ll), de modo que seguimos moviéndonos a la derecha hasta encontrar un carácter con cabezal en la posición jj-ésima de la celda actual.

Ahora estamos en condiciones de incluir las transiciones de δ\delta en δ~\widetilde{\delta}. Sea una transición de δ\delta:

δ(qi,b1,b2,...,bk)=(qj,c1,M1,c2,M2,...,ck,Mk)\delta(q_i, b_1, b_2, ..., b_k) = (q_j, c_1, M_1, c_2, M_2, ..., c_k, M_k)

Para cada una de estas transiciones, deberemos insertar reglas en δ~\widetilde{\delta}. Estas transiciones dependerán de cómo se mueva el cabezal, ya que si lo hace a la derecha o a la izquierda hasta toparse con el delimitador #\#, deberemos extender la cinta de M~\widetilde{M} con un símbolo blanco [⊔k][\sqcup^k] y desplazar el delimitador #\# una posición. Dada la transición anterior, y para cada l=1,2,...,kl=1, 2, ..., k:

  • Si Ml=RM_l=R: insertamos varias transiciones. En primer lugar, la transición correspondiente a sustituir el símbolo en el que se encuentra el cabezal, blb_l, por el correspondiente, clc_l:
δ~(q~i,l,[w1 ... wl−1 blˇ wl+1 ... wk])=(q~i,l(D),[w1 ... wl−1 cl wl+1 ... wk],R)\widetilde{\delta}(\widetilde{q}_{i,l}, [w_1\:...\:w_{l-1}\:\widecheck{b_l}\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(D)}, [w_1\:...\:w_{l-1}\:c_l\:w_{l+1}\:...\:w_k], R)

A continuación simulamos que el cabezal ll-ésimo se mueve a la derecha. En caso de que no hayamos alcanzado el separador #\#, la transición es directa: indicamos que el cabezal está en la posición determinada, y volvemos al primer delimitador #\# mediante el estado (R)^{(R)} (que veremos más adelante):

δ~(q~i,l(D),[w1 ... wl−1 wl wl+1 ... wk])=(q~i,l(R),[w1 ... wl−1 wlˇ wl+1 ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,l}^{(D)}, [w_1\:...\:w_{l-1}\:w_l\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(R)}, [w_1\:...\:w_{l-1}\:\widecheck{w_l}\:w_{l+1}\:...\:w_k], L)

En caso de que nos topemos con el delimitador #\#, iniciaremos una subrutina indicada con los estados (Ds)^{(Ds)}, en el que añadiremos un símbolo blanco y moveremos el delimitador a la derecha, siguiendo al estado (R)^{(R)}:

δ~(q~i,l(D),#)=(q~i,l(Ds),[⊔ ... ⊔⏟l−1 ⊔ˇ ⊔ ... ⊔⏟k−l],R)\widetilde{\delta}(\widetilde{q}_{i,l}^{(D)}, \#) = (\widetilde{q}_{i,l}^{(Ds)}, [\underbrace{{\sqcup}\:...\:{\sqcup}}_{l-1}\:\widecheck{\sqcup}\:\underbrace{{\sqcup}\:...\:{\sqcup}}_{k-l}], R)
δ~(q~i,l(Ds),[⊔k])=(q~i,l(R),#,L)\widetilde{\delta}(\widetilde{q}_{i,l}^{(Ds)}, [\sqcup^k]) = (\widetilde{q}_{i,l}^{(R)}, \#, L)
  • Si Ml=LM_l=L: añadimos de forma análoga las transiciones con los subestados (I)^{(I)} y (Is)^{(Is)}:
δ~(q~i,l,[w1 ... wl−1 blˇ wl+1 ... wk])=(q~i,l(I),[w1 ... wl−1 cl wl+1 ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,l}, [w_1\:...\:w_{l-1}\:\widecheck{b_l}\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(I)}, [w_1\:...\:w_{l-1}\:c_l\:w_{l+1}\:...\:w_k], L)
δ~(q~i,l(I),[w1 ... wl−1 wl wl+1 ... wk])=(q~i,l(R),[w1 ... wl−1 wlˇ wl+1 ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,l}^{(I)}, [w_1\:...\:w_{l-1}\:w_l\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(R)}, [w_1\:...\:w_{l-1}\:\widecheck{w_l}\:w_{l+1}\:...\:w_k], L)
δ~(q~i,l(I),#)=(q~i,l(Is),[⊔ ... ⊔⏟l−1 ⊔ˇ ⊔ ... ⊔⏟k−l],L)\widetilde{\delta}(\widetilde{q}_{i,l}^{(I)}, \#) = (\widetilde{q}_{i,l}^{(Is)}, [\underbrace{{\sqcup}\:...\:{\sqcup}}_{l-1}\:\widecheck{\sqcup}\:\underbrace{{\sqcup}\:...\:{\sqcup}}_{k-l}], L)
δ~(q~i,l(Is),[⊔k])=(q~i,l(R),#,R)\widetilde{\delta}(\widetilde{q}_{i,l}^{(Is)}, [\sqcup^k]) = (\widetilde{q}_{i,l}^{(R)}, \#, R)
  • Si Ml=SM_l=S: la transición es más inmediata, dado que no debemos mover el cabezal: simplemente reemplazamos el símbolo y cambiamos a (R)^{(R)}:
δ~(q~i,l,[w1 ... wl−1 blˇ wl+1 ... wk])=(q~i,l(R),[w1 ... wl−1 clˇ wl+1 ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,l}, [w_1\:...\:w_{l-1}\:\widecheck{b_l}\:w_{l+1}\:...\:w_k]) = (\widetilde{q}_{i,l}^{(R)}, [w_1\:...\:w_{l-1}\:\widecheck{c_l}\:w_{l+1}\:...\:w_k], L)

Ahora nos queda una forma de cambiar la cinta que estamos analizando. Comenzaremos añadiendo para ello un estado de retorno, que posiciona el cabezal en el primer delimitador #\#. Para cada i=0,1,...,mi=0,1,...,m y j=1,2,...,kj=1,2,...,k añadimos las transiciones:

δ~(q~i,j(R),[w1 w2 ... wl ... wk])=(q~i,j(R),[w1 w2 ... wl ... wk],L)\widetilde{\delta}(\widetilde{q}_{i,j}^{(R)}, [w_1\:w_2\:...\:w_l\:...\:w_k]) = (\widetilde{q}_{i,j}^{(R)}, [w_1\:w_2\:...\:w_l\:...\:w_k], L)

Para leer la cinta siguiente, simplemente añadimos estas transiciones para cada i=0,1,...,mi=0,1,...,m y j=1,2,...,k−1j=1,2,...,k-1:

δ~(q~i,j(R),#)=(q~i,j+1(R),#,R)\widetilde{\delta}(\widetilde{q}_{i,j}^{(R)}, \#) = (\widetilde{q}_{i,j+1}^{(R)}, \#, R)

Si ya hemos leído la última cinta, cambiamos el estado a uno de los de QQ de acuerdo a la función de transición. De nuevo revisitamos δ\delta. Para cada transición δ(qi,b1,b2,...,bk)=(qj,c1,M1,c2,M2,...,ck,Mk)\delta(q_i, b_1, b_2,...,b_k) = (q_j, c_1, M_1, c_2, M_2,...,c_k, M_k), añadimos la transición de cambio de estado:

δ~(q~i,k(R),#)=(qj,#,S)\widetilde{\delta}(\widetilde{q}_{i,k}^{(R)}, \#) = (q_j, \#, S)

Ya hemos definido todas las transiciones necesarias. Ahora comprobaremos que la simulación es correcta, es decir, que L(M)=L(M~)L(M)=L(\widetilde{M}). En efecto, hemos construido una máquina M~\widetilde{M} que ejecuta el mismo proceso de cálculo que MM, y que además pasa por los mismos estados QQ de MM tras modificar la cinta. Precisamente por esto, en caso de parar la máquina, se encontrará en un estado q∈Qq\in Q (la hemos construido para que ocurra esto). De este modo, el lenguaje aceptado por ambas máquinas coincide, dado que usamos el mismo conjunto de estados finales FF.

∎

Pensemos un momento en las consecuencias de que estos dos modelos de cálculo sean equivalentes. En primer lugar, nos da una idea del alcance de la máquina de Turing monocinta: introduciendo más complejidad en su descripción, no podemos aspirar a crear algoritmos más complejos, pues siempre podremos encontrar un algoritmo equivalente en una máquina de una sola cinta. Esto, como comentábamos al comienzo del capítulo, nos da una pista de una de las tesis más relevantes de las ciencias de la computación en la actualidad, y que discutimos en la Sección 2.4.

En segundo lugar, esto nos da una ventaja a la hora de querer resolver problemas: poder describir un programa en un modelo que nos da una mayor flexibilidad es mucho más sencillo.

Al avanzar en esta sección, es importante mantener en mente estas dos observaciones. Ahora, introduciremos un nuevo modelo computacional: la máquina de acceso aleatorio4.

Definición2.15—Máquina de Turing de acceso aleatorio

Una máquina de Turing de acceso aleatorio con kk cintas es una máquina de Turing multicinta de kk cintas con dos cintas adicionales, limitadas a la izquierda:

  • Una cinta de dirección, que almacena un número natural n∈Nn \in \mathbb{N}.17
  • Una cinta llamada RAM, que almacena símbolos en cada una de sus pistas.

Denotamos como RAM[n]\text{RAM}[n] a la nn-ésima pista de la RAM.

La función δ:Q×N×B×Bk⟶(Q×N×B×(B×{L,R,S})k)∪{∅}\delta : Q \times \mathbb{N} \times B \times B^k \longrightarrow \left(Q \times \mathbb{N} \times B \times (B \times \{L, R, S\})^k\right) \cup \{\emptyset\} será de la forma:

δ(q,b1,b2,...,bk)=∅\delta(q, b_1, b_2, ..., b_k) = \emptyset

para las transiciones no definidas, y para las definidas:

δ(q,n,r,b1,b2,...,bk)=(p,m,s,c1,M1,c2,M2,...,ck,Mk),\delta(q, n, r, b_1, b_2, ..., b_k) = (p, m, s, c_1, M_1, c_2, M_2, ..., c_k, M_k),

con q,p∈Qq,p\in Q, n,m∈Nn, m \in \mathbb{N}, r,s,bi,ci∈Br, s, b_i, c_i \in B, Mi∈{L,R,S}M_i \in \{L, R, S\}, para todo i∈{1,2,...,k}i \in \{1, 2, ..., k\}.

La transición definida se ejecutará si el valor de RAM[n]=r\text{RAM}[n] = r, y tras ejecutarla el valor de RAM[n]\text{RAM}[n] cambiará a ss, y el valor de la cinta de dirección cambiará de nn a mm. Es decir, en una única transición, es capaz de acceder a la posición de la RAM, modificarla, y cambiar la posición de la RAM.

Una configuración de la máquina se especificará mediante:

q  :  r ; w1 , w2 , ... , wkq\;:\;r\:;\:w_1\:,\:w_2\:,\:...\:,\:w_k

con un símbolo de cada cinta w1,w2,...,wkw_1, w_2, ..., w_k debidamente recuadrado, y con un símbolo de rr recuadrado, concretamente, aquel al que apunte la cinta de dirección.

Podemos ver un ejemplo de tal configuración en la Figura 2.9.

q3  :  1 a b 2 ; 2 X X , 1 0 , ... , wkq_3\;:\;1\:a\:b\:\boxed{2}\:;\:2\:\boxed{X}\:X\:,\:\boxed{1}\:0\:,\:...\:,\:w_k
4tttt...cintadedirecci¶onXX1bt.........cintak...estadoactualq31ab2c...RAMtt1Y0......cinta2t2cXt......cinta1
Figura 2.9. Máquina de Turing de acceso aleatorio.

Probar que una máquina de Turing de acceso aleatorio puede ser simulada mediante una máquina de Turing multicinta es sencillo pero tedioso. En la Proposición 2.16 se explican las rutinas que serían necesarias para tal simulación.

Proposición2.16

Toda máquina de Turing de acceso aleatorio con kk cintas puede ser simulada por una máquina de Turing multicinta.

Demostración

Para probar esto, necesitaremos una máquina de Turing de k+2k+2 cintas. Las dos últimas cintas serán la cinta de dirección (k+1k+1-ésima cinta) y la RAM (k+2k+2-ésima cinta). A continuación, vemos que:

  1. Podemos crear una rutina que reemplace el símbolo de la posición del cabezal en la cinta k+2k+2 por otro.
  2. Podemos crear una rutina que modifique la dirección de la cinta k+1k+1.
  3. Podemos crear una rutina que, dada una dirección en la cinta k+1k+1, mueva el cabezal de la cinta k+2k+2 a la posición correspondiente. Podríamos implementar una rutina que vaya contando hasta el número de la cinta k+1k+1 y, cada vez que va contando, va moviendo el cabezal de la cinta k+2k+2 una posición a la derecha, comenzando al principio.

Implementar la operación adicional atómica de la máquina de Turing de acceso aleatorio en esta máquina multicinta es sencillo: podemos acceder al valor de RAM[n]\text{RAM}[n], rr, viendo dónde se encuentra el cabezal en la cinta k+2k+2. En función de esto, ejecutamos la transición necesaria. Tras esto, modificamos RAM[n]\text{RAM}[n] a ss ejecutando la rutina (1), y cambiamos la dirección de la cinta k+1k+1 ejecutando (2). Finalmente, modificamos la posición del cabezal en la cinta k+2k+2 ejecutando la rutina (3).

∎

A continuación, continuaremos con la cadena de simulaciones. Para ello, introduciremos una definición de ordenador. Ésta es una simplificación de la arquitectura de un ordenador pero, a efectos prácticos, todos los ordenadores modernos siguen la estructura que se especifica en la Definición 2.1718.

Definición2.17—Ordenador moderno

Definimos un ordenador moderno como aquel que dispone de los siguientes elementos:

  • Registros. Un registro permite almacenar exactamente una unidad de información, en binario. Los registros tienen un tamaño fijo (en la mayoría de ordenadores actuales es de 64 bits). Esta unidad de información puede representar caracteres, instrucciones, etc.
  • RAM. La RAM o memoria de acceso aleatorio (Random Access Memory) es el hardware del ordenador que almacena la información en uso por el ordenador.
  • ROM. La ROM o memoria de solo lectura (Read-Only Memory) es el hardware del ordenador que almacena información, pero que no puede ser modificada. Normalmente, la ROM almacena programas fijos que indican al ordenador acciones básicas tales como cargar los programas en el encendido.
  • Repertorio de instrucciones. Cada CPU contiene un conjunto fijo de acciones que puede ejecutar. Cada una de estas acciones se llama instrucción, y el conjunto de todas las instrucciones posibles es el repertorio de instrucciones. Estas instrucciones suelen ser operaciones aritméticas y condicionales con los registros, cargar de la RAM a los registros, etc.

Para nuestro trabajo, saber detalles sobre qué tipos de instrucciones puede ejecutar un ordenador es irrelevante. Lo importante es saber que, independientemente del lenguaje de programación en el que esté escrito, un programa puede ser «traducido»19 a este conjunto de instrucciones.

  • Disco. Un ordenador tiene uno o varios discos (por simplicidad, hablaremos sólo de uno). El disco permite lectura y escritura, y almacena las entradas y salidas a los programas, así como los programas que quieran ejecutarse.

Simular este ordenador mediante una máquina de Turing de acceso aleatorio es inmediato: podemos simular todos los componentes mediante cintas y la RAM.

Proposición2.18

Un ordenador moderno puede ser simulado por una máquina de Turing de acceso aleatorio.

Demostración

Veamos cómo podemos simular cada uno de los elementos del ordenador CC mediante una máquina de Turing de acceso aleatorio MM. La Figura 2.10 simboliza las afirmaciones siguientes.

  • La ROM de CC puede ser simulada incluyendo los estados y transiciones necesarios en MM.
  • Cada instrucción de CC puede implementarse del mismo modo, mediante transiciones y estados.
  • Los registros de CC que sean usados para guardar direcciones de memoria pueden ser simulados por la cinta de direcciones de MM.
  • Otros registros de propósito general de CC pueden ser simulados incorporando cintas adicionales a MM.
  • La RAM de CC puede ser simulada con la RAM de MM.
  • El disco de CC puede ser simulado con una cinta en MM (que podemos llamar cinta de entrada/salida).
∎

La demostración anterior no es estricta, y hacemos esto a propósito: definir de forma concreta cómo implementar cada uno de los componentes del ordenador queda fuera del objeto de este trabajo, y sería más relevante si hablásemos de arquitectura de computadores. Ilustraremos esta simulación mediante un ejemplo.

Ejemplo2.19

Vamos a simular una instrucción de un ordenador moderno en una máquina de Turing de acceso aleatorio. Sea ADD la instrucción de suma:

ADD R0, R1, R2\texttt{ADD R0, R1, R2}

Esta instrucción toma los números en binario de los registros R0 y R1 y guarda su suma en el registro R2. Por ejemplo, si hacemos

ADD r4, r5, r9\texttt{ADD r4, r5, r9}

almacenaremos la suma de los registros r4 y r5 en el registro r9.

Antes de realizar la simulación, observemos una cosa: si el tamaño de cada uno de los registros es de nn bits, podemos hacer que la RAM esté compuesta de mm registros de nn bits y que la cinta de dirección haga que el cabezal de la RAM apunte al primer bit del registro correspondiente. Dicho esto, para realizar la simulación basta seguir el siguiente proceso:

  1. Colocamos en la cinta de dirección la dirección correspondiente al registro R0. De este modo, el cabezal de la RAM apuntará al primer bit del registro R0.
  2. Copiamos los contenidos de la RAM (los nn bits a partir de la posición del cabezal de la RAM) en una cinta nueva, a la que llamaremos C0C_0.
  3. Repetimos los pasos anteriores para copiar el registro R1 en la cinta C1C_1.
  4. Vamos repitiendo los pasos siguientes:
  5. Comprobamos si en la cinta C1C_1 el número almacenado es 00. En tal caso, seguimos al paso 5.
  6. Si no lo es, decrementamos en 11 el número almacenado en la cinta C1C_1 e incrementamos en 11 el número almacenado en la cinta C0C_0. Sumar y restar 11 a un número en binario en una cinta es una operación fácilmente implementable en una máquina de Turing.
  7. Llegado a este punto, en la cinta C1C_1 tendremos almacenado 00 y en la cinta C0C_0 tendremos el resultado de la suma.
  8. Ahora colocamos en la cinta de dirección la dirección correspondiente al registro R2. De este modo, el cabezal de la RAM apunta al registro R2.
  9. Copiamos los contenidos de la cinta C0C_0 a la RAM (los nn bits a partir de la posición del cabezal de la RAM, que apuntarán al registro R2).

Nótese que la simulación que hemos hecho no modifica los registros R0 y R1, únicamente modifica R2.

El resultado que buscamos está a la vuelta de la esquina. La Proposición 2.20 y la Proposición 2.21 cierran el círculo y permiten probar el Teorema 2.23144.

Proposición2.20

Un programa en Python puede ser simulado por un ordenador moderno.

Demostración

La prueba es inmediata: en el momento en el que podemos simular un ordenador moderno de un solo núcleo, podemos simular cualquier software, incluyendo programas en Python.

Otra forma de ver esto es imaginar que configuramos un ordenador para ejecutar un programa en Python justo al iniciarse. De este modo, podemos pensar en el ordenador como un conjunto de hardware que simula el programa en Python.

∎
ROM(contienelasinstrucciones)CPU(unidadcentraldeprocesamiento)circuitosparaejecutarinstruccionesregistrosdedirecci¶onotrosregistrosRAM(memoriaprincipal)discoordenadormodernomaquinadeTuringdeaccesoaleatoriocintadeentrada/salidaRAMcintadedirecci¶on...otrascintas......
Figura 2.10. Simulación de un ordenador moderno a través de una máquina de Turing de acceso aleatorio.
Proposición2.21

Una máquina de Turing de una sola cinta puede ser simulada por un programa en Python.

Demostración

Para probar esto, deberemos de crear un programa en Python que, dada la descripción de una máquina de Turing de una sola cinta MM, sea capaz de aplicar todos los pasos de cálculo y devolver la configuración en la que la máquina para.

Programa2.2—simulate_turing.py
import utilities
from turing import Turing
 
def simulate_turing(input):
machine_encoding, machine_input = utilities.UAM(input)
 
# creamos la máquina de Turing a partir de la entrada de la función SISO
turing_machine = Turing(machine_encoding, machine_input)
 
# ejecutamos la máquina
turing_machine.run()
 
# si llegamos aquí, la máquina ha parado: devolvemos su configuración
return str(turing_machine)

Podemos ver la simulación en el Programa 2.2. Los programas que lo acompañan están en simulate_turing.py y turing.py. Explicaremos cada una de las líneas.

En primer lugar, se importa la clase Turing del paquete turing. Puedes encontrar el código en turing.py. Esta clase se encarga de instanciar una máquina de Turing, e incorpora las funciones necesarias para realizar la simulación.

A continuación tenemos nuestra función main, que acepta un string como entrada. Para poder codificar varias entradas, recurrimos a las funciones MAU y UAM de la librería utilities. Dada una lista de strings, la función MAU (de Múltiple A Uno) los convierte en un único string usando la cadena :: como separador. La función UAM realiza el proceso contrario, y convierte un string en una lista de strings, separando por la cadena ::.

De este modo, el string de entrada tendrá la forma

CODIFICACIOˊN+’::’+ENTRADA\texttt{CODIFICACIÓN} + \texttt{'::'} + \texttt{ENTRADA}

Es decir, estará compuesto de los strings correspondientes a la codificación de la máquina de Turing y a la entrada unidos por ::.

Codificar una máquina de Turing MM mediante un string es sencillo. Lo haremos mediante archivos, que no son más que cadenas de caracteres. Codificaremos cada uno de los estados y símbolos con strings, y crearemos una cadena de caracteres uniendo las siguientes cadenas de caracteres mediante saltos de línea, en este orden:

  • Los estados, separados por espacios.
  • Los símbolos del alfabeto de entrada, separados por espacios.
  • Los símbolos del alfabeto de trabajo, separados por espacios.
  • El símbolo blanco.
  • El estado inicial.
  • Los estados finales, separados por espacios.
  • Una línea para cada transición
δ(EA,SL)=(ES,SR,M)\delta(\texttt{EA}, \texttt{SL}) = (\texttt{ES}, \texttt{SR}, \texttt{M})

(donde EA es el estado actual, SL es el símbolo leído, ES es el estado siguiente, SR es el símbolo de reemplazo y M es el movimiento, pudiendo M ser L, R o S), con el siguiente formato:

EA, SL : ES, SR, M\texttt{EA, SL : ES, SR, M}

En la línea con UAM, separamos la entrada y guardamos la codificación de la máquina en machine_encoding y la entrada en machine_input, mediante la función UAM anteriormente explicada.

En la línea que crea la instancia de Turing, creamos una instancia de la clase Turing con la codificación de la máquina y la entrada. Al crear la instancia, se ejecutarán funciones que comprueban si la descripción es correcta. En caso de haber un error, la máquina no ejecutará ningún paso y la salida será de tipo error.

En la línea que llama a run, ejecutamos la máquina, aplicando transición tras transición hasta que pare. En caso de no parar, el programa en Python tampoco parará.

En la línea de retorno, devolvemos la configuración de la máquina al haber parado, y si acepta o rechaza.

De esta forma hemos simulado MM mediante un programa en Python.

∎

El programa simulate_turing está implementado en simulate_turing.py. Mostraremos un ejemplo de la codificación explicada, y cómo usarlo, mediante el Ejemplo 2.22.

Ejemplo2.22

Una codificación de la máquina M#a>#bM_{\#a>\#b} del Ejemplo 2.11 puede ser:20

q_0 q_a q_b q_R q_F     # states
a b                     # input alphabet
a b X _                 # work alphabet
_                       # blank symbol
q_0                     # initial state
q_F                     # final states
 
# transitions
q_0, a : q_a, X, R
q_0, b : q_b, X, R
q_0, X : q_0, X, R
q_a, a : q_a, a, R
q_a, b : q_R, X, L
q_a, X : q_a, X, R
q_a, _ : q_F, _, S
q_b, a : q_R, X, L
q_b, b : q_b, b, R
q_b, X : q_b, X, R
q_R, a : q_R, a, L
q_R, b : q_R, b, L
q_R, X : q_R, X, L
q_R, _ : q_0, _, R

Esta codificación se encuentra en turing_machines/more_as_than_b.mt. Para probar simulate_turing.py, clona intro-to-incompleteness, abre el intérprete de Python en su raíz y ejecuta:

from utilities import read, MAU
from simulate_turing import simulate_turing
encoding = read('./turing_machines/more_as_than_b.mt')
input_str = 'aababb'

En este caso hemos puesto aababbaababb como entrada, pero se puede reemplazar por cualquier entrada de {a,b}∗\{a,b\}^*. Para ejecutar la función simulate_turing, simplemente hacemos:

simulate_turing(MAU(encoding, input_str))
'q_0 : X X X X X X [_] (rejects)'

Vemos que el programa Python devuelve la cadena de caracteres 'q_0 : X X X X X X [_] (rejects)'. Es decir, la máquina de Turing M#a>#bM_{\#a>\#b} para en la configuración

q0  :  X X X X X X ⊔q_0\;:\;X\:X\:X\:X\:X\:X\:\boxed{\sqcup}

que no es un estado final (q0∉Fq_0 \notin F), y por tanto la entrada aababbaababb es rechazada.

Es posible probar con cualquier entrada y con otras máquinas de Turing de turing_machines/. Algunos de los archivos corresponden a codificaciones incorrectas, para poder ver cómo simulate_turing.py es un programa en Python tal y como lo hemos definido (SISO, sin errores, etc.).

Una vez hemos probado la secuencia de simulaciones de la Figura 2.6, el teorema siguiente queda demostrado.

Teorema2.23—Equivalencia entre programas en Python y máquinas de Turing

Dado un programa en Python PP, existe una máquina de Turing (de una sola cinta) MM que computa la misma función que PP.

Acabamos de probar que las máquinas de Turing y los programas en Python son iguales en términos de los programas que podemos escribir en ellos. Esto nos será útil cuando definamos el concepto de problema en el Capítulo 3.

2.4 #Relación con la tesis de Church-Turing

El trabajo que hemos realizado en este capítulo ha sido arduo, pero el resultado que hemos obtenido es extremadamente interesante. Sin embargo, debemos recordar que la elección del lenguaje de programación Python ha sido completamente arbitraria: podríamos sustituir a Python por cualquier otro lenguaje de programación capaz de codificar una máquina de Turing.

¿Qué nos dice esto de lo que puede ser computado? Podemos verlo de dos formas: por una parte, los programas en Python (así como los ordenadores en sí) no son más que máquinas de Turing con «muchos atajos» y extensiones. Por otra (y de forma más interesante), la máquina de Turing, que inicialmente puede parecer inocente por la sencillez de su descripción, tiene un potencial de cálculo enorme, pues puede ejecutar cualquier programa de ordenador.

En el Capítulo 1 ya expusimos la tesis de Church-Turing. Por conveniencia, la referenciamos de nuevo a continuación.

Tesis de Church-Turing. Toda función calculable (algoritmo) puede calcularse mediante una máquina de Turing.

Como hemos dicho, se trata de una tesis, es decir, es una afirmación que se supone pero que no se demuestra. Probar este resultado es prácticamente imposible, pero hay evidencias que lo refuerzan, como lo que acabamos de probar: hemos visto cómo un modelo de computación, en apariencia, mucho más complejo que la máquina de Turing, resulta ser equivalente a ella.

Sin embargo, siempre existe la posibilidad de que avances en la comprensión de cómo se comporta el universo nos abran las puertas a nuevos y más potentes modelos de cálculo. Este no es el caso de la computación cuántica, que puede acelerar la resolución de algunos problemas, pero no amplía la gama de funciones calculables21.

Esta tesis tiene profundas implicaciones. Concluiremos este capítulo con algunas preguntas.

  1. ¿Es la mente humana una máquina de Turing?
  2. ¿Es el universo una máquina de Turing? Esto es, ¿son las leyes que rigen el universo (las leyes de la física) computables?
  3. En caso de que esto no sea así, ¿es posible crear una máquina más poderosa que la de Turing?

Respecto a la primera pregunta, es importante realizar una puntualización histórica: Turing afirmaba que la respuesta era afirmativa, es decir, que la mente humana podría simularse en un ordenador. Esta tesis fue central en sus trabajos de Inteligencia Artificial22, en los que defendía que el desarrollo de esta disciplina solo era cuestión de tiempo.

Notas al pie

  1. Aiken, H. (1956). Elektronische Rechenmaschinen und Informationsverarbeitung. Nachrichtentechnische Fachberichte, 2:32–34. (Traducción al inglés en MacCormick4.)

    ↩
  2. Turing, A.M. (1937). On computable numbers, with an application to the Entscheidungsproblem. Proceedings of the London Mathematical Society, 42(1):230–265.

    ↩ ↩2 ↩3
  3. Martelli, A. (2006). Python in a Nutshell. O'Reilly Media.

    ↩
  4. MacCormick, J. (2018). What Can Be Computed?: A Practical Guide to the Theory of Computation. Princeton University Press.

    ↩ ↩2 ↩3 ↩4
  5. Nos referimos a las excepciones que se pueden definir implícitamente en el lenguaje o que pueden resultar de errores en su uso, dado que en la definición de PP el programa es correcto sintácticamente.

    ↩
  6. La librería pickle puede codificar y descodificar una gran variedad de tipos de datos, incluyendo todos los nativos. Tiene algunas limitaciones para objetos más complejos pero, dado que todos los objetos se pueden reducir a estos tipos de datos, podemos afirmar que cualquier objeto puede ser codificado. Véase Lutz23.

    ↩
  7. Church, A. (1937). Review of Turing 1936–37. Journal of Symbolic Logic, 2(1):42–43.

    ↩
  8. De Mol, L. (2021). Turing machines. In E.N. Zalta (ed.), Stanford Encyclopedia of Philosophy. https://plato.stanford.edu/entries/turing-machine/

    ↩
  9. Esta notación proviene de la operación de clausura de Kleene.

    ↩
  10. Nótese que esta definición permite que la máquina siga cambiando su configuración aun habiendo llegado a un estado final. Normalmente, las máquinas de Turing paran llegado al estado final, y esto es algo de lo que nos podemos asegurar simplemente haciendo δ(q,b)=∅\delta(q,b)=\emptyset para todo q∈Fq \in F, b∈Bb \in B.

    ↩
  11. Usamos la notación: #(u,a)\#(u, a) es igual al número de apariciones del símbolo aa en uu.

    ↩
  12. No introduciremos el concepto de «problema» con precisión hasta el Capítulo 3 — por ahora, nos quedaremos con la noción intuitiva de problema, sin mayor explicación.

    ↩
  13. Evans, B., & Flanagan, D. (2018). Java in a Nutshell. O'Reilly Media.

    ↩
  14. Sipser, M. (2012). Introduction to the Theory of Computation (3rd ed.). Cengage Learning.

    ↩ ↩2
  15. Basada en la demostración del teorema 3.13 de Sipser14.

    ↩
  16. Siempre es posible cambiar la posición inicial del cabezal de una máquina de Turing ejecutando las transiciones necesarias.

    ↩
  17. Suponemos 0∉N0 \notin \mathbb{N}.

    ↩
  18. Nisan, N., & Schocken, S. (2005). The Elements of Computing Systems: Building a Modern Computer from First Principles. MIT Press.

    ↩
  19. Mediante procesos de compilación y/o interpretación.

    ↩
  20. Incluimos comentarios y líneas vacías, que ignoraremos.

    ↩
  21. Kaye, P., Laflamme, R., & Mosca, M. (2007). An Introduction to Quantum Computing. Oxford University Press.

    ↩
  22. Turing, A.M. (1950). Computing machinery and intelligence. Mind, 59(236):433–460.

    ↩
  23. Lutz, M. (2013). Learning Python. O'Reilly Media.

    ↩