notas de aula teoria da computação
Post on 11-Jul-2016
25 Views
Preview:
DESCRIPTION
TRANSCRIPT
Teoria da Comptutação
Vivek Nigam
8 de fevereiro de 2016
Prefácio
A ciência da computação e em particular a teoria da computação atingiu um estado de ma-
turidade nos últimos 50 anos. Grandes pesquisadores trabalharam duro para entender o que
é um algorítimo e como formalizar essas ideias através de modelos de computação formais na
forma da Máquina de Turing. O resultado foi uma noção bem estabelecida chamada a Tese
de Church-Turing. Este estudo teve impactos muito fortes na construção de sistemas com-
putacionais. Pudemos compreender as surpreendentes limitações dos computadores. Muito
provavelmente, compiladores e linguagens de programação não teriam sido o sucesso que tem
sem a sólida base teórica na qual foram desenvolvidos.
O nosso entendimento do comportamento de algoritmos, seu projeto e análise foram uma
consequência importante da definição e estudo da máquina de Turing. Pudemos formalizar qual
a complexidade de um algorítimo, organizando-os em classes de complexidade computacional.
Por exemplo, a classe de problemas P, que podem ser resolvidos em tempo polinomial, e classe
de problemas NP cuja corretude de sua solução pode ser checada em tempo polinomial.
Contudo, a pesquisa da teoria da computação não termina, pois estamos sempre encon-
trando novas técnicas, novos problemas e novos algorítimos e precisamos entendê-los. O grande
exemplo desta pesquisa contínua sendo o problema em aberto P = NP.
Neste livro iremos introduzir vários dos conceitos fundamentais da teoria da computabili-
dade. Seguiremos o formato do livro Uma Introdução à Teoria da Computação de Michael
Sipser.
∙ Capítulo 1 revisa brevemente conceitos fundamentais da matemática, por exemplo, as
noções básicas de conjuntos, linguagens e métodos de prova. Este capítulo tenta também
estabelecer a notação que iremos usar no restante do livro. Portanto, sugiro que todos
dêem uma passada neste capítulo mesmo aqueles que já conhecem o seu material.
∙ Capítulo 2 introduz os modelos de computação mais simples: Autômatos Finitos e Autô-
matos de Pilha. Veremos que já esse modelos tem aplicações importantes na computação,
1
por exemplo, o uso de Expressões Regulares.
∙ Capítulo 3 introduz as máquinas de Turing e discute o problema da decidibilidade. Ve-
remos como implementar máquinas de Turings para resolver problemas. Terminamos o
capítulo revisitando a tese de Church-Turing.
∙ Capítulo 4 estuda o problema da decidibildade e indecidibilidade de problemas. Veremos
alguns exemplos de problemas decidíveis e estudaremos o problema da Parada demons-
trando que nenhuma máquina de Turing pode resolvê-lo. Introduziremos o método de
redutibilidade de problemas usado para provar que um problema é indecidível.
∙ Capítulo 5 introduz a teoria de complexidade de algoritmos. Iremos explicar como a
complexidade de algoritmos é medida usando a notação pequeno e grande “O”. Em se-
guida iremos descrever as classes de complexidade P e NP discutindo as suas diferenças,
inclusive o problema em aberto P = NP. Este problema irá nos levar a definir a classe
de problemas NP-completos.
Em cada capítulo existem alguns exercícios que podem ser usados para verificar se o conhe-
cimento foi realmente adquirido.
Este livro não foi elaborado para ser uma fonte exaustiva, mas simplesmente um guia para
um aluno que esteja cursando uma disciplina de Teoria da Computação. Provamos alguns
teoremas no livro, mas muitos deixamos ou como exercícios ou sugerimos os leitores mais inte-
ressados lerem alguma referência mais completa. Reforçamos também que existe muito material
disponível na Internet, incluindo exercícios. Sugerimos, portanto, que os leitores procurem esses
materiais caso a explicação dada neste livro não entre em todos os detalhes.
Finalmente, eu gostaria de agradecer os meus alunos da disciplina de Teoria da Computação
que ajudaram formular a organização deste livro. Em especial, agradeço Diogo Dantas que me
ajudou na identificação de erros nos primeiros capítulos deste livro.
2
Sumário
1 Matemática Elementar 5
1.1 Conceitos Preliminares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 Métodos de Prova . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2 Introdução a Linguagens Formais e Autômatos 16
2.1 Linguagens Regulares e Autômatos Finitos . . . . . . . . . . . . . . . . . . . . . 16
2.1.1 Formalização Matemática de Autômatos Finitos . . . . . . . . . . . . . . 17
2.1.2 Formalização de uma Computação . . . . . . . . . . . . . . . . . . . . . 21
2.2 Autômatos Não-Determinísticos . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.2.1 Equivalência entre AFDs e AFNDs . . . . . . . . . . . . . . . . . . . . . 27
2.3 Operações Regulares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.4 Expressões Regulares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.5 Linguagens Não Regulares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.6 Linguagens Livres de Contexto e Autômatos de Pilha . . . . . . . . . . . . . . . 36
2.7 Linguagens que não são Livres de Contexto . . . . . . . . . . . . . . . . . . . . . 42
3 Máquinas de Turing e Decidibilidade 46
3.1 Máquinas de Turing Não Determinísticas . . . . . . . . . . . . . . . . . . . . . . 53
3.2 Tese de Church-Turing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4 Indecidibilidade e Redutibilidade 58
4.1 Exemplos de Linguagens Decidíveis . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.2 O Problema da Parada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.3 Prova da Indecidibilidade do Problema da Parada – Teorema 4.6 . . . . . . . . . 65
4.4 Linguagens Não Recursivamente Enumeráveis . . . . . . . . . . . . . . . . . . . 67
4.5 Redutibilidade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3
5 Introdução à Teoria de Complexidade 73
5.1 Notação Pequeno e Grande “O” . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.2 A Classe P . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.3 A Classe NP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
5.4 P versus NP e a Classe NP-completa . . . . . . . . . . . . . . . . . . . . . . . . 83
Capítulo 1
Matemática Elementar
Neste capítulo iremos revisar alguns conceitos elementares da matemática discreta que serão
utilizados em muitas partes deste livro. Vamos aproveitar também para fixar a notação que
usaremos. Para aqueles que já tem uma formação sólida nos tópicos descritos neste capítulo,
podem-se sentir livres para simplesmente dar uma foleada neste capítulo para entender melhor
a notação usada. (Resolver alguns exercícios também não seria uma má ideia.)
Começaremos do básico, relembrando conceitos de conjuntos, relações e funções, para depois
descrever alguns métodos de prova que usaremos.
1.1 Conceitos Preliminares
Conjuntos Um conjunto é uma coleção de elementos. Certamente você já deve ter visto
e estudado um número de conjuntos. Por exemplo, o conjunto dos números naturais N =
{0, 1, 2, 3, . . .} ou o conjunto dos números reais R que incluem os números√2, 𝜋, 𝜖. Os objetos
de um conjunto são os seus elementos.
Existem várias formas de escrever um conjunto. A mais familiar é listando os elementos
dentre de chaves { }. Por exemplo, o conjunto de cinco cores:
C = {azul, vermelho, laranja, violeta, amarelo}
A cor azul pertence ao conjunto acima. Nós usamos o símbolo ∈ para especificar que um objeto
pertence a um conjunto, por exemplo, azul ∈ C, e usamos o símbolo /∈ para especificar que
um objeto não pertence a um conjunto, por exemplo, rosa /∈ C. Conjuntos com um número
finito de elementos são classificados de finitos, por exemplo o conjunto C acima, e conjuntos
com um número infinito de elementos são classificados de infinitos, por exemplo o conjunto dos
números naturais.
5
Dados dois conjuntos A e B, dizemos que A é um subconjunto de B, escrito A ⊆ B, se
todo elemento de A é um elemento de B. Por exemplo, temos que
{1, 2, 3} ⊆ {1, 2, 3, 4}
Caso haja pelo menos um elemento em A que não é um elemento de B dizemos que A não é
um subconjunto de B, escrito A * B. Por exemplo:
{1, 2, 3, 𝑎} * {1, 2, 3, 4}
já que o objeto 𝑎 pertence a {1, 2, 3, 𝑎}, mas não pertence a {1, 2, 3, 4}. Dizemos que um
conjunto A é um subconjunto próprio de B se A ⊆ B, mas B * A.
Uma outra forma, mais formal, de descrever um conjunto é especificando a propriedade dos
seus elementos. Por exemplo:
A={𝑛 | 𝑛 ∈ N, 𝑛 > 5}B={𝑛 | 𝑛 ∈ N, 𝑛 é divisível por 5}
Este tipo de forma de descrever um conjunto é muito útil quando descrevemos um conjunto
infinito. O conjunto A é o conjunto de números naturais maiores que 5, quer dizer, o conjunto
{6, 7, 8, 9, . . .}. Já o conjunto B é o conjunto de números divisíveis por 5, quer dizer, o conjunto
{0, 5, 10, 15, . . .}.Finalmente A e B são conjunto iguais, escrito A = B, se e somente se A ⊆ B e B ⊆ A.
Perceba que de acordo com essa definição de equivalência, o número de cópias do mesmo objeto
em um conjunto não importa como também não importa a ordem em que os objetos são escritos.
Por exemplo, os seguintes conjuntos são todos iguais:
{1, 2, 3, 4} {1, 1, 2, 2, 2, 3, 4} {4, 2, 1, 3}
Isso porque cada um dos três conjuntos acima tem os mesmos objetos.
Para provar que dois conjuntos A e B são iguais precisamos provar duas propriedades:
1. A ⊆ B, quer dizer todos os elementos de A são elementos de B;
2. B ⊆ A, quer dizer todos os elementos de B são elementos de A.
Podemos fazer isso checando um elemento de cada vez quando os conjuntos A e B
Dica 1.1
são finitos, ou utilizamos as propriedades dos elementos dos conjuntos. Por exemplo
sejam:
A = {𝑛 | 𝑛 ∈ N.𝑛 é par} e B = {𝑛 | ∃𝑘 ∈ N.𝑛 = 2× 𝑘}
Provemos que A ⊆ B. Seja 𝑛 um elemento qualquer de A. Sabemos que 𝑛 é par.
Por definição de um número natural par, ela é da forma 𝑛 = 2 × 𝑟 para um 𝑟 ∈ N.
Devemos mostrar 𝑛 ∈ 𝐵. Mas como 𝑛 = 2 × 𝑟 temos que 𝑛 ∈ B por definição do
conjunto B. De uma forma similar provamos que B ⊆ A
Mostre que os conjuntos A′ e B′ abaixo não são iguais:
A′ = {𝑛 | 𝑛 ∈ N.𝑛 é par} e B′ = {𝑛 | ∃𝑘 ∈ N.𝑛 = 2× 𝑘 e 𝑘 ≥ 5}
Tente provar que ¯A ∪ B = A ∩ B.
O conjunto com nenhum elemento é chamado de vazio é representado pelo símbolo ∅ 1.
Dado dois conjuntos A e B, definimos as seguintes operações:
∙ União: – A união de dois conjuntos, escrito A ∪ B, é o conjunto obtido ao combinar os
elementos de A e B. Por exemplo, {1, 2, 3, 𝑎, 𝑏} ∪ {1, 4, 𝑐} é o conjunto {1, 2, 3, 4, 𝑎, 𝑏, 𝑐};
∙ Interseção – A interseção de dois conjuntos, escrito A ∩ B, é o conjunto que contém
exatamente os objetos que pertencem a ambos A e B. Por exemplo, {1, 2, 3, 𝑎, 𝑏}∩{1, 4, 𝑐}é o conjunto {1};
∙ Complemento – Dado um conjunto de todos os objetos, U, muitas vezes chamado de
universo, o complemento de A, escrito A, é o conjunto de objetos que pertencem em U,
mas não pertencem a A.
Sequências Uma sequência de objetos é uma lista de objetos em uma ordem em particular.
Normalmente, escrevemos uma sequência de objetos usando parenteses ( e ). Por exemplo,
(1, 20, 7)
é uma sequência de três números. Perceba que enquanto em um conjunto a ordem e o número
de cópias do mesmo elementos não importam, esse não é o caso de sequências. Por exemplo,1Alguns livros usam também o símbolo {}.
as seguintes sequências são diferentes uma da outra:
(1, 20, 7) (1, 20, 7, 1) (20, 7, 1)
Como conjuntos, sequências podem ser finitas ou infinitas. Sequências finitas são normalmente
chamadas de tuplas. Uma sequência de 𝑘 elementos é chamada de 𝑘−tupla. Uma 2−tupla
é também chamada de um par. As sequências acimas são exemplos de sequências finitas,
enquanto a sequência abaixo é infinita:
(1, 3, 5, 7, 9, . . .)
Podemos especificar conjuntos de sequências, em particular, usando operações sobre conjuntos.
Dado dois conjuntos A e B:
∙ Produto Cartesiano – O produto cartesiano de A e B, escrito A × B é o conjunto de
pares {(𝑎, 𝑏) | 𝑎 ∈ A, 𝑏 ∈ B}. Por exemplo, seja A = {0, 1} e B = {𝑎, 𝑏, 𝑐}, então:
A× B = {(0, 𝑎), (0, 𝑏), (0, 𝑐), (1, 𝑎), (1, 𝑏), (1, 𝑐)}
é o conjunto de pares. Podemos formar conjuntos de sequências ainda maiores usando
produtos cartesianos. Tente construir o conjunto (A× B)× C onde C = {azul, laranja}.Escrevemos A𝑘 para representar o produto cartesiano A × A × · · · × A, onde A é usado
𝑘 vezes;
∙ Conjunto de Partes – O conjunto de partes de um conjunto 𝐴, escrito 𝒫(A), é o
conjunto de todos os subconjuntos de A. Por exemplo, o conjunto de partes do conjunto
A = {1, 2, 3} é
𝒫(A) = {∅, {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}}
Perceba que o conjunto vazio é um subconjunto de qualquer conjunto.
Strings e Linguagens Strings tem um papel muito importante na ciência da computação.
Como iremos ver nos próximos capítulos, podemos representar problemas concretos da ciência
da computação, por exemplo, o problema de ordenar uma lista, usando conjunto de strings.
Strings são construídos a partir de um conjunto de símbolos chamado de alfabeto. São
exemplos de alfabetos:
Σ = {0, 1} Γ = {𝑎, 𝑏, 𝑐, 𝑑, 𝑒, . . . , 𝑡, 𝑢, 𝑣, 𝑧}
Normalmente usaremos as letras gregas Σ,Γ,Θ para representa alfabetos.
Uma string sobre um alfabeto Σ é uma sequência de objetos de Σ, escrita normalmente
um ao lado do outro sem o uso de parenteses e vírgulas. Por exemplo, 0101000 é uma string
do alfabeto {0, 1}, enquanto 𝑞𝑤𝑒𝑟𝑡𝑦 é uma string do alfabeto Γ acima.
O comprimento de uma string 𝑤, escrito |𝑤|, é o número de objetos em 𝑤. Por exemplo,
|0101000| = 7 e |𝑞𝑤𝑒𝑟𝑡𝑦| = 6. A string de comprimento zero é chamada de string vazia e
representada pelo símbolo 𝜖. Se uma string 𝑤 tem comprimento 𝑛, podemos escrever 𝑤 como
𝑤1𝑤2 · · ·𝑤𝑛 onde 𝑤𝑖 ∈ Σ para 1 ≤ 𝑖 ≤ 𝑛. Escrevemos o reverso de uma string 𝑤 como 𝑤𝑅.
Por exemplo, 𝑞𝑤𝑒𝑟𝑡𝑦𝑅 é a string 𝑦𝑡𝑟𝑒𝑤𝑞.
Se temos uma string 𝑤 de tamanho 𝑛 e uma string 𝑧 de tamanho 𝑚, a concatenação
de 𝑤 com 𝑧 é a string 𝑤𝑧 de comprimento 𝑛 + 𝑚. Usamos a notação 𝑤𝑘 para representar a
concatenação da string 𝑤 com ela mesma 𝑘 vezes. Por exemplo, (𝑎𝑏𝑎)3 é a string 𝑎𝑏𝑎𝑎𝑏𝑎𝑎𝑏𝑎.
Uma linguagem é um conjunto de strings. Linguagens terão um papel muito importante
neste livro. Iremos investigar nos próximos capítulos diversas linguagens, algumas mais ex-
pressivas que outras, quer dizer tipos de linguagens que podem representar diferentes tipos de
problemas computacionais.
Dada uma linguagem, L, definimos o conjunto L𝑖 como a linguagem formada por concatenar
𝑖 strings de L. Formalmente, definimos estes conjuntos da seguinte forma:
L0 = {𝜖}
L1 = L
L𝑖+1 = {𝑤𝑣 | 𝑤 ∈ L𝑖, 𝑣 ∈ L}
Por exemplo, {𝑎, 𝑏}2 é a linguagem {𝑎𝑎, 𝑎𝑏, 𝑏𝑎, 𝑏𝑏}.A operação estrela de Kleene (ou simplesmente estrela) de uma linguagem L é definida
como o seguinte conjunto:
L* = L0 ∪ L1 ∪ L2 ∪ L3 ∪ · · ·
Ou seja a união de todas as formas de concatenar strings de L. Podemos escrever a definição
acima da forma abaixo mais precisa:
L* =⋃𝑖∈N
L𝑖
Por exemplo, {𝑎, 𝑏}* contém as strings 𝑎𝑎𝑎, 𝑎𝑏𝑎, 𝑎𝑏𝑏𝑏𝑎, 𝑏𝑎𝑏𝑎𝑏𝑎.
A soma de Kleene de uma linguagem L, escrita L+ é semelhante a operação estrela de
Kleene, mas não inclui L0, quer dizer:
L+ =⋃
𝑖∈N∖{0}
L𝑖
Funções e Relações Uma função é um objeto que tem um comportamento de entrada e
saída, quer dizer dada uma entrada uma função produz uma saída. Numa função, dada a
mesma entrada se obtém a mesma saída. Conhecemos um número de funções na aritmética,
por exemplo as funções de adição + e multiplicação ×. Dado dois números essas funções sempre
retornam o mesmo número: 2 + 2 resulta sempre em 4 como também 4× 2 resulta sempre 8.
Formalmente usamos dois conjuntos para definir uma função, 𝑓 , o seu domínio, D e sua
imagem I, escrita normalmente como 𝑓 : D → I. O domínio de uma função especifica os objetos
de entrada da função, enquanto que a sua imagem especifica os objetos de saída da função. Por
exemplo, a função adição leva um par de números naturais e resulta em um número natural,
+ : N× N → N.
Podemos representar uma função de diversas maneiras. Por exemplo usando um programa
de computador. Uma forma mais simples é simplesmente listando os valores de entrada e saída
em uma tabela. Por exemplo a função 𝑓 : {1, 2, 3} → {𝑎, 𝑏, 𝑐} é especificada abaixo:
x f(x)
1 a
2 b
3 c
A função 𝑓 especifica a ordem das letras 𝑎, 𝑏, 𝑐.
Uma relação sobre um conjunto de 𝑘−tuplas é um subconjunto de 𝐴𝑘. Por exemplo, a
relação < é uma relação sobre o conjunto de 2−tuplas de números naturais. Em particular,
temos que (2, 3) pertence a relação <, normalmente escrita como 2 < 3, enquanto (2, 1) não
pertence a relação <. A relação de parentesco é uma outra relação que usamos no dia-a-dia.
Esta relação é um subconjunto do produto cartesiano de pessoas. Por exemplo, se João é pai
de Maria então o par (João, Maria) pertence a relação de parentesco, enquanto que se Marcelo
e João não são parentes então não pertencerão a relação de parentesco.
Relações binárias podem ter zero ou mais das seguintes propriedades:
∙ Reflexiva – Uma relação 𝑅 é reflexiva se para todo elemento 𝑥, (𝑥, 𝑥) ∈ 𝑅;
∙ Simétrica – Uma relação 𝑅 é simétrica se toda vez que (𝑥, 𝑦) ∈ 𝑅 então (𝑦, 𝑥) ∈ 𝑅;
∙ Transitiva – Uma relação 𝑅 é transitiva se toda vez que (𝑥, 𝑦) ∈ 𝑅 e (𝑦, 𝑧) ∈ 𝑅, então
(𝑥, 𝑧) ∈ 𝑅.
Por exemplo, a relação < é transitiva, mas não é reflexiva nem simétrica. Dizemos que uma
relação é uma equivalência se ela possui todas as três propriedades acima. A relação = é uma
equivalência.
1.2 Métodos de Prova
Como um livro sobre teoria da computação, é de se esperar que iremos provar alguns teoremas.
Nós revisaremos três métodos de prova que iremos utilizar. Antes contudo é preciso esclarecer
a diferença entre Definições, Proposições, Lemas, Teoremas e Corolários.
∙ Definições – Uma definição descreve um objeto e as noções que usamos. Por exemplo,
acima definimos algumas operações em conjuntos como a união, interseção e o conjunto de
partes. Definições são aceitas (caso bem formuladas) sem precisar provar nada. Contudo,
pode-se questionar a validade de um definição quando esta não pode ser aplicada. Por
exemplo, não faz sentido definir a pessoa mais feliz já que não parecem existir critérios
para medir felicidade.
∙ Proposições – Uma proposição é uma propriedade de um conjunto de objetos. Estes
resultados geralmente são interessantes e devem ser provados. Contudo proposições não
tem o mesmo impacto que teoremas. Um exemplo de proposição é o fato que cada número
natural tem um sucessor.
∙ Lemas – Um lema é um resultado intermediário que é usado na prova de um teorema.
Assim como proposições e teoremas, lemas requerem uma prova. Lemas geralmente não
tem uma importância por si só, mas é um instrumento usado para organizar a prova de
um teorema em resultados e provas menores.
∙ Teorema – Teoremas são os resultados principais que estamos buscando. Teoremas são
a base do avanço de teorias pois esclarecem propriedades de objetos. Por exemplo, o
teorema de Pitágoras estabelece uma relação não trivial entre os lados de um triângulo
retângulo.
∙ Corolários – Um corolário é um resultado de interesse que é um resultado imediato de
um teorema. Um corolário pode ser uma instância de um teorema.
Provas Dedutivas Uma prova é uma sequência de argumentos cuja veracidade leva de um
estado inicial, chamada de hipóteses, até uma conclusão. Cada passo de uma prova deve seguir
ou de um fato aceito, por exemplo uma definição, ou de outros argumentos, por exemplos lemas.
Um teorema da forma “se H então C” normalmente pode ser provado partindo de 𝐶 e chegando
em 𝐻 ou partindo de 𝐻 e chegando em 𝐶.
Considere o seguinte teorema:
Teorema 1.1. Se 𝑥 ≥ 4, então 2𝑥 ≥ 𝑥2.
Demonstração. Neste teorema, a hipótese é que 𝑥 ≥ 4 enquanto que a conclusão é 2𝑥 ≥ 𝑥2.
Não é difícil se convencer que o teorema é verdade. A ideia é que a função 2𝑥 cresce mais
rapidamente que a função 𝑥2. Porém, para valores de 𝑥 menores que 4, por exemplo, 3, não é
verdade que 23 = 8 ≥ 9 = 32.
A função 2𝑥 dobra quando incrementamos 𝑥, enquanto que a função 𝑥2 cresce com um fator
(𝑥+1𝑥)2. Quando 𝑥 ≥ 4 não pode ser que (𝑥+1
𝑥)2 seja maior que 2. Por exemplo, quando 𝑥 = 4 o
valor de (𝑥+1𝑥)2 é 1, 5625 e este valor diminui ao aumentar 𝑥. Assim terminamos essa prova de
maneira precisa, apesar de ainda informal.
Provas por Contradição Outro método de prova é usando contradição. Neste método,
assumimos que o teorema a ser provado é falso e mostramos que essa hipótese resulta em um
resultado claramente falso. Um exemplo clássico é a prova da existência de números que não
são racionais. (Lembrando que um número é racional se este número pode ser escrito da forma𝑎𝑏onde 𝑎, 𝑏 ∈ N são números naturais, por exemplo, 3
2, 21.)
Teorema 1.2. O número√2 não é racional.
Demonstração. Assumimos por contradição que o número√2 é racional. Então ele pode ser
escrito na forma√2 = 𝑎
𝑏. Assumimos ainda que 𝑎1 e 𝑏1 são os menores números cuja fração
𝑎1𝑏1
=√2. Neste caso, não é possível que ambos 𝑎1 e 𝑏1 são pares, pois se fossem poderíamos
dividir 𝑎1 e 𝑏1 por 2 e a fração resultante ainda seria igual a√2.
Agora temos que:
𝑏1 ×√2 = 𝑎1
Elevando ambos os lados ao quadrado, temos que:
𝑏21 × 2 = 𝑎21
Como 𝑎21 é duas vezes 𝑏21, temos que 𝑎21 é par. Mas como o quadrado de um número ímpar
é sempre ímpar, temos que 𝑎1 é par. O que significa que 𝑎1 = 2 × 𝑘 algum número 𝑘 ∈ N.
Substituindo este fato na equação acima, temos:
𝑏21 × 2 = (2× 𝑘)2 = 4× 𝑘2
O que significa que 𝑏21 = 2×𝑘2 é também par. Isso significa que 𝑏1 é par. Isso significa que 𝑎1 e
𝑏1 são ambos pares o que constitui um absurdo. Portanto, a nossa hipótese que√2 é racional
não é verdade, isto é,√2 não é racional.
Provas por Indução Na matemática discreta, em particular, na ciência da computação, o
método de prova por indução é o mais amplamente utilizado. Através deste método, pode-se
provar que todos os objetos de conjunto (possivelmente infinito) tem alguma propriedade.
Uma prova de indução é constituído de duas partes: (1) o(s) caso(s) base e (2) e o(s) caso(s)
de indução. Enquanto o(s) caso(s) base provam que o objeto mais básico do conjunto tem um
propriedade, o(s) caso(s) de indução demonstram que se um objeto menor tem uma propriedade
então um objeto maior também tem uma propriedades.
Por exemplo, no caso dos números naturais, existe somente um caso base: devemos provar
que uma propriedade é verdadeira para o número 0. Se a propriedade é 𝑃 então representamos
esta propriedade por 𝑃 (0). O caso de indução é a prova que se a propriedade é válida para um
número genérico 𝑛, escrito 𝑃 (𝑛), então a propriedade é válida para a seu sucessor 𝑛+1, escrito
𝑃 (𝑛+ 1). O seguinte teorema é um exemplo cuja prova é por indução:
Teorema 1.3. Para todo 𝑛 ∈ N temos que:
1 + 2 + · · ·+ 𝑛 =𝑛(𝑛+ 1)
2.
Demonstração. A prova é por indução. A propriedade é 𝑃 (𝑛) = “1 + 2 + · · ·+ 𝑛 = 𝑛(𝑛+1)2
”.
∙ Caso base, 𝑃 (0): Perceba que a soma a esquerda é reduzida a um caso sem soma. Dizemos
portanto que a soma é 0. Do lado direito, temos que 0(0+1)2
= 0. Portanto, temos que
𝑃 (0) é verdadeira.
∙ Caso de indução. Assumimos que 𝑃 (𝑛) é verdadeira para um 𝑛 genérico. Vamos tentar
provar 𝑃 (𝑛+ 1), escrito abaixo:
1 + 2 + · · ·+ 𝑛+ 𝑛+ 1 =(𝑛+ 1)(𝑛+ 1 + 1)
2.
Mas sabemos que a soma 1 + 2 + · · · + 𝑛 = 𝑛(𝑛+1)2
já que 𝑃 (𝑛) é verdadeira. Portanto
temos que provar que:
𝑛(𝑛+ 1)
2+ 𝑛+ 1 =
(𝑛+ 1)(𝑛+ 1 + 1)
2.
Colocando 𝑛+ 1 em evidência no lado esquerdo temos a equação acima é equivalente a:
(𝑛+ 1)(𝑛+ 2)
2=
(𝑛+ 1)(𝑛+ 1 + 1)
2.
Quer dizer que 𝑃 (𝑛+ 1) é verdadeiro, terminando a prova por indução.
Indução pode ser usado em muitas estruturas de dados em ciência da computação. Por
exemplo, em árvores. A menor árvore é a árvore de um nó. Portanto o caso base consiste em
provar que a propriedade a ser provada é válida para todas as árvores de um nó somente. No
caso de indução assumimos que as árvores 𝑡1, . . . , 𝑡𝑛 tem a propriedade 𝑃 a ser provada, isto é
𝑃 (𝑡1), . . . , 𝑃 (𝑡𝑛) são todas verdadeiras, e temos que provar que a árvore (maior) com raiz 𝑟 e
galhos 𝑡1, . . . , 𝑡𝑛 também tem a propriedade 𝑃 a ser provada.
Exercícios
Exercício 1.1 Diga se os conjuntos abaixos são bem definidos:
∙ A coleção de todos os símbolos alfanuméricos;
∙ A coleção de todas as pessoas altas;
∙ A coleção de números naturais 𝑥 tal que 2𝑥+ 10 = 20;
∙ A coleção de números reais 𝑥 tal que 2𝑥+ 10 = 20;
∙ A coleção de todos os bons jogadores de futebol.
Exercício 1.2 Verdadeiro ou falso:
∙ ∅ = {0};
∙ 𝑥 ∈ {𝑥};
∙ ∅ = {∅};
∙ ∅ ∈ {0};
Exercício 1.3 Para cada item, escreva um exemplo de uma relação que satisfaça a condição
correspondente:
∙ Reflexiva e transitiva, mas não simétrica;
∙ Reflexiva e simétrica, mas não transitiva;
∙ Simétrica e transitiva, mas não reflexiva.
Exercício 1.4 Se A tem 𝑛 elementos e B tem 𝑚 elementos, prove que A × B tem 𝑛 × 𝑚
elementos.
Exercício 1.5 Sendo A um conjunto de 𝑛 elementos, demonstre que o conjunto de partes de
A, 𝒫(𝐴) tem 2𝑛 elementos.
Exercício 1.6 Enuncie e demonstre a equação da soma dos primeiros 𝑛 números pares.
Exercício 1.7 Prove por indução que as seguintes propriedades são verdadeiras para todos os
números naturais 𝑛 ∈ N:
∙ 02 + 12 + · · ·+ 𝑛2 = 𝑛(𝑛+ 1)(2𝑛+ 1)/6;
∙ 20 + 21 + · · ·+ 2𝑛 = 2𝑛+1 − 1;
∙ 𝑛2 + 𝑛 é divisível por 2.
Exercício 1.8 Considere uma festa em que pessoas se cumprimentam, duas a duas, com apertos
de mão. Demonstre que o número de pessoas que apertou um número ímpar de pessoas é sempre
par. (Dica: Separe dois grupos, um grupo de pessoas que cumprimentaram um número ímpar
de pessoas, 𝐼, e outro grupo de pessoas que cumprimentaram um número par de pessoas. Veja
o que acontece com exemplo concretos.)
Capítulo 2
Introdução a Linguagens Formais e
Autômatos
A primeira questão com que a teoria da computação começa é “O que é um computador?”.
Parece um pouco estranho fazer essa pergunta já que todos nós já tivemos contato com um
computador e até temos uma noção do que um computador faz. Mas do ponto de vista teórico,
gostaríamos de formalizar matematicamente essas noções e estudar as reais capacidades de um
computador. Encontrar um modelo matemático que tenha detalhes suficientes para realizar
esse estudo sem entrar em muitos detalhes, por exemplo, o funcionamento do circuito elétricos
de um computador, foi uma das grandes contribuições de um pouco mais da primeira metade
do século XX.
Começaremos com modelos matemáticos de um computador, chamado modelo compu-
tacional, mais simples (e menos poderosos) chamados de autômatos também chamada de
máquina de estado.
2.1 Linguagens Regulares e Autômatos Finitos
Autômatos finitos são modelos computacionais de máquinas com memória bastante limitada.
Apesar de limitadas, muitos sistemas que conhecemos podem ser modelados usando autômato
finitos. Por exemplo, portas automáticas, elevadores, reconhecimento de senhas, etc.
Considere o problema de reconhecer uma senha, por exemplo a senha (bem simples) ‘bra’.
Podemos especificar um sistema que reconheça essa senha usando o seguinte diagrama de
estados:
16
q1 q2 q3 q4
q5
b r a
6= b 6= r6= a
Esta máquina de estados tem 5 estados 𝑞1, 𝑞2, 𝑞3, 𝑞4, 𝑞5. A seta apontando para o estado 𝑞1
especifica que a máquina começa no estado 𝑞1, já o estado 𝑞5 está marcado com dois círculos
especifica que o estado 𝑞5 é o estado final de aceitação. O funcionamento desta máquina
simplesmente lê o primeiro símbolo e caso este seja ‘b’, a máquina migra para o estado 𝑞2.
Caso contrário, a máquina migra para o estado 𝑞5. Uma vez no estado 𝑞5, a máquina não tem
como sair de lá, especificado pela seta apontando de 𝑞5 para o mesmo estado 𝑞5. No estado 𝑞2 a
máquina lê o segundo símbolo e caso este seja ‘r’, a máquina migra para o estado 𝑞3. Finalmente
se o terceiro símbolo for ‘a’, a máquina migra para o estado final 𝑞4.
Portanto duas possível execução da máquina de estados acima podem ser, onde a primeira
o usuário entra o valor errado, enquanto na segunda o usuário entra o valor correto:
𝑞1b−→ 𝑞2
r−→ 𝑞3b−→ 𝑞5
q−→ 𝑞5
𝑞1b−→ 𝑞2
r−→ 𝑞3a−→ 𝑞4
𝑙
Normalmente, estamos interessados nas strings, 𝑤, que levam uma máquina, 𝑀 , do estado
inicial até um estado final (podem haver mais de um estado final numa máquina). Neste caso,
dizemos que a máquina 𝑀 aceita a string 𝑤. Por exemplo, a máquina acima aceita a string
‘bra’.
2.1.1 Formalização Matemática de Autômatos Finitos
Vimos a descrição intuitiva de uma máquina de estados e agora iremos tornar essas intuições
mais precisas. Considere a seguinte máquina de estados:
q1 q2 q30
1 10
0,1
Esta máquina de estados pode ser especificada conforme abaixo:
∙ O conjunto de estados Q = {𝑞1, 𝑞2, 𝑞3};
∙ O alfabeto de entrada Σ = {0, 1};
∙ O estado inicial 𝐼 = 𝑞1;
∙ O conjunto de estados finais {𝑞3};
∙ As transições 𝛿 especificadas pelas setas no diagrama acima podem ser especificadas por
um tabela:
𝛿 0 1
𝑞1 𝑞2 𝑞1
𝑞2 𝑞3 𝑞2
𝑞3 𝑞2 𝑞2
Esta tabela especifica, por exemplo, que se a máquina estiver no estado 𝑞2 e receber como
entrada 0, a máquina migra para o estado 𝑞3.
Este autômato aceita a string 010, já que ao receber esta string a máquina realiza a seguintes
transições terminando no estado final 𝑞3:
𝑞10−→ 𝑞2
1−→ 𝑞20−→ 𝑞3 𝑙
Ao invés de escrever toda a vez ‘a máquina de estados 𝑀 aceita a string 𝑤’, usamos a seguinte
notação
𝑀 � 𝑤
para dizer que a máquina 𝑀 aceita uma string 𝑤. A linguagem de uma máquina de estados 𝑀
é o conjunto de todas as strings que são aceitas pela máquina. Por exemplo a máquina acima
aceita todas as strings que tem um número ímpar de zeros maior que 1. A sua linguagem é
portanto:
𝐿(𝑀) = {𝑤 | 𝑤 contém 𝑘 > 1 zeros onde 𝑘 é ímpar}
A seguinte definição formaliza o que é um autômato finito:
Definição 2.1. Um autômato finito (ou máquina de estados) é uma tupla ⟨Q,Σ, 𝛿, 𝐼,F⟩,onde:
∙ Q é um conjunto de estados;
∙ Σ é um alfabeto de entrada;
∙ 𝛿 : Q× Σ −→ Q é uma função de transição;
∙ 𝐼 ∈ Q é o estado inicial;
∙ F ⊆ Q é o conjunto de estados finais.
Exemplos Seguem mais exemplos de autômatos finitos.
q1 q2
01
0
1
Este autômato é definido formalmente como uma tupla ⟨Q,Σ, 𝛿, 𝐼,F⟩ com os seguintes com-
ponentes:
∙ Q = {𝑞1, 𝑞2};
∙ Σ = {0, 1};
∙ 𝛿 é a função implementando a tabela abaixo:
𝛿 0 1
𝑞1 𝑞1 𝑞2
𝑞2 𝑞1 𝑞2
∙ 𝐼 = 𝑞1;
∙ F = {𝑞2}.
A linguagem desta máquina, quer dizer o conjunto de strings que levam do estado inicial
para um estado final, é:
𝐿(𝑀) = {𝑤 | 𝑤 termina com 1}
Por exemplo, a string 000111 é aceita pela máquina acima, mas 1110 não é aceita.
O próximo exemplo, mostrado na Figura 2.1, parece mais complicado, mas depois de algum
tempo deve ficar claro o que a máquina proposta está fazendo. A máquina começa no estado 𝑠
e tem dois estados finais 𝑞1 e 𝑟1. O alfabeto de entrada é Σ = {a, b}. Caso o primeiro símbolo
de entrada for ‘a’ a máquina migra para o estado 𝑞1 e uma vez neste estado, não é possível mais
s
q1
a b
q2
r1
r2
a
a a
a
b
bb
b
Figura 2.1: Exemplo de Autômato
migrar para o estados 𝑟1 e 𝑟2. Similarmente com 𝑟1 e 𝑟2 quando o primeiro símbolo de entrada
for ‘b’. Portanto podemos analisar essas duas partes separadamente. Elas de fato parece muito
com a máquina de estados que discutimos no exemplo anterior.
A submáquina da esquerda aceita strings que terminem em ‘a’ e o da direita aceita strings
que terminem com ‘b’. Como para entrar na máquina da esquerda e da direita o primeiro
símbolo deve ser ‘a’ e ‘b’, respectivamente, a string que é aceita pela máquina deve ter o mesmo
primeiro e último símbolo. Por exemplo, a máquina aceita ‘ababa’ e ‘baabab’, mas não aceita
‘abab’. Formalmente, a linguagem aceita por esta máquina é:
𝐿(𝑀) = {𝑤 | 𝑤 que começa e termina com o mesmo símbolo}
O terceiro exemplo dada duas máquinas de estado 𝑀1 e 𝑀2 abaixo:
qA qB
110
0
qC qD
001
1
M1M2
Podemos construir um autômato, chamada de autômato produto 𝑀1 × 𝑀2, que simula essas
duas máquinas. Este autômato irá aceitar alguma string 𝑤, se 𝑤 pode ser aceitar por 𝑀1 ou
𝑀2 ou ambas as máquinas.
qA, qC qB, qC
1
0
0
qA, qD qB, qD
111
0
0
Os estados da máquina 𝑀1 ×𝑀2 é o conjunto produto {𝑞𝐴, 𝑞𝐵} × {𝑞𝐶 , 𝑞𝐷}. Os estados finais
são os estados (𝑞1, 𝑞2) tal que 𝑞1 é um estado final de 𝑀1 ou 𝑞2 é um estado final de 𝑀2. Por
exemplo, o estado (𝑞𝐴, 𝑞𝐷) é um estado final de 𝑀1 ×𝑀2 já que 𝑞𝐴 é um estado final de 𝑀1.
Finalmente as transições do autômato produto simulam os passos de cada máquina como se elas
estivessem rodando independentemente. Por exemplo, existe a transição (𝑞𝐴, 𝑞𝐶)0−→ (𝑞𝐵, 𝑞𝐶)
pois a máquina 𝑀1 tem a transição 𝑞𝐴0−→ 𝑞𝐵 e a máquina 𝑀2 tem a transição 𝑞𝐶
0−→ 𝑞𝐶 .
Portanto a linguagem desta máquina é a união das duas máquinas:
𝐿(𝑀1 ×𝑀2) = 𝐿(𝑀1) ∪ 𝐿(𝑀2)
Veremos depois que esta é uma das propriedades básicas das linguagens aceitas por autômatos
finitos.
2.1.2 Formalização de uma Computação
Até agora nós definimos formalmente o que é um automata, mas apesar de termos explicado
como um autômato é executado, não definimos a sua execução formalmente.
Definição 2.2. Seja 𝑀 = ⟨Q,Σ, 𝛿, 𝐼,F⟩ um autômato e seja 𝑤 = 𝑤1𝑤2 · · ·𝑤𝑛 uma string do
alfabeto Σ. 𝑀 aceita 𝑤 se existe uma sequência de estados 𝑟0𝑟1 · · · 𝑟𝑛 tal que:
1. 𝑟0 é o estado inicial 𝐼;
2. 𝑟𝑛 ∈ F é um estado final;
3. 𝛿(𝑟𝑖, 𝑤𝑖+1) = 𝑟𝑖+1 para 0 ≥ 𝑖 ≤ 𝑛− 1.
O primeiros dois itens especificam que a máquina começa do estado inicial e termina em um
estado final. O terceiro item especifica que a string 𝑤 realmente leva de um estado para outro.
Definição 2.3. Se uma linguagem pode ser aceita por um autômato finito, classificamos esta
linguagem como regular.
Definir um autômato pode as vezes ser difícil quando não se tem muita prática. Não
existe uma receita para desenvolver um autômato para resolver algum problema. Ajuda,
contudo,perguntar:
“O que a máquina deve lembrar para resolver uma tarefa?”
Esta pergunta é fundamental pois um autômato tem sérias limitações de memória já
que a memória da máquina é finita.
Considere o seguinte problema: Assuma que o alfabeto de entrada é {0, 1} e que a
linguagem consiste em todas as strings com um número par de 0s. Como desenvolver
um autômato que reconheça essa linguagem?
Claramente, não podemos lembrar quantos 0s estão contidos em um string, pois
não sabemos se existe um limite para este número já que não existe nenhum limite
para o tamanho de strings. Portanto, precisamos pensar o que realmente precisamos
lembrar. Bem se lembrarmos que até um dado ponto da string de entrada nós vimos:
1. um número par de 0s;
2. um número ímpar de 0s.
Já que precisamos somente determinar se o número de 0s é par e não o número total
de 0s, as duas afirmações acima parecem ser suficiente. Portanto, precisamos de dois
estados: 𝑞𝑝𝑎𝑟 e 𝑞𝑚𝑝𝑎𝑟. Inicialmente, como não vimos nenhum 0 o estado inicial é 𝑞𝑝𝑎𝑟.
Já que estamos interessados nas strings que tem um número par de 0s, escolhemos
𝑞𝑝𝑎𝑟 também como o estado final.
Dica 2.2
Agora precisamos entender como é feita a transição de um estado para o outro.
Bem, se até agora a máquina recebeu um número par de 0s e recebemos mais um 0,
ela deve mudar para o estado 𝑞𝑚𝑝𝑎𝑟, mas se a máquina receber um 1, ela não deve
mudar de estado. Portanto, o automata final que obtemos é o seguinte:
qpar qimpar
110
0
2.2 Autômatos Não-Determinísticos
Os autômatos que introduzimos na seção anterior são determinísticos. Isso quer dizer que a
partir de um estado e um caractere de entrada, nós temos certeza para que estado a máquina
irá migrar. Apesar de muitos sistemas poderem serem determinísticos, existem outros os quais
é útil modelar o sistema como um sistema não-deterministico.
Por exemplo, em um sistema distribuído como um sistema de contas financeiras. Clientes
podem fazer débito ou crédito na sua conta. Imagine que um cliente faça ao mesmo tempo um
débito e um cheque seja depositado na sua conta. Como saber como será o comportamento
do sistema. Será que o cheque será compensado primeiro e depois o débito, ou o inverso?
Muitas vezes não dá para prever, já que não podemos ter uma ideia perfeita de quanto esse
dois processos demoram. Pensamos então que a ordem de como estas operações são realizadas
é não determinística.
Não-determinismo é um generalização de determinismo. Portanto, todo autômato determi-
nístico é um autômato não-determinístico, mas nem todo autômato não determinístico é um
autômato determinístico. O autômato a seguir é um exemplo de uma autômato não determi-
nístico assumindo o alfabeto de entrada Σ = {0, 1}:
q3ε
0
q1
q2
q4
0q5
1
0
Podemos observar três diferenças aos autômatos determinísticos:
1. O autômato a partir do estado inicial 𝑞1 e entrada 0, pode migrar ou para o estado 𝑞2 ou
𝑞4. Não podemos determinar a priori para que estado a máquina irá;
2. A máquina não informa todas as possibilidades de transição para cada símbolo do alfabeto.
Por exemplo, se a máquina estiver no estado 𝑞1 e receber como entrada 1, a máquina não
migra para nenhum estado. Neste caso dizemos que a máquina falha.
3. Finalmente, percebemos que existe uma transição do estado 𝑞2 ao estado 𝑞3 que está
indicada com 𝜖. Essa transição especifica que a máquina pode migrar para o estado 𝑞3
do estado 𝑞2 sem precisar receber um símbolo de entrada. Chamamos tais transições de
transições vazias.
Intuitivamente, as strings 0, e 00 são aceitas pela máquina acima usando as transições
abaixo:𝑞1
0−→ 𝑞2𝜖−→ 𝑞3
𝑞10−→ 𝑞4
0−→ 𝑞5
Perceba que a string 000 já não é aceita pela máquina acima.
Para entender melhor a execução de uma máquina não determinística, pense em máquinas
processando a mesma entrada em paralelo. No começo existe somente uma máquina 𝑀1. Assim
que houver uma escolha não determinística, por exemplo, as transições de 𝑞1 usando a entrada
0, criamos cópias da máquina uma para cada escolha não determinística. Por exemplo, ao
receber a entrada 0 no estado 𝑞1, existem duas possibilidade, ou a máquina migrou para o
estado 𝑞2 ou para o estado 𝑞3. Portanto, criamos duas máquinas 𝑀2 e 𝑀3, onde 𝑀2 migrou
para o estado 𝑞2 e 𝑀3 migrou para o estado 𝑞3. Da mesma forma, a máquina 𝑀2 que esta no
estado 𝑞2, pode migrar sem receber uma entrada para o estado 𝑞3 o que significa que mais uma
máquina 𝑀4 é criada. A Figura 2.2.
M1
0
M3M2
0
M4
ε
M5
1
M6
ε
M5
1
ε
· · ·
1
M7
0
Figura 2.2: Ilustração das máquinas paralelas representando uma execução de um autômato
não-determinístico.
Finalmente, dizemos que a máquina não determinística aceita uma string de entrada, 𝑤, se
uma dessas máquinas paralelas aceita 𝑤.
Podemos agora definir formalmente máquinas não determinísticas.
Definição 2.4. Um autômato finito não-determinístico (AFND) (ou máquina de estados
de não-determinística) é uma tupla ⟨Q,Σ, 𝛿, 𝐼,F⟩, onde:
∙ Q é um conjunto de estados;
∙ Σ é um alfabeto de entrada;
∙ 𝛿 : Q× Σ ∪ {𝜖} −→ 𝒫(Q) é uma função de transição;
∙ 𝐼 ∈ Q é o estado inicial;
∙ F ⊆ Q é o conjunto de estados finais.
onde 𝒫(𝑄) é o conjunto de partes de Q e 𝜖 é a string vazia.
Usaremos a sigla AFD para Autômato Finito Determinístico e a sigla AFND para Autômato
Finito Não-Determinístico
Percebemos duas diferenças da definição de AFND com relação a definição de AFD (Defi-
nição 2.1).
1. A primeira diferença é a função de transição tem como argumentos um estado do conjunto
Q e um símbolo do alfabeto ou a string vazia, 𝜖. Assim incluímos as transições sem entrada
no definição de AFND, Por exemplo, no AFND acima temos a seguinte transição:
𝛿(𝑞2, 𝜖) = {𝑞3}
Quer dizer que o único estado que a máquina pode migrar sem entrada do estado 𝑞2 é o
estado 𝑞3;
2. A segunda diferença é que a função de transição ao invés de levar a um estado Q leva ao
conjunto de partes 𝒫(Q) dos estados. Assim podemos especificar formalmente quais os
estados que um AFND pode migra de um estado dada uma entrada. Por exemplo, no
AFND acima, temos a seguintes transições:
𝛿(𝑞1, 0) = {𝑞2, 𝑞4} e 𝛿(𝑞1, 1) = ∅
Quer dizer, do estado 𝑞1 e entrada 0, a máquina pode migrar para os estados 𝑞2 e 𝑞4,
enquanto do mesmo estado mas entrada 1, a máquina não migra para nenhum estado,
sinalizando uma falha da máquina.
Uma forma de interpretar um AFND é que ela tem a capacidade de adivinhar para
que estado ela deve migrar. Na máquina acima, para a entrada 00, por exemplo, a
máquina pode adivinhar qual dos estados levará para um estado de aceitação. Neste
caso a máquina deverá adivinhar o estado 𝑞4, já que não é possível chegar ao estado
de aceitação se a máquina migrar para o estado 𝑞2.
Dica 3
Exemplo 2.5. Considere um outro exemplo de um AFND:
q3
ε0
q1
q2
1
1
Este AFND é definido formalmente da seguinte forma:
∙ O conjunto de estados é Q = {𝑞1, 𝑞2, 𝑞3};
∙ O alfabeto é Σ = {0, 1};
∙ A função de transição a função implementado a tabelo abaixo:
𝛿 0 1 𝜖
𝑞1 {𝑞2} {𝑞3} ∅𝑞2 ∅ {𝑞1} {𝑞3}𝑞3 ∅ ∅ ∅
∙ O estado inicial é 𝐼 = 𝑞1;
∙ O conjunto de estados finais é F = {𝑞3}.
2.2.1 Equivalência entre AFDs e AFNDs
Apesar de AFND parecerem mais poderosos que AFD, tendo a possibilidade de adivinhar para
que estado migrar rodando múltiplas máquinas em paralelo, provaremos nesta seção que AFDs
e AFNDs tem o mesmo poder de expressão. Provaremos o teorema abaixo:
Teorema 2.6. Para uma linguagem qualquer L
1. Se existe um AFD que aceita L, então existe um AFND que aceita L;
2. Se existe um AFND que aceita L, então existe um AFD que aceita L.
Dizemos que dois AFND são equivalentes se elas aceitam a mesma linguagem.
A parte 1 do Teorema 2.6 é trivial, pois sabemos que todo AFD é um AFND.1
A parte 2 do Teorema 2.6 já é mais complicado. Provaremos esta direção mostrando que
a partir de um AFND genérico, podemos construir um AFD que aceite a mesma linguagem.
Iremos generalizar a ideia que vimos no final da Seção 2.1.1 da construção do autômato produto
que simula a execução em paralelo de dois autômatos. Em particular, ao invés de construir um
autômato produto, iremos construir um AFD com o conjunto de partes.1Formalmente, precisamos mostrar como converter um AFD em um AFND. Basta usar conjuntos unitários,
isto é com um só elemento, na função de transição. Tente fazer isso.
Dado um AFND com 𝑘 estados, iremos construir um AFD com 2𝑘 estados. (Por que 2𝑘?
Veja Exercício 5.) E usaremos a mesma estratégia de construção do AFD como fizemos com o
AFD produto. Contudo, precisamos também levar em consideração as transições vazias.
Vamos formalizar esta ideia. Seja 𝑀 = ⟨Q,Σ, 𝛿, 𝐼,F⟩ um AFND qualquer. Construirmos
um AFD ⟨Q𝑀 ,Σ, 𝛿𝑀 , 𝐼𝑀 ,F𝑀⟩ que aceita a mesma linguagem da seguinte forma:
1. Q𝑀 = 𝒫(Q);
2. Para todo𝑅 ∈ Q𝑀 e 𝑎 ∈ Σ, definimos 𝛿𝑀(𝑅, 𝑎) = {𝑞 ∈ Q | 𝑞 ∈ 𝐸(𝛿(𝑟, 𝑎)) para algum 𝑟 ∈ 𝑅 },onde
𝐸(𝑅) = {𝑞 | 𝑞 pode ser alcançado de um estado em 𝑅 com transições vazias};
3. I𝑀 = {𝐼};
4. F𝑀 = {𝑅 ∈ Q𝑀 | F ∩𝑅 = ∅};
Vamos construir o AFD que aceita a mesma linguagem do AFND 𝑀 descrito no Exem-
plo 2.5. Como𝑀 tem 3 estados 𝑞1, 𝑞2, 𝑞3, o AFND terá 8 estados e transições conforme ilustrado
abaixo:
{q1}
1
{q2}
{q3}
∅
{q1, q2}
{q2, q3}
{q1, q3}
{q1, q2, q3}
0
00
0,1 1
0,1
0,1
1
0
1
Perceba que todos os estados contendo o estado 𝑞3 que é final na máquina𝑀 , estão marcados
como estados finais. O estado inicial é o conjunto {𝑞1} já que 𝑞1 é o estado inicial em 𝑀 . As
transições levam em consideração as transições vazias. Por isso que temos a transição
{𝑞1} 0−→ {𝑞2, 𝑞3}
pois a partir de 𝑞1 e entrada 0, a máquina migra para o estado 𝑞2 e com mais uma transição
vazia a máquina migra para o estado 𝑞3.
Podemos deduzir o seguinte corolário a partir do Teorema 2.6:
Corolário 2.7. Uma linguagem é regular se e somente se ela pode ser aceita por um AFND.
2.3 Operações Regulares
As linguagens regulares tem uma propriedade muito importante de serem fechadas com relação
as seguinte operações:
∙ União – quer dizer, se L1 e L2 são linguagens regulares, então L1 ∪ L2 também é uma
linguagem regular;
∙ Concatenação – quer dizer, se L1 e L2 são linguagens regulares, então a linguagem L =
{𝑤𝑣 | 𝑤 ∈ L1, 𝑣 ∈ L2}, escrita L1 ∘ L2, também é uma linguagem regular;
∙ Na operação estrela de Kleene (Veja Seção 1.1 para a definição da estrela de Kleene) – se
L é regular, então L* também é regular.
Provaremos cada uma das três afirmações acima.
Teorema 2.8. As linguagens regulares são fechadas pela operação de união.
Demonstração. Assuma que tenhamos duas linguagens regulares quaisquer L1 e L2, e iremos
provar que L1 ∪ L2 é uma linguagem regular.
Pela definição de linguagens regulares, existem duas máquinas finitas 𝑀1 e 𝑀2 que aceita
L1 e L2, respectivamente. Podemos construir um AFND a partir de 𝑀1 e 𝑀2 que aceita a
linguagem L1 ∪ L2 conforme o diagrama abaixo:
M1
M2
q0
ε
ε
Onde o estado 𝑞0 é novo, isto é, não aparece em 𝑀1 nem em 𝑀2. É fácil se convencer que o
AFND aceita a linguagem L1 ∪ L2. Para uma string da linguagem L1 a máquina migra usando
uma transição vazia para o estado inicial da máquina 𝑀1. Para uma string da linguagem L2 a
máquina migra usando uma transição vazia para a máquina 𝑀2. Usamos o poder de um AFND
de adivinhar para que estado a máquina irá migrar.
Como uma linguagem é regular se e somente se ela é aceita por um AFND (Corolário 2.7),
temos que L1 ∪ L2 é regular como queríamos demonstrar.
Teorema 2.9. As linguagens regulares são fechadas pela operação de concatenação.
Demonstração. Assuma que tenhamos duas linguagens regulares quaisquer L1 e L2, e iremos
provar que L = {𝑤𝑣 | 𝑤 ∈ L1, 𝑣 ∈ L2} é uma linguagem regular.
Pela definição de linguagens regulares, existem duas máquinas finitas 𝑀1 e 𝑀2 que aceita
L1 e L2, respectivamente. Sem perda de generalidade, considere que estas máquinas são da
forma ilustrada abaixo:
M1M2
Podemos construir um AFND a partir de 𝑀1 e 𝑀2 que aceita a linguagem L1 ∘L2 conforme
o diagrama abaixo:
M1M2
ε
ε
ε
O estado inicial deste AFND é o estado inicial da máquina 𝑀1. É fácil se convencer que o
AFND aceita a linguagem L1 ∘ L2. A máquina usa a máquina 𝑀1 para processar a substring
inicial que pertence a linguagem L1 e em seguida passa para a máquina 𝑀2 para processar
a substring restante que pertence a linguagem L2. Caso a substring inicial não pertença a
linguagem L1 e/ou a substring final não pertença a linguagem L2, pelo menos uma das máquinas
irá falhar.
Como uma linguagem é regular se e somente se ela é aceita por um AFND (Corolário 2.7),
temos que L1 ∘ L2 é regular como queríamos demonstrar.
Teorema 2.10. As linguagens regulares são fechadas pela operação da estrela de Kleene.
Demonstração. Assuma que tenhamos uma linguagem regular qualquer L e iremos provar que
L* é uma linguagem regular. Como L é regular, existe uma máquina 𝑀 que aceita L. Sem
perda de generalidade, assuma que a máquina seja da forma abaixo:
M
Podemos construir um AFND a partir de 𝑀 que aceita a linguagem L*, conforme abaixo:
M
ε
ε
Criamos um novo estado inicial que é ao mesmo tempo estado final. Isso é necessário para
capturar o fato que 𝜖 ∈ L*, isto é, o AFND deve aceitar a string vazia. Para cada estado final
de 𝑀 criamos uma transição vazia para o estado inicial de 𝑀 . Esta transição captura o fato
que um número arbitrário de strings de L podem ser concatenadas.
Como uma linguagem é regular se e somente se ela é aceita por um AFND (Corolário 2.7),
temos que L* é regular como queríamos demonstrar.
2.4 Expressões Regulares
Quando você busca uma string em um editor de texto é possível encontrar strings que sigam
algum padrão, por exemplo,
Todas as strings que começam e terminam com 0, ou strings que tenham pelo menos
um 1, ou strings que tenham comprimento par.
Para fazer isso é necessário inserir uma expressão regular no campo de busca. Nesta seção
iremos introduzir expressões regulares.
Expressões regulares pode ser vista como uma outra forma de escrever autômatos regulares.
Ao invés de usar transições e estados, podemos obter o mesmo efeito com expressões similares
as expressões que estamos acostumados na aritmética.
Definição 2.11. Seja Σ um alfabeto de entrada, 𝑅 é uma expressão regular se 𝑅 for da forma:
1. 𝑎 ∈ Σ – um símbolo do alfabeto;
2. 𝜖 – a string vazia;
3. ∅ – o conjunto vazio;
4. (𝑅1 ∪𝑅2) – onde 𝑅1 e 𝑅2 são expressões regulares;
5. (𝑅1𝑅2) – onde 𝑅1 e 𝑅2 são expressões regulares;
6. (𝑅*1) – onde 𝑅1 é uma expressão regular.
Os itens 1 e 2 representam as linguagens {𝑎} e {𝜖}, enquanto o item 3 representa a linguagem
∅. Se 𝑅1 e 𝑅2 representam as linguagens L1 e L2, então (𝑅1∪𝑅2) representa a linguagem L1∪L2;
(𝑅1𝑅2) representa a linguagem L1 ∪ L2; e (𝑅*1) representa a linguagem L*
1.
Alguns exemplos de expressões regulares assumindo um alfabeto Σ:
∙ 1Σ*0 – todas as strings que começam com o símbolo 1 e terminam com o símbolo 0;
∙ Σ*1Σ*1 – Todas as strings contendo pelo menos uma ocorrência do símbolo 1;
∙ (ΣΣ)* – todas as strings de comprimento par;
∙ 01 ∪ 10 – é a linguagem {01, 10};
∙ 0Σ*0 ∪ 1Σ*1 ∪ 0 ∪ 1 – todas as strings que começam e terminam com o mesmo símbolo.
Você pode encontrar muitos outros exemplos de expressões regulares no site
http://www.regxlib.com/.
Como pode-se perceber expressões regulares usam as operações regulares de união, conca-
tenação e estrela de Kleene, descritas acima. De fato, pode-se provar o seguinte teorema:
Teorema 2.12. Uma linguagem pode ser representada por uma expressão regular se e somente
se a linguagem é regular.
A prova deste teorema é um pouco técnica envolvendo a introdução de maquinário que não
será utilizado necessariamente no restante deste livro. Para os mais interessados recomendamos
ver a Seção 1.3 do livro Uma Introdução à Teoria da Computação de Michael Sipser.
2.5 Linguagens Não Regulares
Uma pergunta natural que podemos fazer é será que existe alguma linguagem que não é re-
gular, isto é, será que todas as linguagens podem ser aceitas por máquinas de estados finitos.
Não precisamos pensar muito para responder essa pergunta negativamente. Nós sabemos que
autômatos são finitos, quer dizer, tem uma memória finita. Portanto, linguagens cujas strings
não sabemos quanta memória precisamos para determinar não podem ser regulares.
Por exemplo considere a seguinte linguagem:
{0𝑛1𝑛 | 𝑛 ∈ N}
Para saber se uma string pertence a essa linguagem, precisamos que ela comece com 𝑛 0s e
termine com 𝑛 1s. Mas o valor de 𝑛 pode ser qualquer. Isto significa que não tem como
prever inicialmente quanta memória precisaremos. Para fazer o contraste, considere a seguinte
linguagem:
{0𝑛1𝑛 | 𝑛 ∈ N, 𝑛 ≤ 5}
Neste caso sabemos que 𝑛 pode ser no máximo 5 o que significa que podemos reconhecer strings
desta linguagem usando autômatos finitos. (Tente construir o autômato desta linguagem!)
Intuitivamente, o que foi dito acima faz sentido, mas como podemos formalizar e provar que
por exemplo a linguagem {0𝑛1𝑛 | 𝑛 ∈ N} não é regular? Temos que provar que não existe
autômato finito que aceite essa linguagem. Provar esse tipo de propriedade é geralmente um
pouco mais difícil. Felizmente, temos uma forma simples de fazer isso. Usaremos o Lema do
Bombeamento.
O Lema de Bombeamento define uma propriedade que todas as linguagens regulares demons-
tram. Caso uma linguagem não exibe esta propriedade, então esta linguagem não é regular.
Perceba, contudo, que se uma linguagem exibe essa propriedade, não podemos deduzir que
a linguagem é regular, pois podem existir linguagen não regulares que também exibem esta
propriedade.
No enunciado do Lema de Bombeamento, assumimos um valor chamado de comprimento
de bombeamento,p.
Teorema 2.13. Se a linguagemL é regular, então existe um númerop (comprimento de bombe-
amento), tal que para qualquer strings 2 L cujo comprimento seja maior ou igual ap, jsj � p,
então s pode ser particionado em três substringss = xyz tal que:
1. Para cadai � 0, xy i z 2 L;
2. jyj > 0;
3. jxyj � p.
Demonstração.Daremos aqui a intuição da prova.
Se a linguagemL é regular, então existe um AFNDM que aceitaL. Assuma queM tem
n estados. Então usamosp = n. Assuma uma strings 2 L com tamanho maior ou igual ap.
Sejas = s1s2s3 � � � sm . Portanto, uma execução da máquinaM usandos passa por um número
de estados começando do estado inicialq1 e terminando em um estado �nal,qf :
q1s1�! q s2�! � � � q0 sm�! qf
Como m � p, temos que o número de estados nesta execução é maior quep. Portanto, um
mesmo estado,u, deve ter sido passado mais de uma vez. Isto quer dizer que a execução acima
é da forma:
q1s1�! q s2�! � � � q
si�! usi +1�! r � � � t
sj�! u � � �sj +1�! q0 sm�! qf
Sejay string correspondente ao ciclo:
usi +1�! r � � � t
sj�! u
O comprimento dey é maior que zero. Podemos veri�car também que a stringxy i z 2 L para
qualquer i � 0. Por exemplo se retirarmos o ciclo, temos quexz 2 L. Se repetirmos o ciclo
duas vezes, temos quexyyz 2 L. Finalmente, com certeza, o ciclo deverá aparecer em pelo
menosp transições.
1. Como 𝐿𝑅 é regular, por definição existe um AFD, 𝑀 , que reconhece 𝐿𝑅;
2. Construímos a partir de 𝑀 , um autômato de pilha 𝑀 ′ que simula o comportamento de
𝑀 . Basta que 𝑀 ′ tenha exatamente as mesmas transições sem precisar da pilha.
3. É fácil verificar que 𝑀 ′ reconhece 𝐿𝑅;
4. Portanto, 𝐿𝑅 também é livre de contexto.
Corolário 2.22. Toda linguagem regular é uma linguagem livre de contexto.
Contudo nós vimos que o contrário não é verdade, já que a linguagem
{0𝑛11 | 𝑛 ∈ N}
não é regular, mas é livre de contexto.
2.7 Linguagens que não são Livres de Contexto
Assim como para mostrar que uma linguagem não é regular, podemos mostrar que existem
linguagens que não são livres de contexto. Um exemplo de tal linguagem é
{𝑤𝑤 | 𝑤 ∈ {0, 1}*}
Essa linguagem não é livre de contexto. Mas como podemos provar isso. Para prova que
uma linguagem não é regular usamos o lema de bombeamento (Veja Dica 4). Existe um lema
parecido para linguagens livres de contexto enunciado a seguir:
Teorema 2.23. Se 𝐿 é uma linguagem livre de contexto, então existe um número 𝑝 (o com-
primento de bombeamento) tal que, se uma string 𝑠 ∈ 𝐿 de comprimento maior ou igual a 𝑝,
|𝑠| ≤ 𝑝, então 𝑠 pode ser particionado como 𝑠 = 𝑢𝑣𝑥𝑦𝑧 onde:
1. para cada 𝑖 ∈ N, temos que 𝑢𝑣𝑖𝑥𝑦𝑖𝑧 ∈ 𝐿;
2. |𝑣𝑦| > 0;
3. |𝑣𝑥𝑦| ≤ 𝑝.
Não provaremos este teorema, mas somente o seu uso. Leitores mais interessados podem
revisar o Capítulo 2.3 do livro Uma Introdução à Teoria da Computação de Michael Sipser.
Provemos portanto que a linguagem
{𝑤𝑤 | 𝑤 ∈ {0, 1}*}
acima não é livre de contexto. Para isso considere um comprimento de bombeamento 𝑝 genérico
e a string 0𝑝1𝑝0𝑝1𝑝 e mostremos que esta string não pode ser bombeada. Para isso usamos a
terceira condição do teorema acima, quer dizer |𝑣𝑥𝑦| ≤ 𝑝. Precisaremos analisar os possíveis
casos de onde a string 𝑣𝑥𝑦 aparece em 0𝑝1𝑝0𝑝1𝑝:
∙ Se 𝑣𝑥𝑦 aparece na primeira metade ou na segunda metade, é fácil verificar que 𝑥𝑣2𝑥𝑦2𝑧
não é da forma 𝑤𝑤, pois ou acrescentamos símbolos a primeira metade e não a segunda
metade ou ao contrário;
∙ Se 𝑣𝑥𝑦 aparece exatamente na metade de 0𝑝1𝑝0𝑝1𝑝. Então 𝑣 é composto de 1s e 𝑦 de 0s.
Se bombearmos 𝑣, 𝑦 com 𝑖 = 0 apagaremos 0s e 1s obtendo uma string da forma 0𝑝1𝑘0𝑙1𝑝
onde 𝑘, 𝑙 < 𝑝 e esta string não é da forma 𝑤𝑤.
Portanto, como o lema de bombeamento não funciona para uma string da linguagem acima,
esta linguagem não livre de contexto.
Exercícios
Exercício 2.1 A descrição formal de um AF é ⟨{𝑞1, 𝑞2, 𝑞3}, {𝑎, 𝑏}, 𝛿, 𝑞2, {𝑞1, 𝑞2}⟩, onde 𝛿 é im-
plementa a tabela abaixo:
𝛿 𝑎 𝑏
𝑞1 𝑞3 𝑞1
𝑞2 𝑞1 𝑞3
𝑞3 𝑞1 𝑞3
Desenhe o diagrama desta máquina de estados.
Exercício 2.2 Desenha o diagrama de estados de um AFD que aceite as seguintes linguagens,
assumindo o alfabeto Σ = {0, 1}:
∙ Todas as strings que começam com 1 e terminam com 0;
∙ Todas as strings que contém pelo menos quatro ocorrências do símbolo 1;
∙ Todas as strings que tenham comprimento quatro e que o segundo símbolo é 0;
∙ Todas as strings exceto 00 e 111;
∙ Todas as strings cujas posições ímpares é o símbolo 0, por exemplo, 101010 e 100001;
∙ O conjunto vazio;
∙ Todas as strings exceto a string vazia.
Exercício 2.3 Desenha o diagrama de estados de um AFND que aceite as seguintes linguagens,
assumindo o alfabeto Σ = {0, 1}:
∙ Todas as strings de comprimento quatro que terminam em 00;
∙ A linguagem {1} com dois estados;
∙ A linguagem 0*1 * 0* com três estados;
∙ A linguagem 0* com um estado.
Exercício 2.4 Prove que todo AFND pode ser convertido em um AFND equivalente com um
único estado final.
Exercício 2.5 Converta o seguinte AFND em um AFD equivalente, conforme descrito na prova
do Teorema 2.6:
q2q1
a,b
b
a
Exercício 2.6 Escreva as expressões regulares que gerem as linguagens descritas no Exercí-
cio 2.2.
Exercício 2.7 Prove que as linguagens regulares são fechadas pela operação de complemento
e pela operação de interseção.
Exercício 2.8 Para cada uma das expressões regulares, escreva uma string que pertence e uma
que não pertence a linguagem gerada assumindo o alfabeto Σ = {0, 1}:
∙ 01 ∪ 10;
∙ (000)*;
∙ Σ*1Σ*1;
∙ 0* ∪ 1*;
∙ (𝜖 ∪ 1)0Σ*.
Exercício 2.9 Usando a propriedade do Lema de Bombeamento, prove que as linguagens
abaixo não são regulares:
∙ {0𝑖1𝑗0𝑖 | 𝑖 ≥ 𝑗};
∙ {𝑤𝑤𝑅 | 𝑤 ∈ {0, 1}};
∙ {02𝑛 | 𝑛 ∈ N};
∙ {0𝑖1𝑗0𝑖+𝑗 | 𝑖, 𝑗 ∈ M}.
Exercício 2.10 Desenha o diagrama de autômatos de pilha que reconheçam as seguintes lin-
guagens:
∙ {𝑤 | 𝑤 tem pelo menos três 1};
∙ {𝑤 | 𝑤 ∈ {0, 1}*, 𝑤 = 𝑤𝑅} – quer dizer o conjunto de palíndromos
∙ {𝑥𝑦 | |𝑥| = |𝑦| e 𝑥 = 𝑦}
∙ {𝑎2𝑛𝑏3𝑛 | 𝑛 ∈ N}.
∙ {𝑎𝑛𝑏𝑘𝑐𝑘 | 𝑘 = 𝑛+𝑚}.
Exercício 2.11 Usando o lema do bombeamento, mostre que as seguintes linguagens não são
livres de contexto:
∙ {𝑎𝑛𝑏𝑛𝑐𝑛 | 𝑛 ≥ 0};
∙ {0𝑛#02𝑛#03𝑛 | 𝑛 ≥ 0};
∙ {𝑤#𝑥 | 𝑤 é uma substring de 𝑥 para , 𝑥, 𝑤 ∈ {0, 1}*}.
Capítulo 3
Máquinas de Turing e Decidibilidade
Introduziremos agora uma máquina muito mais poderosa que as máquinas que vimos até agora:
as Máquinas de Turing. Uma máquina de Turing não tem mais restrições de memória,
podendo armazenar quantos símbolos ela quer e acessá-las como quiser. As máquinas de Turing
são um modelo muito mais próximo dos computadores que usamos podendo fazer tudo que um
computador faz. Contudo, veremos que existem problemas que máquinas de Turing não podem
resolver, por exemplo, não podem resolver o problema da parada. Esse resultado é muito
surpreendente pois impões limites aos computadores em geral.
Um máquina de Turing tem uma máquina de estados finita que pode acessar uma fita
infinita, conforme ilustrada no diagrama abaixo:
Maquina de
Estados
Fita
0 1 0 1 1 t t t · · ·
Cabeca da Maquina
Uma máquina de Turing pode realizar as seguintes operações:
1. Ler e escrever na fita;
2. Mover a cabeça da máquina para esquerda ou direita.
A string de entrada é escrita na fita da máquina de Turing e a cabeça da máquina começa
na posição mais a esquerda. Nós assumimos que a cabeça da máquina não pode ir mais a
esquerda que o início da fita. Ao ler o símbolo apontado pela cabeça da máquina, a máquina
pode mudar o seu estado, escrever e ler na fita e mover a cabeça da máquina.
46
A máquina de estados pode ter estados de aceitação e de rejeição. Quando a máquina
de Turing chega em um estado de aceitação ou rejeição a máquina para aceitando a string
de entrada. Isso não significa que a máquina deve necessariamente chegar em um estado de
aceitação e rejeição. Pode ser que a máquina nunca pare. Isso seria similar a um programa que
nunca termina, por exemplo, fica executando o mesmo loop de instruções.
Vejamos um exemplo de uma máquina de Turing que aceita strings da linguagem1:
{𝑤#𝑤 | 𝑤 ∈ {0, 1}*}
Como podemos checar se ums string pertence a esta linguagem? Bem podemos checar se o
primeiro símbolo da string antes de # é o mesmo que o primeiro símbolo da string vindo depois
de #. Em seguida repetimos esta operação com o segundo símbolo e se todos forem iguais
então a string pertence a esta linguagem.
A máquina de Turing irá fazer exatamente isso. Primeiro, ela ler o primeiro símbolo e o
marca como lido. Depois move a cabeça até o próximo símbolo não lido após o símbolo #,
e se for igual então o marca como lido. A seguinte sequência de operações deve tornar essa
estratégia clara:
0 1 0 1 1 # 0 1 0 1 1 t
x 1 0 1 1 # 0 1 0 1 1 t
x 1 0 1 1 # x 1 0 1 1 t
x 1 0 1 1 # x 1 0 1 1 t
x x 0 1 1 # x 1 0 1 1 t
x x 0 1 1 # x x 0 1 1 t
x x x x x # x x x x x t· · ·
1que não é livre de contexto.
A maquina de Turing aceita a string quando ela checa que a substring aparacendo antes de
# é igual a substring aparecendo depois de #. Os símbolos ⊔ representa um posição vazia da
fita, quer dizer, uma posição da fita não contendo nenhum símbolo.
A definição formal de máquinas de Turing é dada abaixo:
Definição 3.1. Um máquina de Turing é uma 7-tupla ⟨Q,Σ,Γ, 𝛿, 𝑞𝐼 , 𝑞𝐴, 𝑞𝑅⟩, onde
∙ Q é um conjunto finito de estado;
∙ Σ é o alfabeto de entrada;
∙ Γ é o alfabeto da fita, onde Σ ⊆ Γ;
∙ 𝛿 : Q× Σ −→ Q× Γ× {𝑒, 𝑑} é a função de transição;
∙ 𝑞𝐼 é o estado inicial;
∙ 𝑞𝐴 é o estado de aceitação;
∙ 𝑞𝑅 é o estado de rejeição, onde 𝑞𝑅 = 𝑞𝐴.
Perceba que o alfabeto de entrada (Σ) é um subconjunto do alfabeto de fita (Γ). Isso se
deve ao fato que a string de entrada é escrita na fita no começo da computação. Os demais
símbolos são vazios ⊔.Durante a computação a máquina de Turing pode ler e escrever na fita, especificado pela
função de transição 𝛿. Esta função lê o símbolo apontado pela cabeça da máquina e o estado
atual e retorna um símbolo a ser escrito na fita no lugar do símbolo da fita lido, um novo estado,
e finalmente move a cabeça da máquina para a esquerda, representado pelo símbolo 𝑒, ou para
a direita, representado pelo símbolo 𝑒.
Chamaremos de configuração de uma máquina de Turing o seu estado atual, o conteúdo da
fita e a posição da cabeça da máquina. A função de transição, conforme descrita acima, leva uma
máquina de uma configuração para outra. Nós usaremos a seguinte notação para representar
uma configuração no estado 𝑞 com string 𝑢𝑣 na fita e a cabeça da máquina apontando para o
primeiro símbolo de 𝑣:
𝑢 𝑞 𝑣
Mais exemplos de configurações são:
∙ 0 𝑞 1 1 0 𝑥 – o conteúdo da fita é 0 1 1 0 𝑥, o estado é 𝑞 e a cabeça da máquina está
apontando para o primeiro 1;
∙ 𝑞 0 1 1 0 𝑥 – novamente o conteúdo da fita é 0 1 1 0 𝑥 e o estado é 𝑞, mas a cabeça da
máquina está apontando para o começo da fita;
∙ 0 1 1 0 𝑥 𝑞 – novamente o conteúdo da fita é 0 1 1 0 𝑥 e o estado é 𝑞, mas a cabeça da
máquina está apontando para a posição a direita do último símbolo não vazio da fita.
Podemos agora formalizar o que é uma computação de uma máquina de Turing. Para isso
dizemos que uma máquina de Turing ⟨Q,Σ,Γ, 𝛿, 𝑞𝐼 , 𝑞𝐴, 𝑞𝑅⟩ leva
a configuração 𝑢 𝑎 𝑞𝑖 𝑏 𝑣 para a configuração em um passo 𝑢 𝑞𝑗 𝑎 𝑐 𝑣
se a a máquina de Turing tem a transição 𝛿(𝑞𝑖, 𝑏) = ⟨𝑞𝑗, 𝑐, 𝑒⟩ onde a cabeça é movida para a
esquerda. E esta máquina leva
a configuração 𝑢 𝑞𝑖 𝑎 𝑏 𝑣 para a configuração em um passo 𝑢 𝑐 𝑞𝑗 𝑏 𝑣
se a a máquina de Turing tem a transição 𝛿(𝑞𝑖, 𝑏) = ⟨𝑞𝑗, 𝑐, 𝑑⟩ onde a cabeça é movida para a
direita.
Dizemos agora que esta máquina de Turing aceita (respectivamente, rejeita) uma string de
entrada 𝑢 se existe uma sequência de configurações:
𝐶0 𝐶1 𝐶2 · · · 𝐶𝑛
onde 𝐶0 = 𝑞𝐼 𝑢 é a configuração inicial com a string de entrada 𝑢 escrita na fita, 𝐶𝑛 é uma
configuração de estado de aceitação (respectivamente, rejeição), e a máquina de Turing leva a
configuração 𝐶𝑖 para a configuração 𝐶𝑖+1 para todo 1 ≤ 𝑖 ≤ 𝑛− 1.
Definição 3.2. Dizemos que uma linguagem é recursivamente enumerável se existe uma
máquina de Turing que aceita todas as suas strings.
Existem três possíveis possibilidades para uma entrada ser processada por uma máquina de
Turing:
1. A máquina aceita a string o que significa que a string pertence a linguagem da máquina
de Turing;
2. A máquina rejeita a string o que significa que a string não pertence a linguagem da
máquina de Turing;
3. A máquina roda indefinidamente o que significa que a string não pertence a linguagem
da máquina de Turing.
Claramente, o terceiro caso não algo desejado, pois ao tentarmos decidir se uma string
pertence ou não a uma linguagem, pode ser que a máquina nunca nos retorne que que ela
aceita ou rejeita esta string, mas simplesmente fique processando indefinidamente.
Estaremos portanto interessados em saber que linguagens existe uma máquina de Turing
que decida se uma string pertence ou não a linguagem da máquina. Chamamos estas linguagens
de recursivas ou linguagens decidíveis.
Definição 3.3. Dizemos que uma linguagem 𝐿 é recursiva ou decidível se existe uma máquina
de Turing que aceita todas as suas strings e rejeita todas as strings que não pertencem a 𝐿.
Vejamos alguns de máquinas de Turing.
Exemplo 3.4. Vamos formalizar a máquina de Turing que descrevemos acima que aceita a
linguagem:
{𝑤#𝑤 | 𝑤 ∈ {0, 1}*}
A máquina terá 16 estados, Q = {𝑞1, 𝑞2, . . . , 𝑞14, 𝑞𝐴, 𝑞𝑅}, conforme ilustrado pelo diagrama
abaixo:
q3
q1
q2
0 → x, d
qAt → d
0, 1 → d
1 → x, d
q4# → d
q6
x → d
0 → x, e
0, 1, x → e
0, 1 → d
q5
x → d
q8# → d
x → R
# → d1 → x, e
q7
# → ex → d
0, 1 → e
Neste diagrama não mostramos as transições e os estados necessários para a máquina re-
jeitar uma string. Usamos a seguinte notação para representar transições:
∙ 𝑎 → 𝑏, 𝑑𝑟 para representar a transição quando a cabeça da máquina lê o símbolo 𝑎, escreve
o símbolo 𝑏 e move para a direção 𝑑𝑟 ∈ {𝑒, 𝑑};
∙ 𝑎 → 𝑑𝑟 para representar a transição quando a cabeça da máquina lê o símbolo 𝑎, não
escreve na fita e move para a direção 𝑑𝑟 ∈ {𝑒, 𝑑}.
Por exemplo, a máquina no estado 𝑞1 verifica o símbolo na primeira substring, a que antecede
#: Se for 0 a máquina move para o estado 𝑞2; se for 1 a máquina move para o estado 𝑞3; se
for #, significa que a máquina já leu todas os símbolos da primeira substring, e ela move para
o estado 𝑞8. No estado 𝑞2 a máquina move a cabeça da máquina para a direita até que encontre
o marcador #. No estado 𝑞4, a máquina procura pelo primeiro símbolo diferente de 𝑥. Se este
for 0, quer dizer igual ao símbolo na primeira substring, ela o substitui por 𝑥 signalizando que
este símbolo foi checado com o correspondente da primeira substring. Em seguida a máquina no
estado 𝑞6 move a cabeça para a esquerda até encontrar o marcador #. No estado 𝑞7, a máquina
procura pelo símbolo 𝑥 aparecendo mais a direita da primeira substring.
Os estados 𝑞3, 𝑞5 são similares, mas verificam se o símbolo correspondente é 1.
Finalmente, no estado 𝑞8 a máquina verifica que não há mais símbolos 0 ou 1 para serem
processados na segunda substring.
Exemplo 3.5. Vamos descrever uma máquina de Turing que aceita a seguinte linguagem:
{02𝑛 | 𝑛 ∈ N}
Quer dizer strings formadas por 2𝑛 0s onde 𝑛 é um número natural qualquer. A ideia será de
passar pela fita e apagar cada segundo 0 visto. Se após vários loops a fita tiver somente um 0s,
então aceite a string, caso a máquina tiver visto um número ímpar diferente de um de símbolos
0s, então rejeite.
O diagrama desta máquina de Turing é mostrado abaixo:
q1
qA
0 → x, d
q5
q3
x → e
q4
0 → t, d q2
x → dx → d
0 → e
t → d
t → e
0 → d0 → x, d
x → d
t → d
qR
t → d
t → d
x → d
A máquina começa escrevendo ⊔ no primeiro 0 para que ela saiba onde a string de entrada
começa. No estado 𝑞2, a máquina procura pela próximo (o segundo) 0 e o substitui por 𝑥. Caso
não haja mais 0s a serem marcados a máquina aceita.
No estado 𝑞3, a máquina está esperando pelo próximo 0, o qual não deve ser substituído
por 𝑥. Caso não haja nenhum 0 restante, a máquina deve encontrar um símbolo em branco
⊔. Neste caso a máquina migra para o estado 𝑞5 que simplesmente move a cabeça da fita para
a esquerda até o começo da fita. Quando isso acontecer a máquina volta para o estado 𝑞2. O
loop de transições entre 𝑞3 e 𝑞4 descrevem o processo de marcar todo segundo 0: no estado
𝑞4 a máquina viu um número ímpar e diferente de 1 de símbolos 0s, enquanto no estado 𝑞3 a
máquina viu um número par de 0s. Caso no estado 𝑞4 e a entrada não tem nenhum 0, então a
máquina falha.
Tente executar a máquina com a entrada 0000 e se convença que a máquina realmente
funciona como descrito informalmente acima.
Exemplo 3.6. Considere a linguagem abaixo:
{0𝑖1𝑗2𝑘 | 𝑖× 𝑗 = 𝑘, 𝑖, 𝑗, 𝑘 ≥ 1}
Esta linguagem pode representar o problema de determinar se a igualdade é verdadeira 𝑖×𝑗 = 𝑘
para alguns números 𝑖, 𝑗, 𝑘 ≥ 1. A máquina de Turing que aceita esta linguagem realiza os
seguintes passos:
1. Verifica se a entrada é da forma: uma sequência de um ou mais 0s, seguido de uma
sequência de um ou mais 1s, seguido de uma sequência de um ou mais 2s. Caso a
entrada não seja desta forma, a máquina rejeita a entrada.
2. Apaga um 0 e depois move a cabeça da máquina para o primeiro 1 que aparecer na
fita. Apaga um número de 2s igual ao número de 1s na fita. Para isso a máquina
vai precisar percorrer (possivelmente muitas vezes) a fita. Se não houver 2s suficientes
rejeite a entrada.
3. Repita o passo anterior até que todos os 0s estiverem marcados. Se não sobrarem 2s,
aceite a entrada, caso contrário rejeite a entrada.
Não é difícil construir uma máquina que realiza essa linguagem, apesar de requerer um pouco
de tempo. Tente implementar essa máquina e aplica a sua solução na entrada: 00111222222.
3.1 Máquinas de Turing Não Determinísticas
Assim como autômatos, podemos construir máquinas de Turing Não Determinísticas. O que
muda não definição de máquinas de Turing (Definição 3.1) é a função de transição:
𝛿 : Q× Γ → 𝒫(Q× Γ× {𝑒, 𝑑})
Em particular, a função de transição especifica que uma máquina pode migrar de um estado
e de símbolo da fita para zero ou mais estados, escrevendo na fita e movendo a cabeça da fita
diferentemente.
A computação de uma máquina de Turing não determinística é parecida com as execuções
de um AFND. Existem múltiplas máquinas executando em paralelo e se uma delas chegar em
um estado de aceitação então a máquina aceita a entrada.
Podemos provar o seguinte teorema afirmando que o poder de expressão de uma máquina
não determinística é o mesmo que o poder de expressão de uma máquina determinística.
Teorema 3.7. Toda máquina não determinística tem uma máquina determinística equivalente.
Demonstração. Não iremos detalhar a prova somente daremos a sua intuição. Para mais deta-
lhes veja o a seção 4.2 do livro do Sipser.
Dada uma máquina não determinística, podemos construir uma máquina determinísitca que
a simula. A dificuldade é de simular todas as possíveis máquinas paralelas da máquina não
determinística. Contudo, podemos o fazer usando uma busca em largura: dada uma entrada
𝑤 = 𝑠1𝑠2𝑠3 · · · 𝑠𝑛 escrita na fita:
1. Seja 𝑖 := 0
2. Repita:
3. Calcule o resultado de todas as excuções da máquina não determinística com 𝑖 passos
usando a entrada;
4. Caso alguma delas chegue no estado de aceitação, então aceite.
5. Caso contrário, incremente 𝑖.
Em cada iteração do loop acima, a máquina deve começar recomeçar do estado inicial e
re-calcular o processamento de todas as execuções. Para isso a máquina terá que lembrar a
string de entrada e em que posição da árvore de execuções da máquina não determinística a
máquina determinística está.
Como estamos usando uma busca por largura, o procedimento irá terminar caso a máquina
não determinística aceite a string. Contudo, ela irá rodar indefinidamente caso contrário.
3.2 Tese de Church-Turing
Nos anos do século XX, David Hilbert propos 23 problemas que ele achava que importantes
de serem resolvidos. Esta lista de problemas teve um impacto muito grande na comumidade
matemática guiando muitos na sua pesquisa. O problema número 10 dizia o seguinte:
Tem como desenvolver um método que para um dado um polinômio
𝑝(𝑥1, 𝑥2, . . . , 𝑥𝑛) qualquer consegue determinar se este polinômio tem uma raiz
inteira em um número finito de ações.
Em outras palavras, Hilbert queria era saber se existe um algoritmo que dado um polinômio
qualquer checa se este polinômio tem uma raiz inteira. Para resolver este problema, ma-
temáticos formularam muitos modelos matemáticos muitos deles parecidos com os modelos
computacionais que vimos até agora. Outros exemplos de teorias foram:
∙ Cáculo 𝜆 – Este sistema foi proposto por Alonzo Church e teve um impacto muito grande
na ciência da computação, em particular, na teoria de linguagens de programação. A
maioria das linguagens de programação tem conceitos que se originaram do cálculo 𝜆.2
∙ Máquinas de Turing – As máquinas de Turing foram também motivadas pelo programa de
Hilbert. Turing pensou que as suas máquinas poderiam dar originem a uma máquina que
poderia resolver qualquer problema matemático. Infelizmente, como veremos na próxima
seção, este não foi o caso;
∙ Funções 𝜇-recursiva parciais. Este modelo proposto por talvez o mais importante lógico do
século XX, Kurt Gödel, e também teve um impacto imporante na ciência da computação.
Computação neste modelo é representada por uma função parcial, quer dizer, nem todo
elemento do domínio tem o seu correspondente na imagem, augmentada por uma operação
recursiva 𝜇. Assim ao invés de descrever os detalhes dos passos da computação como
escritas e leituras na fita de um máquina de Turing, podemos usar funções. Este modelo
deu origem a modelos denotacionais que são usadas na verificação de compiladores por
exemplo.
Interessantemente, foi provado que todos esses modelos (e outros mais) são equivalentes,
no sentido, que qualquer problema cuja solução pode ser descrita em um modelo pode ser
descrita no outro modelo. Isso levou Church e Turing pensar que todos os algoritmos podem
ser expressos usando máquinas de Turing. Quer dizer, existe a equivalência entre a noção
intuitiva de algoritmos e máquinas de Turing:
Noção intuitiva de algoritmos ⇔ Máquinas de Turing
Esta é a tese de Church-Turing. Ela é uma tese e não um teorema porque ainda não se pode
provar que todos os algoritmos podem ser expressos por máquinas de Turing. Podem haver
algoritmos que ainda não descobrimos que não podem ser expressos como máquinas de Turing.
Contudo até agora todos os algoritmos que conhecemos podem ser expressos como máquinas de
Turing. Uma área de pesquisa que pode mudar esta visão é a computação quântica que pode
proporcionar um modelo de computação mais poderoso que o de máquinas de Turing.
Na verdade, a nossa intenção desde o início com o estudo de máquinas de Turing era
responder a questão:2Para aqueles mais interessados, recomendo a leitura do artigo de Peter Landin The next 800 programming
languages. e o livro de Benjamin Peirce Types and Programming Languages.
O que é um algoritmo?
De acordo com a Tese Church-Turing, um algoritmo é qualquer procedimento que pode ser
descrito por uma máquina de Turing. De fato, agora que temos um bom entendimento das
máquinas de Turing, não estaremos mais interessados nos detalhes da sua execução. Poderemos
descrever algoritmos usando uma linguagem mais alto nível como é normalmente feito em aulas
introdutórias de computação. Mas devemos sempre lembrar que estas descrições tem informação
suficiente para extrair a máquina de Turing que corresponde ao algoritmo descrito.
Exercícios
Exercício 3.1 Desenhe máquinas de Turing que aceite seguintes linguagens:
∙ {𝑎𝑛𝑏𝑛𝑐𝑛 | 𝑛 ≥ 1};
∙ {𝑤𝑤𝑅 | 𝑤 ∈ Σ*};
∙ {(𝑎𝑏)𝑛 | 𝑛 ≥ 1};
∙ {𝑤 | 𝑤 contém o mesmo número de 0s e 1s}.
Exercício 3.2 Dê uma descrição alto nível de como uma máquina de Turing reconheceria a
linguagem: {𝑎𝑛𝑏2𝑛 | 𝑛 ≥ 1}.
Exercício 3.3 Desenhe uma máquina de Turing que dado uma entrada um número 𝑁 em
binário, chegue no estado de aceitação de tal forma que a fita contém exatamente o número
𝑁 + 1 em binário.
Exercício 3.4 Desenhe uma máquina de Turing que dada uma entrada na linguagem {0, 1}*
que troque todos os 0s aparecendo na entrada por 1, deixando o resto inalterado, e pare no
estado de aceitação.
Exercício 3.5 Imagine uma máquina de Turing bi-dimensional. Portanto, ao invés de uma
máquina com uma fita linear infinita, a máquina tem um quadrante infinito onde a cabeça
começa na posição (0,0). Após ler uma entrada a máquina pode mover a cabeça para esquerda,
direita, cima ou baixa contanto que não saia do quadrante. Prove que esta máquina não é mais
expressiva que a máquina de Turing com a fita linear. Para isso mostre que uma máquina com
fita bi-dimensional qualquer pode ser simulada por uma máquina de Turing com fita linear.
Exercício 3.6 Mostre que podemos assumir que a máquina termina com a fita vazia. Quer
dizer, mostre como converter uma máquina de Turing qualquer 𝑀 em uma máquina 𝑀 ′, tal
que 𝐿(𝑀) = 𝐿(𝑀 ′) onde a máquina 𝑀 ′ só aceita ou rejeita se a fita estiver vazia.
Capítulo 4
Indecidibilidade e Redutibilidade
Voltemos ao décimo problema de Hilbert descrito na página 54. Este problema foi tópico de
intensa pesquisa de vários brilhantes matemáticos da época. A esperança era poder descrever
um formalismo que pudesse checar em um número finito de ações se um polinômio tem raízes
inteiras. Infelizmente, este problema é indecidível, quer dizer, não há nenhum algoritmo que
possa resolver este problema. De fato, Hilbert nos anos trinta propôs mais um desafio para a
comumidade o problema de decidibilidade, ou em alemão: Entscheidungsproblem.
Existe um algoritmo que dada uma sentença de primeira ordem determina se esta
sentença é uma tautologia?
Pela Tese de Church-Turing, usamos como noção de algoritmo máquinas de Turing. Alan
Turing e Alonzo Church provaram que não é possível existir tal algoritmo, ou seja não existe
uma máquina de Turing que resolva o problema acima. Este resultado teve impactos muito
grandes na ciência da computação, pois formalmente definiu um limite para o o que pode
ser calculado e o que não pode ser calculado. De fato, estaremos interessados nas seguintes
perguntas:
Quais problemas existem um algoritmo que o resolve?
Qual o limite da computação?
Além destas perguntas serem de grande interesse científico já que questionam os limites da
computação, elas tem um valor prático muito grande. Muitos problemas que gostaríamos de
resolver na prática acabam sendo impossíveis de resolver em geral. Por exemplo seria muito
útil se pudéssemos:
1. verificar automaticamente se um dado algoritmo (ou subrotina) não termina, quer dizer,
fica rodando indefinidamente para uma certa entrada. Como os códigos e algoritmos
58
sendo mais e mais complexos, muitos erros de programação poderiam ser detectados por
tal procedimento.
2. verificar se um programa realmente executa o que foi especificado. Este é o problema de
verificação de programas. Infelizmente este problema é em geral indecidível, quer dizer,
não se pode determinar se um programa realmente implementa o que foi especificado a
fazer.
3. verificar se existe um sub-algoritmo (ou subrotina) não tem utilidade, quer dizer pode ser
apagado sem afetar a execução global do algoritmo. Com este procedimento, poderíamos
eliminar tais subrotina simplificando o algoritmo.
Infelizmente tais problemas são impossíveis de resolver.1
Iremos primeiro investigar alguns problemas para os quais existe uma máquina de Turing
que o resolve, ou seja são decidíveis. Em seguida iremos provar a existência de problemas para
os quais não existe nenhuma máquina de Turing que possa o resolver, ou seja são indecidíveis.
4.1 Exemplos de Linguagens Decidíveis
Iremos provar que para decidir se uma string pertence a uma linguagem regular ou a uma
linguagem livre de contexto é decidível.
Comecemos com a linguagem abaixo:
𝐿𝐴𝐹𝐷 = {⟨𝑀,𝑤⟩ | 𝑀 é um AFD que aceita 𝑤}
𝑀 é uma representação usando strings de um AFD. Uma string ⟨𝑀,𝑤⟩ pertence à 𝐿𝐴𝐹𝐷 se a
máquina representada por 𝑀 aceita a string 𝑤. Verifiquemos que esta linguagem é decidível.
Teorema 4.1. 𝐿𝐴𝐹𝐷 é uma linguagem decidível.
Demonstração. Para provar este teorema, precisamos construir uma máquina de Turing que
aceita uma string ⟨𝑀,𝑤⟩ se 𝑀 é uma AFD e aceita 𝑤. A máquina de Turing executa os
seguintes passos:
1. Verifica se de fato 𝑀 é um AFD, caso não seja rejeite;
2. Simula a execução de 𝑀 usando a entrada 𝑤;1Para muitas instância é possível verificar se um algoritmo termina ou não tem utilidade usando análise
estática de programas. A construção de técnicas de análise de algoritmos é um tópico de muita pesquisa.
3. Caso𝑀 aceita a string 𝑤 a máquina de Turing entra no estado de aceitação, caso contrário
rejeita.
É fácil verificar que os passos acima podem ser executados por uma máquina de Turing.
Para isso ela precisa lembrar o estado em que 𝑀 está, e precisa checar as transições da string
representando 𝑀 .
Considere a seguinte linguagem que considera linguagens não determinísticas:
𝐿𝐴𝐹𝑁𝐷 = {⟨𝑀,𝑤⟩ | 𝑀 é um AFND que aceita 𝑤}
Esta linguagem também é decidível conforme afirma o teorema abaixo:
Teorema 4.2. 𝐿𝐴𝐹𝑁𝐷 é uma linguagem decidível.
Demonstração. Para provar este teorema, precisamos construir uma máquina de Turing que
para uma entrada ⟨𝑀,𝑤⟩ aceita quando o AFND 𝑀 aceita 𝑤 e rejeita caso contrário. A
máquina de Turing executa os seguintes passos:
1. Verifica se 𝑀 é um AFND, caso não seja rejeite;
2. Converta a máquina 𝑀 em um AFD equivalente usando a construção descrita na prova
do Teorema 2.6;
3. Use a máquina de Turing descrita na prova do Teorema 4.1 como subrotina, retornando
o mesmo resultado.
Perceba que a construção do AFD a partir do AFND pode ser realizada por uma máquina
de Turing em um número finito de passos.
Da mesma forma podemos provar que a seguinte linguagem é decidível:
𝐿𝐸𝑋𝑅𝐸𝐺 = {⟨𝑅,𝑤⟩ | 𝑅 é uma expressão regular que gera 𝑤}
Não mostraremos a prova.
Teorema 4.3. 𝐿𝐸𝑋𝑅𝐸𝐺 é uma linguagem decidível.
Agora tentemos algo um pouco diferente. Considere a linguagem:
𝐸𝐴𝐹𝐷 = {𝑀 | 𝑀 é um AFD que não aceita nenhuma string, quer dizer 𝐿(𝑀) = ∅}
Este teste, chamado de teste de vázio, é útil pois podemos determinar se um AFD não tem
utilidade, já que não aceita nenhuma string. Esta linguagem também é decidível, como afirma
o teorema abaixo:
Teorema 4.4. 𝐸𝐴𝐹𝐷 é uma linguagem decidível.
Demonstração. Para provar este teorema, precisamos construir uma máquina de Turing que
aceite quando dado um AFD 𝑀 que não aceita nenhuma string. Podemos mostrar que um
AFD 𝑀 não aceita nenhuma string se e somente se não existe um caminho entre o seu estado
inicial e um estado final. Pois caso exista um caminha, então a máquina 𝑀 aceita pelo menos
uma string. Portanto, iremos construir uma máquina de Turing que checa quais os estados que
podem ser alcançados do estado inicial. Caso o estado final seja um deles, então rejeitamos,
caso contrário aceitamos:
1. Marque o estado inicial de 𝑀 ;
2. Repita até que nenhum novo estado seja marcado:
(a) Marque qualquer estado que tenha uma transição de um estado marcado;
3. Se nenhum estado final estiver marcado, aceite, caso contrário rejeite.
É fácil perceber que todas as operações descritas podem ser realizadas por uma máquina de
Turing. Em particular, o passo de marcar novos nós, a máquina precisa ficar checando todas
as transições para todos os nós que ainda não estão marcados.
Finalmente, considere a seguinte linguagem:
𝐸𝑞𝐴𝐹𝐷 = {⟨𝑀1,𝑀2⟩ | 𝑀1,𝑀2 são AFDs tal que 𝐿(𝑀1) = 𝐿(𝑀2)}
Esta linguagem é interessante pois caso seja decidível podemos substituir um AFD mais com-
plexo por um AFD equivalente menos complexo, por exemplo.2 Esta linguagem também é
decidível.
Teorema 4.5. 𝐸𝑄𝐴𝐹𝐷 é uma linguagem decidível.
Demonstração. Para provar este teorema, usaremos o Teorema 4.4. Dadas as linguagens 𝐿1 e
𝐿2 aceitas pelos AFD 𝑀1 e 𝑀2, respectivamente, iremos verificar se o seguinte conjunto é vazio,
onde 𝐿 é o complemento de 𝐿:
𝐿3 = (𝐿1 ∪ 𝐿2) ∪ (𝐿1 ∪ 𝐿2)
A primeira parte da união é o conjunto de elementos que pertencem a 𝐿1, mas não pertencem a
𝐿2, enquanto a segunda parte é o conjunto simétrico, isto é, os elementos que pertencem a 𝐿2,2De fato, existe um método que dado um AFD, encontra o AFD com o menor número de estados equivalente.
mas não pertencem a 𝐿1. Se este conjunto é vazio, 𝐿3 = ∅), então 𝐿1 = 𝐿2. (Tente desenhar o
diagrama do conjunto 𝐿3.)
Usaremos o Teorema 4.4. Construiremos o AFD 𝑀3 cuja linguagem é 𝐿3 e em seguida
verificaremos usando a máquina de Turing construída na prova do Teorema 4.4:
1. Construa a máquina 𝑀3 cuja linguagem é 𝐿3;
2. Use a máquina de Turing construída na prova do Teorema 4.4 com entrada 𝑀3;
3. Se esta máquina de Turing aceitar, então aceite, caso contrário rejeite.
É fácil perceber que todas as operações descritas podem ser realizadas por uma máquina de
Turing. Em particular, o passo de construir o AFD 𝑀3 pode ser realizado por uma máquina
de Turing do mesmo que fizemos com a união. (Veja o Exercício 7 na página 44.)
4.2 O Problema da Parada
Iremos descrever um problema que é indecidível o problema da parada. Este problema teve
impactos importantes para a matemática, filosofia e para a ciência da computação. Alan Turing
provou que não existe um algoritmo que consiga determinar se uma máquina de Turing qualquer
sempre para com uma resposta, ou pode rodar indefinidamente.
Isto significa que computadores tem limites e podemos determinar estes limites formalmente.
De fato, vários problemas que gostaríamos de resolver acabam sendo indecidíveis.3 Por exemplo,
dado um program de computador e uma especificação precisa do que o programa deve e não
deve fazer, gostaríamos de poder determinar se o dado programa corresponde a especificação.
Infelizmente o problema de verificação de programas cai normalmente no conjunto de problemas
indecidíveis. De fato, a área de verificação formal de programas é uma área bastante ativa de
pesquisa e um dos tópicos envolvidos é de determinar se existem sub-conjunto de problemas de
verificação que são decidíveis.
Provaremos que a seguinte linguagem é indecidível:
𝐿𝑀𝑇 = {⟨𝑀,𝑤⟩ | 𝑀 é uma máquina de Turing que aceita a string 𝑤}
O grande problema desta linguagem é que ummáquina de Turing𝑀 pode rodar indefinidamente
para uma entrada 𝑤. Portanto, para a linguagem 𝐿𝑀𝑇 ser decidível devemos construir uma3Veja uma lista de problemas indecidíveis no link http://en.wikipedia.org/wiki/List_of_undecidable_
problems.
máquina de Turing que determine se a entrada 𝑀 irá ou não rodar indefinidamente com a
entrada 𝑤. Por isso que o problema acima é também chamado de Problema da Parada. Veremos
que não é possível em geral determinar se uma máquina de Turing sempre pára.
Teorema 4.6. 𝐿𝑀𝑇 não é decidível.
Para provar este teoremo, contudo, precisaremos revisar uma técnica de prova chamada
método da Diagonalização introduzida por Georg Cantor. Este método foi usado para estabe-
lecer resultados não triviais sobre conjuntos infinitos. Em particular, Cantor estava interessado
em medir o tamanho de conjuntos infinitos. Com tais conjuntos, simplesmente contar os seus
elementos não funciona pois conjuntos infinitos tem, bem, um número infinito de elementos.
Ao invés de contar o número de elementos de um conjunto infinito, Cantor propôs a com-
paração entre os números de elementos entre conjuntos infinitos: sejam A e B dois conjuntos
infinitos. Dizemos que eles tem o mesmo número de elementos se existe uma bijeção entre A
e B. Por exemplo, existe um bijeção entre os números naturais N e os números pares, P, pois
existe a bijeção:
𝑓 : N → P
definida como 𝑓(𝑛) = 2𝑛. É fácil verificar que 𝑓 é uma bijeção. Ela é injetiva, pois para cada
𝑛 ∈ N, 𝑓(𝑛) tem somente um resultado. A função é surjetiva pois para cada 𝑝 ∈ P existe um
número 𝑛 ∈ N, tal que 𝑓(𝑛) = 𝑝, o número 𝑝/2.
Os números naturais representam de fato um conjunto especial de números infinitos. Todos
os conjuntos infinitos que tenham uma bijeção com os números naturais são chamados de
contáveis.
Definição 4.7. Um conjunto é contável se ele é finito ou que tenha uma bijeção com o número
naturais.
De fato, a grande maioria de conjuntos que tratamos na ciência da computação são contáveis,
por exemplo, o conjunto de todas as listas finitas, conjunto de todas as árvores finitas, conjunto
de todas as strings de tamanho finito.
A grande descoberta de Cantor é a existência de conjuntos que não são contáveis. Can-
tor descobriu que o conjunto de números reais R não é contável: existem mais números reais
que números naturais. De fato, existem mais números reais no intervalo entre 0 e 1 que nú-
meros naturais. Para provar esse resultado bastante não intuitivo, Cantor usou o método da
diagonalização.
Teorema 4.8. O conjunto dos números reais R não é contável.
Demonstração. Assuma por contradição (veja o capítulo 1.2 para ver como são provas por con-
tradição), que o conjunto dos números reais entre 0 e 1, escrito ]0, 1[, é contável e mostraremos
que isso leva a uma contradição. Podemos deduzir então que ]0, 1[ não é contável. Como os
números reais contém o intervalo entre 0 e 1, então o conjunto dos números reais também não
é contável.
Mostremos a prova por contradição. Assumindo que ]0, 1[ é contável, então por definição
existe uma bijeção 𝑓 : N →]0, 1[. Podemos portanto colocar os números reais do conjunto ]0, 1[
em uma ordem.
N 𝑓(𝑛)
0 0.𝑎11𝑎12𝑎13𝑎14 · · ·1 0.𝑎21𝑎22𝑎23𝑎24 · · ·2 0.𝑎31𝑎32𝑎33𝑎34 · · ·3 0.𝑎41𝑎42𝑎43𝑎44 · · ·...
...
onde 𝑎𝑖𝑗 ∈ {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} é um dígito. Nós mostraremos que existe um número no
conjunto ]0, 1[ que não pode estar na lista de elementos acima. Para isso olhemos para a diagonal
(por isso método de diagonalização): 𝑎11, 𝑎22, 𝑎33, 𝑎44, . . .. Podemos construir o número
𝑥 = 0.𝑏1𝑏2𝑏3𝑏4 · · ·
tal que 𝑏𝑖 = 𝑏𝑖𝑖 para todo 𝑖 ∈ N. Claramente 𝑥 pertence ao intervalo ]0, 1[. Porém, 𝑥 não pode
estar na sequência de números acima: caso esteja na posição 𝑗, o algarismo 𝑏𝑗 deve ser diferente
de 𝑎𝑗𝑗, o que é uma contradição, pois para o número 𝑥 estar na posição 𝑗, o algarismo 𝑏𝑗 deve
ser igual a 𝑎𝑗𝑗, mas da forma que construirmos 𝑥 o algarismo 𝑏𝑗 é diferente de 𝑎𝑗𝑗. Quer dizer
temos ao mesmo tempo 𝑏𝑗 = 𝑎𝑗𝑗 e 𝑏𝑗 = 𝑎𝑗𝑗, o que é uma contradição.
Portanto não pode haver a função 𝑓 : N →]0, 1[ que seja uma bijeção e assim provamos que
o conjunto de números reais não é contável.
Como a prova do teorema acima pode demonstrar, o método da diagonalização é bastante
poderoso. Ele permite provar a inexistência de algum objeto, no caso acima de uma função
bijetora. (Compare com a prova da existência de um objeto, por exemplo, uma máquina de
Turing nos teoremas da Seção 4.1.) Usaremos a mesma técnica para mostrar a inexistência de
uma máquina de Turing que decida a linguagem 𝐿𝑀𝑇 .
4.3 Prova da Indecidibilidade do Problema da Parada –
Teorema 4.6
A prova será por contradição. Assuma que exista uma máquina de Turing, 𝑀𝐷, que decida a
linguagem:
𝐿𝑀𝑇 = {⟨𝑀,𝑤⟩ | 𝑀 é uma máquina de Turing que aceita a string 𝑤}
Quer dizer, 𝑀𝐷 aceita uma outra máquina 𝑀 e uma entrada 𝑤 e:
∙ Aceita se a máquina 𝑀 aceita 𝑤;
∙ Rejeita se a máquina 𝑀 rejeita 𝑤 ou roda indefinidamente.
Iremos agora construir uma nova máquina 𝑀𝐼 que usa a máquina 𝑀𝐷 como subrotina. A
máquina 𝑀𝐼 recebe uma máquina 𝑀 e chama 𝑀𝐷 na entrada ⟨𝑀, ⟨𝑀⟩⟩, onde ⟨𝑀⟩ é a descrição
em forma de string da máquina 𝑀 . Caso 𝑀𝐷(⟨𝑀, ⟨𝑀⟩⟩) resultar em aceita, então 𝑀𝐼 rejeita,
caso contrário, aceita. Mais formalmente a máquina 𝑀𝐼 é especificada como abaixo para uma
dada entrada ⟨𝑀⟩:
1. Roda a máquina 𝑀𝐷(⟨𝑀⟩);
2. Caso o resultado de 𝑀𝐷(⟨𝑀⟩) for “aceita”, então “rejeite”;
3. Caso o resultado de 𝑀𝐷(⟨𝑀⟩) for “rejeita”, então “aceite”;
O leitor pode achar um pouco estranho que estamos usando a descrição da máquina 𝑀 como
a string de entrada. O mesmo ocorre quando compilamos um programa: o compilador recebe
a descrição de uma máquina e o transforma em um programa equivalente em linguagem de
máquina.
Abrindo a definição da máquina 𝑀𝐷 podemos descrever a saída da máquina 𝑀𝐼 da seguinte
forma:
𝑀𝐼(⟨𝑀⟩) =
⎧⎪⎨⎪⎩rejeita se 𝑀 aceita ⟨𝑀⟩
aceita se 𝑀 rejeita ⟨𝑀⟩ ou roda indefinidamente
Agora chegaremos a uma contradição quando aplicamos a máquina 𝑀𝐼 usando como entrada
a máquina 𝑀𝐼 . Vejamos o que acontece:
𝑀𝐼(⟨𝑀𝐼⟩) =
⎧⎪⎨⎪⎩rejeita se 𝑀𝐼 aceita ⟨𝑀𝐼⟩
aceita se 𝑀𝐼 rejeita ⟨𝑀𝐼⟩ ou roda indefinidamente
O que significa que 𝑀𝐼 aceita ⟨𝑀𝐼⟩ se 𝑀𝐼 rejeita ⟨𝑀𝐼⟩ e 𝑀𝐼 rejeita ⟨𝑀𝐼⟩ se 𝑀𝐼 aceita ⟨𝑀𝐼⟩!Isso é uma contradição. Portanto a máquina 𝑀𝐷 não pode existir. Como 𝑀𝐼 é definida usando
𝑀𝐷, a máquina 𝑀𝐼 também não pode existir. O que significa que não existe uma máquina que
decida a linguagem 𝐿𝑀𝑇 .
O leitor pode-se perguntar onde o argumento da diagonalização foi usado na prova da
indecidibilidade do problema da parada. Considere a tabela abaixo com as execuções de
uma máquina 𝑀𝑖 usando como entrada uma descrição de uma máquina ⟨𝑀𝑗⟩ onde os
símbolos “?” representam que a máquina ou rejeita a entrada ou roda indefinidamente:
⟨𝑀1⟩ ⟨𝑀2⟩ ⟨𝑀3⟩ ⟨𝑀4⟩ · · ·𝑀1 ? aceita ? aceita · · ·𝑀2 aceita aceita ? ? · · ·𝑀3 ? ? aceita aceita · · ·𝑀4 aceita ? aceita ? · · ·...
......
...... · · ·
Rodando a máquina 𝑀𝐷 na lista de máquinas na tabela acima, obtemos uma nova
tabela onde o símbolo “?” é substituído por “rejeita”:
⟨𝑀1⟩ ⟨𝑀2⟩ ⟨𝑀3⟩ ⟨𝑀4⟩ · · ·𝑀1 rejeita aceita rejeita aceita · · ·𝑀2 aceita aceita rejeita rejeita · · ·𝑀3 rejeita rejeita aceita aceita · · ·𝑀4 aceita rejeita aceita rejeita · · ·...
......
...... · · ·
Agora podemos aplicar o método da diagonalização. Construirmos uma máquina,𝑀𝐼 que faz o oposto da diagonal da tabela acima, quer dizer:
⟨𝑀2⟩ ⟨𝑀3⟩ ⟨𝑀4⟩ · · ·𝑀𝐼 aceita rejeita rejeita aceita · · ·
Como 𝑀𝐼 é uma máquina de Turing, ela deve aparecer na lista de máquinas acima.
Dica 4.5
⟨𝑀1⟩ ⟨𝑀2⟩ ⟨𝑀3⟩ ⟨𝑀4⟩ · · · ⟨𝑀𝐼⟩ · · ·𝑀1 rejeita aceita rejeita aceita · · · aceita · · ·𝑀2 aceita aceita rejeita rejeita · · · rejeita · · ·𝑀3 rejeita rejeita aceita aceita · · · aceita · · ·𝑀4 aceita rejeita aceita rejeita · · · aceita · · ·...
......
...... · · · ... · · ·
𝑀𝐼 rejeita rejeita aceita aceita · · · ? · · ·...
......
...... · · · ... · · ·
Mas é impossível que 𝑀𝐼 esteja na lista acima, pois não temos como definir o valor
marcado “?”, gerando uma contradição.
4.4 Linguagens Não Recursivamente Enumeráveis
Agora provaremos que existem linguagens não são recursivamente enumeráveis. Isto é, lingua-
gens para as quais não existe nenhuma máquina de Turing que aceite strings pertencentes a
linguagens e rejeite ou rode indefinidamente para strings que não pertencem a esta linguagem.
Perceba que a linguagem 𝐿𝑀𝑇 do problema da parada é recursivamente enumerável, apesar de
não ser decidível.
Para isso, provaremos o seguinte teorema.
Teorema 4.9. Uma linguagem 𝐿 é decidível se e somente se 𝐿 e o seu complemento 𝐿 são
recursivamente enumeráveis.
Demonstração. Se uma linguagem 𝐿 é decidível, então é fácil verificar que o seu complemento
também é decidível.
Mais difícil é a prova da direção oposta: se temos que 𝐿 e o seu complemento 𝐿 são
recursivamente enumeráveis, devemos provar que 𝐿 é decidível. Como 𝐿 e 𝐿 são recursivamente
enumeráveis, existem duas máquinas 𝑀 e 𝑀𝐶 que aceitam strings de 𝐿 e de 𝐿, respectivamente.
Para decidir a linguagem 𝐿 iremos construir uma máquina 𝑀𝐿 que roda 𝑀 e 𝑀𝐶 em paralelo.
Para uma dada entrada 𝑤, a máquina 𝑀𝐿 executa os seguintes passos:
1. Roda em paralelo as máquinas 𝑀 e 𝑀𝐶 com a entrada 𝑤;
2. Caso 𝑀 aceite 𝑤, então aceite a entrada 𝑤;
3. Caso 𝑀𝐶 aceite 𝑤, então rejeite a entrada 𝑤.
Perceba que a máquina 𝑀𝐿 não roda indefinidamente, pois alguma hora ou 𝑀 ou 𝑀𝐶 irão
terminar.
Como provamos que 𝐿𝑀𝑇 é não decidível, mas é recursivamente enumerável, podemos con-
cluir que o seu complemento 𝐿𝑀𝑇 não é recursivamente enumerável.
Corolário 4.10. A linguagem 𝐿𝑀𝑇 não é recursivamente enumerável.
4.5 Redutibilidade
Nós provamos acima que o problema da parada é indecidível. Para isso precisamos usar o
método da diagonalização resultando em um argumento relativamente complexo. Nesta seção
iremos introduzir um método, chamado de redutibilidade. Em particular, dados dois problemas
𝐴 e 𝐵, iremos reduzir a resolução do problema 𝐵 para a resolução do problema 𝐴. Quer dizer,
se 𝐵 tiver uma solução, então esta solução pode ser usada para resolver o problema 𝐴.
Iremos usar o método da redutibilidade para provar a indecidibilidade problemas. Para isso,
temos dois problemas
∙ 𝐴 – O problema que sabemos ser indecidível, por exemplo, o problema da parada;
∙ 𝐵 – O problema para o qual gostaríamos de provar ser indecidível.
Iremos então construir uma redução do problema 𝐵 ao problema 𝐴 e chegaremos a uma con-
tradição. Assumindo que exista máquina de Turing 𝑀𝐵 que decida a linguagem 𝐵, iremos
construir uma máquina 𝑀𝐴que use 𝑀𝐵 como subrotina e decida a linguagem 𝐴. Mas como
sabemos que 𝐴 não é decidível, não tem como haver uma máquina 𝑀𝐴 o que significa que não
pode haver uma máquina 𝑀𝐵. Portanto, a linguagem 𝐵 é indecidível.
Apesar de usarmos o método da redutibilidade para demonstrarmos a indecidibilidade de
problemas, este método pode ser usado para provar outras propriedades de objetos da mate-
mática. Iremos utilizar este método no Capítulo 5 por exemplo para provar a complexidade de
problemas.
Para provar que uma linguagem 𝐵 é indecidível, seguimos os passos abaixo:
∙ Assuma por contradição que 𝐵 é decidível. Quer dizer, existe uma máquina 𝑀𝐵
Dica 4.6
que decide 𝐵;
∙ Usando a máquina 𝑀𝐵, construa uma máquina 𝑀𝑃 que decida um problema 𝑃
que é sabido ser indecidível, por exemplo, o problema da parada;
∙ Isso leva a uma contradição. Pois como 𝑃 é indecidível, não pode existir a máquina
𝑀𝑃 e consequentemente, não pode existir a máquina 𝑀𝐵;
∙ Podemos concluir que a linguagem 𝐵 é indecidível.
O passo mais difícil e que requer um pouco de criatividade e experiência é a constru-
ção da máquina 𝑀𝑃 a partir da máquina 𝑀𝐵. Para isso, tente fazer alguns exercícios
e entender as provas de indecidibilidade neste capítulo.
Teorema 4.11. A linguagem 𝐿𝑃𝐴𝑅𝐴𝑅 abaixo é indecidível:
𝐿𝑃𝐴𝑅𝐴𝑅 = {⟨𝑀,𝑤⟩ | 𝑀 para na entrada 𝑤}
Demonstração. Assuma por contradição que exista uma máquina de Turing𝑀𝑃𝐴𝑅𝐴𝑅 que decide
a linguagem 𝐿𝑃𝐴𝑅𝐴𝑅. Podemos então construir a máquina 𝑀𝑀𝑇 abaixo que decide a linguagem
𝐿𝑀𝑇 :
Dada uma entrada ⟨𝑀,𝑤⟩, a máquina 𝑀𝑀𝑇 executa os passos:
1. Rode 𝑀𝑃𝐴𝑅𝐴𝑅 em ⟨𝑀,𝑤⟩;
2. Caso 𝑀𝑃𝐴𝑅𝐴𝑅 rejeite a entrada ⟨𝑀,𝑤⟩ – o que significa que a máquina 𝑀 não para
quando dada a entrada 𝑤 –então rejeite;
3. Caso 𝑀𝑃𝐴𝑅𝐴𝑅 aceite a entrada ⟨𝑀,𝑤⟩ – o que significa que a máquina 𝑀 para quando
dada a entrada 𝑤 – então rode 𝑀 com a entrada 𝑤;
(a) Caso 𝑀 aceite a entrada 𝑤, então aceite;
(b) Caso 𝑀 rejeite 𝑤, então rejeite.
Podemos verificar que a máquina 𝑀𝑀𝑇 decide a linguagem 𝐿𝑀𝑇 . Usamos a máquina
𝑀𝑃𝐴𝑅𝐴𝑅 para verificar se a máquina 𝑀 roda indefinidamente quando dada a entrada 𝑤. Caso
afirmativo, podemos rejeitar. Caso 𝑀 não roda indefinidamente com a entrada 𝑤 (casos 3a e
3b), podemos simplesmente rodar 𝑀 com a entrada 𝑤 e verificar se ela aceita ou não a entrada
𝑤.
Mas como sabemos que a linguagem 𝐿𝑀𝑇 é indecidível, a máquina 𝑀𝑀𝑇 não pode existir. O
que significa que a máquina 𝑀𝑃𝐴𝑅𝐴𝑅 também não pode existir. Portanto, a linguagem 𝐿𝑃𝐴𝑅𝐴𝑅
é indecidível.
Teorema 4.12. A linguagem 𝐿𝑃𝐴𝑅𝐴𝑅 abaixo é indecidível:
𝐿𝑉 𝐴𝑍𝐼𝑂 = {⟨𝑀⟩ | 𝐿(𝑀) = ∅}
Demonstração. Assuma por contradição que a linguagem 𝐿𝑉 𝐴𝑍𝐼𝑂 é decidível, quer dizer, existe
uma máquina 𝑀𝑉 𝐴𝑍𝐼𝑂 que decide 𝐿𝑉 𝐴𝑍𝐼𝑂. Precisamos agora construir uma máquina 𝑀𝑀𝑇 que
use 𝑀𝑉 𝐴𝑍𝐼𝑂 para decidir o problema da parada 𝐿𝑀𝑇 . Portanto somos dados uma entrada da
forma ⟨𝑀,𝑤⟩ e queremos decidir se 𝑀 aceita ou não 𝑤. Se rodarmos 𝑀 na máquina 𝑀𝑉 𝐴𝑍𝐼𝑂,
nós descobrimos somente se 𝐿(𝑀) = ∅ ou não. Se 𝐿(𝑀) = ∅, então com certeza 𝑀 não aceita
𝑤. Mas se 𝐿(𝑀) = ∅, então não podemos afirmar se 𝑀 aceita ou não 𝑤, pois 𝑀 pode aceitar
uma outra string. Resolvemos este problema definindo uma nova máquina 𝑀𝑤 construída a
partir da máquina 𝑀 :
Para uma dada entrada 𝑥, a máquina 𝑀𝑤 executa os seguintes passos:
1. Se 𝑥 = 𝑤, então rejeite;
2. Se 𝑥 = 𝑤, então rode 𝑀 com 𝑥. Se 𝑀 aceitar 𝑥, então aceite.
Perceba que a linguagem 𝐿(𝑀 ′) ou é vazio, quando 𝑀 ′ não aceita 𝑤ou aceita somente 𝑤. Agora
podemos construir a máquina 𝑀𝑀𝑇 que usa a máquina 𝑀𝑉 𝐴𝑍𝐼𝑂 para verificar se 𝑀 aceita uma
string 𝑤.
Dada uma entrada ⟨𝑀,𝑤⟩
1. Construa a máquina 𝑀𝑤 a partir de 𝑀 ;
2. Rode a máquina 𝑀𝑉 𝐴𝑍𝐼𝑂 usando como entrada a máquina ⟨𝑀𝑤⟩;
(a) Caso 𝑀𝑉 𝐴𝑍𝐼𝑂 aceite 𝑀𝑤, então aceite;
(b) Caso 𝑀𝑉 𝐴𝑍𝐼𝑂 rejeite 𝑀𝑤, então rejeite;
Pode-se verificar que de fato a máquina 𝑀𝑀𝑇 decide a linguagem 𝐿𝑀𝑇 , o que consiste uma
contradição pois sabemos que 𝐿𝑀𝑇 é indecidível. Consequentemente, não existe a máquina
𝑀𝑉 𝐴𝑍𝐼𝑂 e 𝐿𝑉 𝐴𝑍𝐼𝑂 é indecidível.
Teorema 4.13. A linguagem 𝐿𝑅𝐸𝐺 é indecidível:
𝐿𝑅𝐸𝐺 = {⟨𝑀⟩ | 𝐿(𝑀) é regular}
Demonstração. Assumimos por contradição que a linguagem 𝐿𝑅𝐸𝐺 é decidível, o que significa
que existe uma máquina 𝑀𝑅𝐸𝐺 que decida 𝐿𝑅𝐸𝐺. Precisamos construir uma máquina 𝑀𝑀𝑇
que decida a linguagem 𝐿𝑀𝑇 a partir da máquina 𝑀𝑅𝐸𝐺, isto é, que decida para uma dada
máquina 𝑀 e uma dada entrada 𝑤, 𝑀 aceita ou não 𝑤. Iremos construir uma máquina 𝑀𝑤
a partir de 𝑀 que aceita uma linguagem regular se 𝑀 aceita 𝑤 e aceita uma linguagem não
regular caso 𝑀 não aceite 𝑤:
A máquina 𝑀𝑤 executa os passos abaixo para uma entrada 𝑥:
1. Rode 𝑀 com a entrada 𝑤;
(a) Se 𝑀 aceita 𝑤, então rejeite 𝑥;
(b) Se 𝑀 não aceita 𝑤, então aceite 𝑥 somente se 𝑥 ∈ {0𝑛1𝑛 | 𝑛 ∈ N}.
Lembrando que {0𝑛1𝑛 | 𝑛 ∈ N} é uma linguagem não regular.
Dada uma entrada ⟨𝑀,𝑤⟩
1. Construa a máquina 𝑀𝑤 a partir de 𝑀 ;
2. Rode a máquina 𝑀𝑅𝐸𝐺 usando como entrada a máquina ⟨𝑀𝑤⟩;
(a) Caso 𝑀𝑅𝐸𝐺 aceite 𝑀𝑤, então aceite;
(b) Caso 𝑀𝑅𝐸𝐺 rejeite 𝑀𝑤, então rejeite;
Pode-se verificar que de fato a máquina 𝑀𝑀𝑇 decide a linguagem 𝐿𝑀𝑇 , o que consiste uma
contradição pois sabemos que 𝐿𝑀𝑇 é indecidível. Consequentemente, não existe a máquina
𝑀𝑅𝐸𝐺 e 𝐿𝑅𝐸𝐺 é indecidível.
De fato a estratégia usada na prova do Teorema 4.13 pode ser generalizada no Teorema de
Rice.
Teorema 4.14 (Teorema de Rice). Seja 𝑃 qualquer propriedade não-trivial4, então é indecidível
saber se uma máquina de Turing satisfaz 𝑃 .
Tente provar este teorema.4Onde não trivial, significa uma propriedade para o qual existe um máquina de Turing que satisfaz a pro-
priedade, mas também existe uma máquina de Turing que não a satisfaz.
Exercícios
Exercício 4.1 Mostre que a seguinte linguagem é decidível:
𝑇𝑈𝐷𝑂𝐴𝐹𝐷 = {⟨𝐴⟩ | 𝐴 é um AFD e 𝐿(𝐴) = Σ*}.
Exercício 4.2 Mostre que a seguinte linguagem é decidível:
𝐼𝑁𝐹𝐼𝑁𝐼𝑇𝑂𝐴𝐹𝐷 = {⟨𝐴⟩ | 𝐴 é um AFD e 𝐿(𝐴) é infinito}.
Exercício 4.3 Mostre que a seguinte linguagem é decidível:
𝐼𝑁𝐹𝐼𝑁𝐼𝑇𝑂𝐴𝐷𝑃 = {⟨𝐴⟩ | 𝐴 é um Autômato de Pilha e 𝐿(𝐴) é infinito}.
Exercício 4.4 Mostre que a seguinte linguagem é decidível:
𝑃𝐴𝑅 = {⟨𝑀⟩ | 𝑀 é uma máquina de Turing que não aceita strings com um número par de 0s}.
Exercício 4.5 Caso podemos reduzir a linguagem 𝐿1 ao problema 𝐿2:
∙ Se 𝐿2 é indecidível, o que podemos afirmar de 𝐿1;
∙ Se 𝐿2 é decidível, o que podemos afirmar de 𝐿2;
∙ Se 𝐿1 é indecidível, o que podemos afirmar de 𝐿1;
∙ Se 𝐿1 é decidível, o que podemos afirmar de 𝐿1.
Exercício 4.6 Mostre que a seguinte linguagem é indecidível:
{⟨𝑀⟩ | 𝑀 é uma máquina de Turing que aceita 𝑤𝑅 quando aceita 𝑤.}
Exercício 4.7 Mostre que a seguinte linguagem é indecidível:
𝐼𝑁𝐹𝐼𝑁𝐼𝑇𝑂𝑀𝑇 = {⟨𝑀⟩ | 𝑀 é uma máquina de Turing e 𝐿(𝑀) é infinito}.
Capítulo 5
Introdução à Teoria de Complexidade
No capítulo anterior discutimos problemas que podem ser decididos e problemas para os quais
não existe nenhum algoritmo que pode os decidir. Contudo, até mesmo problemas decidíveis
podem exigir muitos recursos para serem resolvidos no tempo ou espaço de memória disponível.
Iremos neste capítulo introduzir tais métricas usadas para a análise de algoritmos.
Considere o problema de determinar se uma string pertence a linguagem:
{0𝑛1𝑛 | 𝑛 ∈ N}
Nós sabemos que este problema é decidível. Agora a pergunta é, dada uma string, em quantos
passos uma máquina de Turing precisa realizar para decidir se a dada string pertence ao conjunto
acima ou não? Podemos usar os seguintes passos para fazer tal checagem para uma dada string
𝑤 de entrada:
1. Verificar passando pela string se existe um 0 aparecendo depois de um 1. Se for o caso,
rejeite, caso contrário continue:
2. Apague um 0 e em seguida apague um 1;
3. Caso no final houver pelo menos um 0 e não se puder apagar um 1, ou um 1 e não se
puder apagar um 0, então rejeite. Caso não houver mais 0s e 1s para apagar aceite.
O tempo necessário para para executar esse algoritmo dependerá do tamanho da string
de entrada. Se a string for muito grande, então o tempo necessário será maior. O tempo de
execução também irá depender da forma da string. Por exemplo, a string da esquerda será
rejeitada rapidamente já que um 0 aparece depois do 1 já no começo da string:
010000001111111
73
O tempo de execução será uma função, 𝑓 : N → N, que leva o tamanho da string de entrada,
𝑛, para o maior número de passos, 𝑓(𝑛), necessários para decidir se uma string qualquer de
tamanho 𝑛.
Na literatura se usa normalmente o pior caso como medição da complexidade. Por isso a
função de complexidade retorna o maior valor. É possível também pensar na complexidade
média de um algoritmo. Contudo na prática tais valores são difíceis de calcular.
5.1 Notação Pequeno e Grande “O”
Muitas vezes a função de complexidade pode ser complexa o que causa muita dificuldade em
compara diferentes algoritmos para o mesmo problema. É portanto conveniente usar uma
análise assintótica de complexidade. Para isso introduzimos as notações pequeno e grande “O”.
Para isso simplesmente consideramos o fator de expoente maior da função de complexidade
ignorando o seu coeficiente.
Por exemplo, considere a função de complexidade 𝑓(𝑛) = 47𝑛4 + 1200𝑛3 + 2𝑛2 + 000𝑛+ 1.
A sua complexidade assintótica é de 𝑛4, apesar que 𝑛3 tem um coeficiente de 1200. A lógica
por trás desta simplificação é que quando o tamanho da entrada aumenta, o valor dos outros
monômios não interferem tanto no resultado da função. Escrevemos usando a notação grande
“O” que 𝑓(𝑛) tem complexidade assintótica 𝑂(𝑛4).
Definição 5.1. Sejam 𝑓 e 𝑔 duas funções N → R+ do conjunto dos números naturais para o
conjunto dos números reais positivos. Dizemos que a complexidade de 𝑓(𝑛) é 𝑂(𝑔(𝑛)), escrita
𝑓(𝑛) ∈ 𝑂(𝑔(𝑛)), se existem números inteiros positivos 𝑛0, 𝑐 tal que para todo 𝑛 ≥ 𝑛0:
𝑓(𝑛) ≤ 𝑐𝑔(𝑛)
Dizemos que 𝑔(𝑛) é a cota superior de 𝑓 .
Como exemplo considere as cotas superiores das funções abaixo:
∙ 𝑓(𝑛) = 20𝑛2 + 2 pertence 𝑂(𝑛2);
∙ ℎ(𝑛) = 20𝑛 log(𝑛2) + 100 pertence a 𝑂(𝑛 log(𝑛));
∙ 𝑔(𝑛) = 2𝑛 + 200𝑛4 + 1000 pertence a 𝑂(2𝑛).
Para se ter uma comparação do comportamento destas funções quando aumentamos o ta-
manho da entrada 𝑛, considere a tabela a seguir:
log2(𝑛) 𝑛 𝑛 log2(𝑛) 𝑛2 𝑛3 2𝑛
10 3 10 30 102 103 103
102 6 102 664 104 106 1030
103 9 103 9965 106 109 10300
Percebam que para entradas de tamanhos bem maiores o componente 2𝑛 domina o componente
𝑛2 que domina o componente 𝑛 e assim por diante. Por isso que a notação assintótica faz
sentido na prática.
Usamos a notação grande “O” para formalizar quando uma função não é pior que uma outra
função. Por exemplo, a função quadrática 𝑛2 não é pior que a função exponencial 2𝑛. Mas
podemos também definir quando uma função não é melhor que uma outra função. Para isso
usamos a notação pequeno “O”, definido abaixo:
Definição 5.2. Sejam funções 𝑓, 𝑔 : N → R+, dizemos que 𝑓(𝑛) ∈ 𝑜(𝑔(𝑛)) se temos que:
lim𝑛→∞
𝑓(𝑛)
𝑔(𝑛)= 0
Quando 𝑓 ∈ 𝑜(𝑔), significa que a função 𝑔 cresce mais rápido que a função 𝑓 quando
aumentamos o tamanho da entrada. Por exemplo:
∙ 𝑛2 ∈ 𝑜(𝑛3);
∙ 𝑛 log(𝑛) log(𝑛) ∈ 𝑜(𝑛 log(𝑛)
∙ 𝑛 log(𝑛) ∈ 𝑜(𝑛2)
Voltemos ao problema inicial de determinar se uma string pertence ao conjunto:
{0𝑛1𝑛 | 𝑛 ∈ N}
Nós descrevemos um algoritmo que soluciona este problema. Podemos agora analisar a com-
plexidade deste algoritmo para uma entrada de tamanho 𝑛. No passo 1, o algoritmo no pior
dos casos precisa percorrer e checar 𝑛 caracteres e depois voltar ao começo da fita, quer dizer,
realizar 2𝑛 operações o que pertence a 𝑂(𝑛). Em seguida no passo 2, a máquina cada vez
que apagar um 0, precisa realizar 𝑛 operações. Como a máquina precisa repetir esse passo no
máximo 𝑛/2 vezes, temos uma complexidade de 𝑂(𝑛2). No passo 3, a máquina checa se existem
0s ou 1s sobrando, o que pode ser feito em 𝑂(𝑛) passos. Portanto a complexidade do algoritmo
é 𝑂(𝑛) +𝑂(𝑛2) +𝑂(𝑛) o que resulta em uma complexidade de 𝑂(𝑛2).
Definição 5.3. Seja 𝑡 : N → R+ uma função. Definimos a classes de complexidade de tempo
TIME(𝑡(𝑛)) todos os problemas decidíveis que podem ser resolvidos por um algoritmo de com-
plexidade 𝑂(𝑡(𝑛)).
5.2 A Classe P
Analisaremos nesta seção a classe de problemas que podem ser resolvidos por algoritmos de
complexidade polinomial. Esta classe de problemas se distingue da classe de problemas que
podem ser resolvidos em tempo exponencial, chamada de classe NP. Problemas na classe P
são problemas para os quais se espera que possamos resolve até mesmo com entradas bastante
grandes.
Algoritmos exponenciais normalmente ocorrem quando tentamos enumerar todas as possí-
veis soluções. Este tipo de método é chamado em inglês de brute force. Contudo, muitos dos
problemas resolvidos por algoritmos brute force podem ser resolvidos em tempo polinomial no
tamanho da entrada usando outras técnicas como programação dinâmica ao não precisar passar
por todas as possíveis soluções.
Apesar que as diferenças de complexidades de dois algoritmos polinomiais, por exemplo, 𝑛
e 𝑛3, podem ter impactos importantes na prática, para o nosso estudo neste capítulo iremos
ignorar estas diferenças colocando-os na mesma classe de algoritmos. Isto pode parecer uma
generalização muito grande já que algoritmos de complexidade 𝑛 e de complexidade 𝑛3 tem
eficiências muito diferentes. De fato, essas diferenças são importante, mas também são im-
portantes as diferenças entre algoritmos polinomiais e algoritmos exponenciais. Análises mais
refinadas de complexidade de algoritmos serão realizadas em outras disciplinas, como disciplinas
de estruturas de dados e análise de algoritmos.
Definição 5.4. P é a classe de linguagens decidíveis em tempo polinomial por uma máquina
de Turing determinística, quer dizer
P =⋃𝑘∈N
TIME(𝑛𝑘).
Veremos a seguir alguns problemas da classe P. Muitos algoritmos de grafos são problemas
que podem ser resolvidos por algoritmos polinomias no tamanho da entrada. Por exemplo, o
problema de encontrar se em um grafo existe um caminho entre dois nós:
𝐶𝐴𝑀𝐼𝑁𝐻𝑂 = {⟨𝐺, 𝑠, 𝑡⟩ | 𝐺é um grafo onde existe um caminho do nó 𝑠 até o nó 𝑡}.
Um exemplo de um grafo está ilustrado abaixo:
ab
c
d
e
f
g
h
Este grafo tem oito nós 𝑎, 𝑏, 𝑐, 𝑑, 𝑒, 𝑓, 𝑔, ℎ, e sete arestas. Existe um caminho entre o nó 𝑎 e o
nó 𝑒, o caminho 𝑎 → 𝑏 → 𝑐 → 𝑑 → 𝑒. Já não existe um caminho entre o nó 𝑎 e o nó ℎ.
Teorema 5.5. 𝐶𝐴𝑀𝐼𝑁𝐻𝑂 ∈ P.
Demonstração. Para mostrar que o problema 𝐶𝐴𝑀𝐼𝑁𝐻𝑂 ∈ P precisamos desenvolver um
algoritmo que resolva o problema do caminho em tempo polinomial no tamanho do grafo.
Consideramos o tamanho do grafo o número de nós mais o número de arestas. Por exemplo, o
grafo acima tem tamanho quinze.
Perceba que somente enumerar todas os possíveis caminhos de um grafo não seria uma
solução polinomial. Caso o grafo tenha 𝑚 nós, enumerar todos os caminhos levaria aproxima-
damente 𝑚𝑚 operações o que é exponencial. Precisamos de uma algoritmo mais inteligente. A
ideia é que não precisamos passar pelo mesmo nó duas vezes. Essas várias formas de buscar em
um grafo, por exemplo, busca por largura e busca por profundidade. Abaixo está um algoritmo:
1. Marque o nó 𝑠 e repita o passo seguinte até que nenhum novo nó é marcado;
2. Caso haja uma aresta (𝑎, 𝑏) onde 𝑎 está marcado, mas 𝑏 não esta marcado, então marque
𝑏;
3. Caso 𝑡 seja marcado, então aceite, caso contrário rejeite.
Vamos analisar este algoritmo. A primeira e última operações podem ser realizadas em um
número constante de passos. Já o segundo passo requer 𝑚 passos onde 𝑚 é o número de arestas
do grafo. Portanto, a complexidade deste algoritmo é polinomial.
Nosso próximo exemplo é determinar se dois números inteiros são primos relativos, quer dizer
não tem divisores em comum. Este problema pode ser representado pela seguinte linguagem:
𝑃𝑅𝐼𝑀𝑂𝑆 −𝑅𝐸𝐿 = {⟨𝑥, 𝑦⟩ | 𝑥 e 𝑦 são primos relativos}
Teorema 5.6. 𝑃𝑅𝐼𝑀𝑂𝑆 −𝑅𝐸𝐿 ∈ P.
Demonstração. Para provar este teorema precisamos propor um algoritmo que decida se um par
de números inteiros ⟨𝑥, 𝑦⟩ pertencem a 𝑃𝑅𝐼𝑀𝑂𝑆 − 𝑅𝐸𝐿, quer dizer, se são primos relativos.
Para isso, usaremos o algoritmo de Euler que calcula o máximo divisor comum de dois números,
escrito 𝑚𝑑𝑐(𝑥, 𝑦). Por exemplo, 𝑚𝑑𝑐(100, 35) = 5. Caso 𝑚𝑑𝑐(𝑥, 𝑦) = 1 então 𝑥 e 𝑦 são primos
relativos.
O algoritmo é descrito a seguir:
1. Repita até que 𝑦 = 0 o seguintes passos:
(a) Se 𝑥 := 𝑥 𝑚𝑜𝑑 𝑦;
(b) Troque 𝑥 e 𝑦;
2. Retorne 𝑥.
Usamos este algoritmo e depois checamos se a saída é 1, neste caso aceita, ou diferente de
1, neste caso rejeite.
1. Dado ⟨𝑥, 𝑦⟩, rode 𝑚𝑑𝑐(𝑥, 𝑦);
2. Caso resulte em 1, aceite;
3. Caso contrário, rejeite.
Devemos agora provar que o algoritmo acima roda em tempo polinomial no tamanho da
entrada ⟨𝑥, 𝑦⟩. A análise se reduz ao loop do algoritmo de Euler. Perceba que 𝑥 := 𝑥 𝑚𝑜𝑑 𝑦, faz
com que o valor de 𝑥 diminua em pelo menos a metade. Como 𝑥 e 𝑦 são invertido no loop, isso
faz com que na próxima iteração o valor original em 𝑦 seja reduzido em pelo menos a metade
também. Ou seja, em uma iteração o valor original de 𝑥 é reduzido pela metade, e na outra
iteração o valor original de 𝑦 é reduzido pela metade. Portanto, o loop irá ser executados no pior
caso 2 log2 𝑥 e 2 log2 𝑦 vezes. Dado que representamos um inteiro 𝑥 em binário, a complexidade
do algoritmo é 𝑂(𝑛).
Existem um número muito grande de problemas na classe P, muitos dos quais você deve
ter visto nas suas aulas introdutórias de programação. Abaixo segue uma lista curta destes
problemas:
∙ Ordenar uma lista;
∙ Multiplicação de matrizes 𝑛× 𝑛;
∙ Busca binária;
∙ Algoritmo de Dijkstra para descobrir o menor caminho em um grafo;
∙ Operações de inserção e remoção em uma árvore binária;
∙ Programação Linear;
∙ Determinar se um número é primo.
5.3 A Classe NP
Em muitos casos é possível evitar a explosão de casos a serem analisados e evitar o uso de força
bruta para resolver um problema. Contudo, para muitos problemas importantes não sabemos
como evitar a explosão de estados e a única solução é enumerar todos (ou uma quantidade muito
grande) de casos. Pode ser que haja uma forma muito inteligente de tratar esse problemas
e resolvê-los em tempo polinomial, mas ainda não descobrimos, ou pode ser que não haja
nenhuma forma de resolver estes problemas. Infelizmente não sabemos qual é o caso. Este é
um dos problemas mais importantes da teoria de computação, chamado o problema:
P versus NP
Para entender esse problema, precisamos primeiro definir a classe NP de problemas. In-
formalmente, um problema é da classe NP se podemos checar em tempo polinomial se uma
solução dada é correta ou não. Por exemplo, considere o seguinte grafo:
O problema é descobrir um caminho que passe por todos os nós do grafo sem passar pelo
mesmo nó mais que uma vez. Descobrir tal caminho é difícil, pois existem muitas possibilidades,
mas checar se um dado caminho resolve o problema é fácil. Por exemplo, o caminho ilustrado
pela linha pontilhada no grafo acima é uma solução o que é fácil verificar.
Outro problema que é difícil resolver, mas é fácil de checar se a solução dada é correta é o
problema da satisfatibilidade. Considere a fórmula proposicional abaixo:
(𝑎 ∨ 𝑏 ∨ ¬𝑐) ∧ (¬𝑎 ∨ 𝑏 ∨ ¬𝑐)
Como assinalar 𝑎, 𝑏, 𝑐 para verdadeiro ou falso de tal forma que a fórmula acima seja verdadeira.
Novamente, existem muitos casos, de fato 23 possibilidades para esta fórmula bem simples.
Contudo, é fácil checar que se 𝑎 e 𝑏 forem verdadeiros e 𝑐 falso é uma solução para o problema.
Definição 5.7. Um verificador de uma linguagem L é um algoritmo 𝐴, tal que
L = {𝑤 | 𝐴 aceita ⟨𝑤, 𝑐⟩ para alguma string 𝑐}.
Nós medimos a complexidade do verificador em função do tamanho da entrada 𝑤 somente. Uma
linguagem pode ser verificada em tempo polinomial se ela tem um verificador de complexidade
polinomial. A string 𝑐 é um certificado ou prova usada para verificar 𝑤.
Definição 5.8. A classe NP é o conjunto de todos as linguagens que tem um verificador de
complexidade polinomial.
Percebam que qualquer problema na classe P é um problema da classe NP, já que todos
os problemas da classe P tem um verificador de complexidade polinomial.
O nome NP vem do fato que uma máquina de Turing não determinística pode resolver
o problema em tempo polinomial. Isso porque uma máquina não determinística pode sim-
plesmente adivinhar qual a solução do problema, caso exista, e como essa solução pode ser
construída usando um número polinomial de passos, caso contrário não haveria um verifica-
dor de complexidade polinomial, a máquina de Turing não determinística termina em tempo
polinomial no tamanho da entrada.
O seguinte teorema formaliza esta ideia.
Teorema 5.9. Um problema é em NP se e somente se este problema pode ser decidido em
tempo polinomial por uma máquina de Turing não determinística.
Demonstração. Para a direção direta, dado uma linguagem 𝑃 em NP. Seja 𝑉 o verificador que
checa por soluções em tempo 𝑛𝑘. Sabemos que 𝑉 existe pela definição de problemas NP. Nós
construímos a seguinte máquina de Turing não determinística que decide a linguagem 𝑃 :
1. Dado uma string 𝑤 de tamanho 𝑛:
(a) Adivinhe uma string 𝑠 de tamanho no máximo 𝑛𝑘;
(b) Rode o verificador 𝑉 com a entrada ⟨𝑤, 𝑠⟩;
(c) Caso o verificador aceite, aceite, caso contrário rejeite.
Para a direção inversa, considere uma linguagem 𝑃 que pode ser decidida por uma máquina
de Turing não determinística 𝑀 . Precisamos construir um verificador que cheque em tempo
polinomial se uma string pertence a 𝑃 .
1. Dada uma entrada ⟨𝑤, 𝑐⟩;
(a) Simule a máquina 𝑀 usando 𝑐 como guia das escolhas usadas pela máquina de
Turing não determinística;
(b) Caso 𝑀 aceite, então aceite. Caso contrário rejeite.
Uma consequência deste teorema é que apesar de máquinas de Turing determinísticas serem
tão expressivas quanto máquinas de Turing não determinísticas, elas diferem no tempo de
resolução de problemas. Isso nos motiva a considerar a seguinte classes de linguagens:
Definição 5.10.
NTIME(𝑡(𝑛)) =
⎧⎨⎩ 𝐿 | 𝐿 pode ser decidido por uma máquina de Turing
não determinística em tempo 𝑂(𝑡(𝑛))
⎫⎬⎭Do teorema acima podemos concluir que:
Corolário 5.11. NP =⋃
𝑘∈N NTIME(𝑛𝑘).
Além de todos os problemas P que vimos anteriormente, os seguintes problemas também
pertencem a classe NP:
Exemplo 5.12. O problema 3-SAT é definido como o problema de determinar se fórmulas da
lógica proposicional da forma abaixo é satisfatível:
𝐹 = (𝐿1,1 ∨ 𝐿2,1 ∨ 𝐿3,1) ∧ (𝐿1,2 ∨ 𝐿2,2 ∨ 𝐿3,2) ∧ · · · ∧ (𝐿1,𝑛 ∨ 𝐿2,𝑛 ∨ 𝐿3,𝑛)
Nesta fórmulas 𝐿𝑖,𝑗 pode ser uma variável proposicional 𝐴 ou a sua negação ¬𝐴. Chamamos
esses tipos de fórmulas de fórmulas em forma normal conjuntiva. O problema se chama 3-SAT
pois em cada disjunção temos exatamente 3 fórmulas, 𝐿1,𝑗, 𝐿2,𝑗 e 𝐿3,𝑗, chamada de cláusula.
Por exemplo, a fórmula acima 𝐹 tem 𝑛 cláusulas.
Provamos que este problema é em NP pois dada uma interpretação para as variáveis em 𝐹 ,
isto é assinalando as variáveis para verdadeiro ou falso, é possível checar em tempo polinomial
no tamanho de 𝐹 se a interpretação torna a fórmula 𝐹 verdadeira verificando se cada cláusula
é verdadeira. De fato, podemos checar isso em tempo linear no tamanho de 𝐹 .
Exemplo 5.13. A teoria de grafos tem vários problemas que estão em NP. Por exemplo, o
problema do clique. Um clique em um dado grafo é um subgrafo onde todos os pares de nós tem
uma aresta entre eles. O subgrafo ilustrado na figura abaixo é um clique.
O problema 𝑘-clique é o problema de saber se em um grafo tem um clique com 𝑘 nós.
Portanto, o grafo acima satisfaz o problema 4-cliques, mas não satisfaz o problema 5-cliques,
já que não há subgrafo que forme um clique com 5 nós. Podemos definir o problema do clique
como um problema de linguagens:
𝐶𝐿𝐼𝑄𝑈𝐸 = {⟨𝐺, 𝑘⟩ | 𝐺 é um grafo que tem um 𝑘-clique}.
Este problema está em NP. Podemos mostrar isso propondo um verificador que dado uma
possível solução, 𝑆, e um grafo, 𝐺, pode verificar se 𝑆 é de fato um 𝑘-clique de 𝐺.
1. Teste se 𝑆 tem de fato 𝑘 nós de 𝐺;
2. Teste se 𝐺 tem arestas conectando cada par de nós de 𝑆;
3. Caso os dois testes sejam satisfeitos então aceite, caso contrário rejeite.
Exemplo 5.14. Outro problema clássico em NP é o problema do caixeiro viajante. A entrada
é um grafo cujos nós representam cidades ou localidades e as arestas tem pesos especificando
as distâncias entre as localidades. O desafio é encontrar um ciclo Hamiltoniano cuja a soma
dos pesos das suas arestas seja menor ou igual a um dado valor 𝑛.
Considere o grafo abaixo. O caminho ilustrado tem soma de pesos 1+2+4+2+3+5+1 = 18.
Portanto o grafo abaixo tem uma solução para o problema do caixeiro viajante com peso 18 ou
acima.
2
1
4
2
3
5
1
710
8
6
162
Novamente, podemos descrever este problema como um problema de linguagens usando a
linguagem abaixo:
𝐶𝐴𝐼𝑋𝐸𝐼𝑅𝑂 =
⎧⎨⎩ ⟨𝐺, 𝑛⟩ | 𝐺 é um grafo que tem solução para
o problema do caixeiro viajante com peso 𝑛
⎫⎬⎭Esta linguagem está em NP. Para isso basta propor um verificador que cheque em tempo po-
linomial se uma solução proposta é de fato uma solução. Deixamos a definição deste verificador
como exercício.
5.4 P versus NP e a Classe NP-completa
Vimos duas classes de problemas, a classe P e a classe NP. A primeira são problemas que
podem ser resolvidos por máquinas de Turing determinísticas em tempo polinomial, enquanto
a segunda por máquinas de Turing não determinísticas. Uma forma de interpretar essas duas
classes de problemas é da seguinte forma:
∙ Para todo problema em P, uma solução pode ser encontrada facilmente;
∙ Para todo problema em NP, a corretude de uma solução pode ser verificada facilmente.
Claro que se uma solução pode ser encontrada facilmente, podemos também checar que
ela de fato é uma solução para o problema. Portanto temos que P ⊆ NP. Mas e o inverso:
NP ⊆ P? Quer dizer, todo problema que é possível checar se uma solução é correta facilmente,
podemos encontrar a solução facilmente?
Infelizmente não sabemos como responder essa pergunta. Quer dizer, não podemos afirmar
que a classe P = NP ou não. Esse é um dos grandes problemas abertos na teoria da com-
putação. Como este problema tem resistido muito tempo, a maioria dos pesquisadores estão
começando a acreditar que as classes são diferentes, mas isso é somente especulação.
O melhor que podemos fazer é afirmar que é possível resolver um problema em NP em
tempo exponencial, quer dizer:
NP ⊆ EXPTIME =⋃𝑘∈N
TIME(2𝑛𝑘
).
Um grande avanço nesta questão foi dada por Stephan Cook e Leonid Levin. Eles descobriram
alguns problemas que são tão difíceis como qualquer outro problema na classe NP. Um desse
problemas é o problema 𝑆𝐴𝑇 a seguir:
𝑆𝐴𝑇 = {𝐹 | 𝐹 é uma fórmula proposicional satisfatível}
Eles provaram que se é possível resolver o problema 𝑆𝐴𝑇 em tempo polinomial então é possível
resolver qualquer outro problema NP em tempo polinomial. Isso quer dizer que não tem
problema na classe NP que tenha complexidade maior que o problema 𝑆𝐴𝑇 .
Teorema 5.15. 𝑆𝐴𝑇 ∈ P se e somente se P = NP.
Um desafio é formalizar a frase “é possível resolver qualquer outro problema NP em tempo
polinomial” acima. Para isso usamos a técnica de redução em tempo polinomial. Esta téc-
nica é muito parecido ao problema da redutibilidade de problemas indecidíveis descrito no
Capítulo 4.5. Dados dois problemas 𝐴 e 𝐵 dizemos que o problema 𝐴 pode ser reduzido efici-
entemente ao problema 𝐵, quando uma solução do problema 𝐵 pode ser usada para resolver o
problema 𝐴.
Definição 5.16. Uma função 𝑓 : Σ* → Σ* é uma função de complexidade polinomial se existe
uma máquina de Turing 𝑀 que para uma entrada qualquer 𝑤, a máquina para com 𝑓(𝑤) na
fita em tempo polinomial.
Definição 5.17. Um problema 𝐴 tem uma redução polinomial ao problema 𝐵, escrito 𝐴 ≤𝑃 𝐵
se existe uma função de complexidade polinomial 𝑓 : Σ* → Σ* tal que para todo 𝑤 ∈ Σ*
𝑤 ∈ 𝐴 ⇐⇒ 𝑓(𝑤) ∈ 𝐵
Chamamos 𝑓 de redução polinomial de 𝐴 a 𝐵.
Se um problema 𝐴 já tem um algoritmo que o resolve em tempo polinomial, quer dizer o
problema esta em P, e existe uma redução polinomial de um problema 𝐵 para o problema 𝐴,
então podemos resolver o problema 𝐵 também em tempo polinomial.
Teorema 5.18. Se 𝐴 ≤𝑃 𝐵 e 𝐵 ∈ P, então 𝐴 ∈ P.
Demonstração. Seja 𝑀 o algoritmo que decide 𝐵 em tempo polinomial e 𝑓 o mapa de redução
polinomial de 𝐴 a 𝐵. Podemos construir um algoritmo que decide 𝐴.
1. Dada uma entrada 𝑤:
2. Usamos 𝑀 em 𝑓(𝑤), se aceita, então aceite, caso contrário rejeite.
Como ambas 𝑓 e𝑀 tem complexidade polinomial, este algoritmo tem complexidade polinômnial
no tamanho da entrada.
Vamos reduzir o problema 3− 𝑆𝐴𝑇 descrito acima ao problema 𝐶𝐿𝐼𝑄𝑈𝐸.
Teorema 5.19. O problema 3− 𝑆𝐴𝑇 tem uma redução polinomial ao problema 𝐶𝐿𝐼𝑄𝑈𝐸.
Demonstração. Dada uma fórmula 𝐹 da forma:
𝐹 = (𝐿1,1 ∨ 𝐿2,1 ∨ 𝐿3,1) ∧ (𝐿1,2 ∨ 𝐿2,2 ∨ 𝐿3,2) ∧ · · · ∧ (𝐿1,𝑛 ∨ 𝐿2,𝑛 ∨ 𝐿3,𝑛)
Iremos reduzir esta fórmula a um grafo 𝐺 e mostraremos que 𝐺 tem um 𝐶𝐿𝐼𝑄𝑈𝐸, em parti-
cular, um 𝑛-Clique, onde 𝑛 é o número de cláusulas de 𝐹 se e somente se 𝐹 é satisfatível.
Os nós serão organizados em 𝑛 conjuntos, cada um com três nós 𝐿1,𝑗, 𝐿2,𝑗 e 𝐿3,𝑗 para
1 ≤ 𝑗 ≤ 𝑛. O grafo tem arestas somente entre nós de conjuntos diferentes e sem arestas
conectando uma variável proposicional 𝐴 a sua negação ¬𝐴. Por exemplo, considere a fórmula
abaixo:
(𝐴 ∨ ¬𝐵 ∨ 𝐶) ∧ (¬𝐴 ∨𝐵 ∨ 𝐶) ∧ (𝐴 ∨𝐵 ∨ ¬𝐶)
O grafo associado a esta fórmula é o seguinte:
A
¬B
C
A
B
¬C
¬A B C
Vamos demonstrar que o grafo de uma fórmula 𝐹 tem um 𝑛-clique se e somente se a fórmula
𝐹 é satisfatível. Podemos ver o clique no grafo acima marcado pelas arestas mais grossas.
Considere que 𝐹 é satisfatível. Portanto existe uma interpretação que torna todas as suas 𝑛
cláusulas verdadeiras ou seja pelo menos uma das disjunções é verdadeira em cada cláusula. No
grafo 𝐺 selecione em cada grupo a disjunção que torna a cláusula correspondente verdadeira.
No exemplo acima, escolhemos 𝐴,𝐵,𝐴. Estes nós formam um 𝑛-clique, pois escolhemos 𝑛 nós
e cada para de nós é conectado por uma aresta, já que não infrige nenhuma das regras acima.
Agora considere um grafo com um 𝑛-clique. Iremos mostrar que a fórmula 𝐹 é satisfatível.
Para isso construímos a interpretação que torna todas as fórmulas nos nós do clique verdadeiras.
Usando o mesmo raciocínio acima, esta interpretação torna a fórmula verdadeira.
Definimos a classe de problemas NP-completo da seguinte forma:
Definição 5.20. Um linguagem 𝐵 pertence à classe NP-completo (ou simplesmente é NP-
completo) se para qualquer linguagem 𝐴 em NP:
∙ 𝐴 ≤𝑃 𝐵;
∙ 𝐵 em NP.
O seguinte teorema nos permite provar se um problema é NP-completo mostrando uma
redução polinomial de um problema NP-completo.
Teorema 5.21. Se 𝐵 ≤𝑃 𝐶 onde 𝐵 é NP-completo e 𝐶 em NP, então B é NP-completo.
Demonstração. Devemos provar que qualquer problema 𝐴 em NP, temos 𝐴 ≤𝑃 𝐶. Mas como
sabemos que 𝐵 é NP-completo, temos que 𝐴 ≤𝑃 𝐵. Pela hipótese que 𝐵 ≤𝑃 𝐶, temos por
transitividade que 𝐴 ≤𝑃 𝐶.
Exsitem muitos problemas NP-completos. Veja o link abaixo para alguns exemplos:
http://pt.wikipedia.org/wiki/NP-completo#Exemplos.
Discutimos um exemplo curioso de um problema NP-completo.
Exemplo 5.22. O jogo Campo Minado (Minesweeper, em inglês) é nativo ao sistema operaci-
onal Windows. O jogo em si é muito simples, o computador inicia o jogo mostrando uma grade
de quadrados vazios. Alguns deles ocultam minas, e outros são seguros. O objetivo do jogo é
descobrir onde estão as minas sem detonar nenhuma delas. A cada jogada você deve escolher
um quadrado, se houver uma mina debaixo dele, ela é detonada e o jogo acaba, e você perdeu.
Mas se não houver, o computador escreve nesse quadrado um número (de 1 a 8) que informa
quantas minas estão escondidas nos oito quadrados adjacentes a esse (na horizontal, vertical e
diagonal).
Figura 5.1: Exemplo de posição do Campo Minado
Não esbarrando em uma mina na primeira tentativa, você obtém uma informação parcial
sobre a localização de minas próximas. Com isso, você poderá escolher mais um quadrado, e,
novamente, detonar uma mina ou obter mais informações sobre a posição de bombas próxi-
mas. Se desejar, você poderá marcar um quadrado como se ele contivesse uma bomba, mas sem
detoná-la; mas se errar, perde o jogo. Procedendo assim, você pode ganhar o jogo localizando e
marcando todas as minas.
A figura 1 mostra um cenário típico do jogo. Nela, os asteriscos mostram uma mina conhe-
cida (posição já deduzida), os números são a informação que você obteve do computador, e as
letras marcam quadrados de status ainda não verificado.
Analisando as posições, podemos deduzir que os quadrados marcados com a letra ’A’ devem
conter minas, por causa do 2 que aparecem logo abaixo deles. Os quadrados com ’B’ também
devem conter minas, por causa dos 4 e 5 próximos. Da mesma maneira, ’C’ deve conter uma
mina; e por consequência ’D’ e ’E’ não devem conter mina alguma. O status de ’F’ pode então
ser definido após algumas jogadas, marcando-se D e vendo que número aparece.
Resolver o problema do Campo Minado não é simplesmente ganhar o jogo, achar minas.
Mas determinar se um dado estado do que pretende ser um jogo de Campo Minado é ou não
logicamente coerente. Por exemplo, se, durante o jogo, aparecesse o estado mostrado na figura
2, saberiamos que o programa fizera um erro: não há qualquer alocação de minas compatível
com a informação mostrada.
Podemos verificar que o Campo Minado é equivalente ao problema SAT no seguinte sentido:
o problema SAT para um dado circuito booleano pode ser ’codificado’ como um ’problema da
coerência do Campo Minado’ para alguma posição do jogo, usando-se um procedimento de
Figura 5.2: Posição impossível no Campo Minado
codificação que é processado em tempo polinomial. Portanto, se puder resolver o ’problema da
coerência do Campo Minado’ em tempo polinomial, teria resolvido o problema SAT para esse
circuito em tempo polinomial. Em outras palavras, Campo Minado é NP-Completo.
Figura 5.3: Um fio no Campo Minado
A prova envolve um procedimento sistemático para converter circuitos booleanos em posições
do Campo Minado. Nela, um quadrado na grade tem estado ’T’ se conter uma mina, e ’F’ se
não conter. O primeiro passo não envolve nenhuma porta, mas os fios que a conectam. A
figura 3 mostra um fio do Campo Minado. Todos os quadrados marcados com ’x’ contêm uma
mina (T) ou não contém (F), mas não sabemos quais. Todos os quadrados marcados com x’
fazem o oposto de x. Deve ser verificado se todos os números mostrados estão corretos, quer x
seja ’T’ ou ’F’. O efeito do fio é “propagar” o sinal ’T’ ou ’F’ ao longo de sua extensão, pronto
para ser introduzido numa porta. A figura 4 mostra uma porta NOT. Os números marcados no
Figura 5.4: Porta lógica NOT no Campo Minado
bloco do meio forçam um intercâmbio de x e x’ no fio de saída, se comparado ao fio de entrada.
A porta AND é mais complicada. Tem dois fios de entrada, ’U’ ’V’, e um de saída ’W’. Para
estabelecer que isto é uma porta AND supomos que a saída é ’T’ e mostramos que ambas as
saídas devem ser ’T’ também. Como a saída é ’T’, todo simbolo t deve indicar uma mina, e
todo t’ uma não mina. Ora, 3 acima e abaixo de a3 implica que a2 e a3 são minas, portanto
a1 não é uma mina, portanto s é uma mina. De maneira semelhante, r é uma mina. Além
disso o 4 central já tem quatro minas como vizinhos, o que implica que u’ e v’ não são minas,
portanto u e v são minas, ou seja, ’U’ e ’V’ têm valor de verdade. Inversamente, se ’U’ e ’V’
têm valor ’T’, ’W’ também tem. Portanto, temos a porta AND.
Figura 5.5: Porta lógica AND no Campo Minado
top related