Notas de Aula Teoria da computação

90
Teoria da Comptutação Vivek Nigam 8 de fevereiro de 2016

description

Arquivo falando sobre autômatos.

Transcript of Notas de Aula Teoria da computação

Page 1: Notas de Aula Teoria da computação

Teoria da Comptutação

Vivek Nigam

8 de fevereiro de 2016

Page 2: Notas de Aula Teoria da computação

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

Page 3: Notas de Aula Teoria da computação

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

Page 4: Notas de Aula Teoria da computação

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

Page 5: Notas de Aula Teoria da computação

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

Page 6: Notas de Aula Teoria da computação

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

Page 7: Notas de Aula Teoria da computação

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

Page 8: Notas de Aula Teoria da computação

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 {}.

Page 9: Notas de Aula Teoria da computação

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} Γ = {𝑎, 𝑏, 𝑐, 𝑑, 𝑒, . . . , 𝑡, 𝑢, 𝑣, 𝑧}

Page 10: Notas de Aula Teoria da computação

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

Page 11: Notas de Aula Teoria da computação

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 (𝑦, 𝑥) ∈ 𝑅;

Page 12: Notas de Aula Teoria da computaçã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.

Page 13: Notas de Aula Teoria da computação

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

Page 14: Notas de Aula Teoria da computação

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.

Page 15: Notas de Aula Teoria da computação

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};

∙ 𝑥 ∈ {𝑥};

∙ ∅ = {∅};

Page 16: Notas de Aula Teoria da computação

∙ ∅ ∈ {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.)

Page 17: Notas de Aula Teoria da computação

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

Page 18: Notas de Aula Teoria da computação

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:

Page 19: Notas de Aula Teoria da computação

∙ 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;

Page 20: Notas de Aula Teoria da computação

∙ 𝛿 : 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

Page 21: Notas de Aula Teoria da computação

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.

Page 22: Notas de Aula Teoria da computação

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.

Page 23: Notas de Aula Teoria da computação

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

Page 24: Notas de Aula Teoria da computação

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}:

Page 25: Notas de Aula Teoria da computação

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.

Page 26: Notas de Aula Teoria da computação

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).

Page 27: Notas de Aula Teoria da computação

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

Page 28: Notas de Aula Teoria da computação

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.

Page 29: Notas de Aula Teoria da computação

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}

Page 30: Notas de Aula Teoria da computação

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

ε

ε

Page 31: Notas de Aula Teoria da computação

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.

Page 32: Notas de Aula Teoria da computação

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.

Page 33: Notas de Aula Teoria da computação

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:

Page 34: Notas de Aula Teoria da computação

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.

Page 35: Notas de Aula Teoria da computação

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.

Page 36: Notas de Aula Teoria da computação

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.

Page 37: Notas de Aula Teoria da computação

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;

Page 38: Notas de Aula Teoria da computação

∙ 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;

Page 39: Notas de Aula Teoria da computação

∙ (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}*}.

Page 40: Notas de Aula Teoria da computação

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

Page 41: Notas de Aula Teoria da computação

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.

Page 42: Notas de Aula Teoria da computação

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;

Page 43: Notas de Aula Teoria da computação

∙ 𝑞 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.

Page 44: Notas de Aula Teoria da computação

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:

Page 45: Notas de Aula Teoria da computação

∙ 𝑎 → 𝑏, 𝑑𝑟 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:

Page 46: Notas de Aula Teoria da computação

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:

Page 47: Notas de Aula Teoria da computação

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

Page 48: Notas de Aula Teoria da computaçã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:

Page 49: Notas de Aula Teoria da computação

∙ 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.

Page 50: Notas de Aula Teoria da computação

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.

Page 51: Notas de Aula Teoria da computação

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.

Page 52: Notas de Aula Teoria da computação

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

Page 53: Notas de Aula Teoria da computação

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.

Page 54: Notas de Aula Teoria da computação

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:

Page 55: Notas de Aula Teoria da computação

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.

Page 56: Notas de Aula Teoria da computação

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.

Page 57: Notas de Aula Teoria da computação

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.

Page 58: Notas de Aula Teoria da computaçã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 𝐿𝑀𝑇 .

Page 59: Notas de Aula Teoria da computação

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

Page 60: Notas de Aula Teoria da computação

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

Page 61: Notas de Aula Teoria da computação

⟨𝑀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 𝑤.

Page 62: Notas de Aula Teoria da computação

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

Page 63: Notas de Aula Teoria da computação

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

Page 64: Notas de Aula Teoria da computação

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.

Page 65: Notas de Aula Teoria da computação

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.

Page 66: Notas de Aula Teoria da computação

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}.

Page 67: Notas de Aula Teoria da computação

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

Page 68: Notas de Aula Teoria da computação

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:

Page 69: Notas de Aula Teoria da computação

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).

Page 70: Notas de Aula Teoria da computação

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:

Page 71: Notas de Aula Teoria da computação

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}

Page 72: Notas de Aula Teoria da computação

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;

Page 73: Notas de Aula Teoria da computação

∙ 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.

Page 74: Notas de Aula Teoria da computação

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 𝑛𝑘;

Page 75: Notas de Aula Teoria da computação

(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.

Page 76: Notas de Aula Teoria da computação

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.

Page 77: Notas de Aula Teoria da computação

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.

Page 78: Notas de Aula Teoria da computaçã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.

Page 79: Notas de Aula Teoria da computação

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

Page 80: Notas de Aula Teoria da computação

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).

Page 81: Notas de Aula Teoria da computação

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

Page 82: Notas de Aula Teoria da computação

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

Page 83: Notas de Aula Teoria da computação

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