Você está na página 1de 7

1

Complexidade Computacional
Nossos estudos de processos computacionais levaram-nos a classificar problemas em duas grandes
categorias: os problemas solúveis e os problemas insolúveis. Nesta sessão concentramo-nos na classe
dos problemas solúveis. Nossa meta é estudar as soluções dos problemas de uma forma prática. Em
resumo, veremos que apesar de solúveis em tese, muitos desses problemas requerem uma quantidade
de recursos tão grande (tempo e espaço) que permanecem insolúveis na prática. Vamos estabelecer
uma escala de medida onde possamos classificar os problemas de acordo com sua complexidade. Para
atender nossa meta de medir a complexidade de problemas, devemos então aprender a medir a
complexidade de computações individuais, para podermos determinar a complexidade dos problemas.

Consideremos o conceito de complexidade em si: apesar de termos uma noção intuitiva do que é
complexidade, necessitamos uma definição precisa para nossa investigação. Uma aproximação será a
medida indireta por meio dos recursos exigidos para sua execução. Um dos recursos usados nesse
contexto é o tempo. Consideramos uma computação mais complexa que outra se sua execução toma
mais tempo que a outra. Designemos esse tempo de complexidade de tempo. Outro recurso também
usado para medir complexidade é o espaço para armazenar requerido. Isso está baseado em assumir
uma maior dificuldade de uma computação em relação à outra pela quantidade de espaço exigida.
Designemos esse espaço de complexidade de espaço.

Não é difícil concluir que a complexidade de tempo ou de espaço poderá variar dependendo do
sistema em que a computação está sendo executada. Para remover tais variações, estudaremos as
complexidades no contexto de um só sistema computacional: a máquina de Turing. Referimo-nos a
execução de uma transição simples em uma máquina de Turing (para realizar uma escrita ou um
movimento), como um passo na computação da máquina e definindo a complexidade de tempo como o
número de passos executados durante a computação.
Por exemplo, considere uma máquina de Turing como a apresentada na figura abaixo:

Temos no algoritmo abaixo, a descrição da operação da MT M1:


M1 : “entrada = uma cadeia w ∈ {x}*.
1. inicia processo com movimento para direita sob leitura “∆” no início da fita.
2. enquanto ler “x”, movimenta-se uma célula para direita.
3. sob leitura de “∆” escreve “x” na célula atual.
4. enquanto ler “x”, movimenta-se uma célula para esquerda.”

Não anotamos a última transição do diagrama pois ela apenas para sinaliza o final do processo no
estado final. A complexidade de tempo dessa máquina de Turing quando iniciada com a configuração
de fita: ∆xxx∆∆... seria 9, pois executa 4 passos para mover-se até o primeiro símbolo branco
à direita, 1 para escrever x e outros 4 passos para retornar a posição inicial. E, se a configuração
inicial da fita fosse tudo em branco: ∆∆∆... teria complexidade de tempo igual a 3.

A complexidade de espaço de uma máquina de Turing é definida como o número de células na fita
requeridas para a computação. Para a máquina da figura acima, quando iniciada com a configuração de
fita ∆xxx∆∆... é 5: ocupa 4 células antes do processamento (incluindo o cursor na célula que
antecede a cadeia de entrada) e ao final temos: ∆xxxx∆∆∆... Para a segunda configuração ∆∆∆... a
complexidade de espaço será 2 pois somente duas células são empregadas ficando ao final do
processamento: ∆x∆∆...
2
Complexidade Computacional
As complexidades não são totalmente independentes: se a complexidade de tempo de uma máquina
de Turing é n, a complexidade de espaço daquela computação não ocupará mais do que n+1 células.
Definida a complexidade das computações passamos a complexidade dos algoritmos: e aqui sabemos
que diferentes aplicações de um mesmo algoritmo poderão produzir diferentes computações. Para os
algoritmos, vamos endereçar a questão em termos da complexidade de tempo.

Complexidade de tempo das máquinas de Turing

Consideremos uma MT M2 que realiza a comparação de duas cadeias de igual comprimento, por
exemplo: u, v ∈ {x, y, z}*. Assumimos que essas cadeias estão escritas na fita da máquina uma após a
outra, com um símbolo * separando as duas, por exemplo: ∆yxxz*yxzx∆∆... A máquina irá decidir se
as duas são iguais ou não parando com a configuração ∆S∆∆∆... se sim e com a configuração
∆N∆∆∆... se não, como podemos conferir na descrição abaixo:

M2 : “entrada = cadeias u, v ∈ {x, y, z}*.


0. move-se para direita
1. enquanto o símbolo lido for diferente de “*” faça
2. armazena símbolo lido e escreve # no lugar, move-se para direita.
3. enquanto não encontra o separador “*” movimenta-se para a direita.
4. enquanto não encontra símbolo diferente de “#” movimenta-se para a direita.
5. se o símbolo lido for igual ao armazenado no passo 2.
6. então escreve # no lugar, move-se para direita.
7. enquanto não encontra símbolo diferente de “*” movimenta-se para a esquerda.
8. enquanto não encontra símbolo diferente de “#” movimenta-se para a esquerda.
9. senão enquanto não encontra “∆” movimenta-se para a direita.
10. move para esquerda
11. enquanto símbolo for diferente de ∆, escreve ∆ (apaga fita)
12. move-se para direita, escreve “N” e move-se para esquerda.. (fim: u ≠ v)
13. enquanto não encontra “∆” movimenta-se para a direita.
14. move para esquerda
15. enquanto símbolo for diferente de ∆, escreve ∆ (apaga fita)
16. move-se para direita, escreve “S” e move para esquerda.” (fim: u = v)

O tempo requerido para executar essa comparação tende a uma função do comprimento da entrada,
no exemplo: o tempo de processamento de M2 será dado por 2n2 + 10n + 9, que inclui 2n2 + 5n + 1
passos para completar a comparação em si e 5n + 4 passos para mover-se da direita para a esquerda
e apagar a fita, 3 passos para escrever S e finalmente o último passo para mover para o estado de
parada (o movimento para esquerda). Como sabemos da análise de algoritmos, costumamos
considerar o pior caso na execução desses algoritmos para verificar seu desempenho.

A complexidade de problemas
A complexidade de tempo de um problema é a complexidade da solução mais simples do problema.
Infelizmente, temos que o maior obstáculo em estabelecer a complexidade de um problema é
justamente encontrar uma solução ótima para ele. Retornando ao exemplo anterior da MT e o
problema de comparação de cadeias em {x, y, z}* de mesmo comprimento. A solução apresentada no
algoritmo de M2 não é ótima, e uma solução mais rápida está representada na MT M3 a seguir, onde a
máquina compara cadeias interrogando duas entradas na primeira cadeia antes de mover a cabeça
para a outra cadeia e investigar suas posições correspondentes.
3
Complexidade Computacional
M3 : “entrada = cadeias u, v ∈ {x, y, z}*.
0. move-se para direita
1. enquanto o símbolo lido for diferente de “*” faça
2. armazena 1º símbolo lido e escreve # no lugar, move-ser para direita.
3. se o símbolo lido é diferente de “*”
4. então armazena 2º símbolo lido e escreve # no lugar.
5. enquanto não encontra o separador “*” movimenta-se para a direita.
6. enquanto não encontra símbolo diferente de “#” movimenta-se para a direita.
7. senão executa a partir do passo 13.
8. se o 1º símbolo lido for igual ao armazenado no passo 2.
9. então escreve # no lugar, move-se para direita.
10. se o 2º símbolo lido for igual ao armazenado no passo 4.
11. então escreve # no lugar, move-se para direita.
12. enquanto não encontra símbolo diferente de “*” move-se para a esquerda.
13. enquanto não encontra símbolo diferente de “#” move-se para a esquerda.
14. senão executa a partir do passo 16.
15. senão executa a partir do passo 16.
16. enquanto não encontra “∆” movimenta-se para a direita.
17. move para esquerda
18. enquanto símbolo for diferente de ∆, escreve ∆ (apaga fita)
19. move-se para direita, escreve “N” e move-se para esquerda.. (fim: u ≠ v)
20. enquanto não encontra “∆” movimenta-se para a direita.
21. move para esquerda
22. enquanto símbolo for diferente de ∆, escreve ∆ (apaga fita)
23. move-se para direita, escreve “S” e move para esquerda.” (fim: u = v)

Para a máquina M3 a complexidade corresponde a função n2 + 7n + 8 melhorando um pouco a solução


do problema como vemos nos histogramas comparativos entra as duas MT’s.

Nos dois mecanismos apresentados a taxa de crescimento da complexidade é a mesma, ou seja, para
uma entrada n crescendo temos o mesmo comportamento assintótico: f(n) = O(n2).

A análise desses algoritmos nos dá informação que esse problema, a comparação de duas cadeias de
símbolos, teria um limite de tempo para sua solução de ordem quadrática. Entretanto, podemos
encontrar uma solução linear modificando o modelo de máquina de Turing, ao empregar uma máquina
com 2 fitas como descrevemos M4 a seguir.
4
Complexidade Computacional
M4 : “entrada = cadeias u, v ∈ {x, y, z}*.
1. enquanto não encontra o separador “*” movimenta-se para a direita na fita-1.
2. enquanto o símbolo na fita-1 lido for diferente de “∆” faça (copia v na fita-2)
3. move-se para direita na fita-2.
4. move-se para direita na fita-1.
5. se o símbolo lido na fita-1 for diferente de “∆”.
6. então escreve esse símbolo na fita-2 e escreve “∆” na fita-1. (apaga v na fita-1)
7. senão executa a partir do passo 8.
8. enquanto não encontra o separador “*” na fita-1 movimenta-se para a esquerda na mesma.
9. enquanto o símbolo lido na fita-1 for diferente de “∆” faça
10. escreva “∆” na fita-1.
11. move-se para esquerda na fita-1.
12. move-se para esquerda na fita-2.
13. se o símbolo lido na fita-1 for igual ao símbolo lido na fita-2
14. então executa a partir do passo 10.
15. senão enquanto o símbolo lido na fita-1 for diferente de “∆” faça
16. escreve “∆” na fita-1. (apaga a fita-1)
17. move-se para esquerda na fita-1.
18. move-se para direita na fita-1,
19. escreve “N” na fita-1
20. move-se para esquerda na fita-1 (fim: u ≠ v)
21. move-se para esquerda na fita-2.
22. se o símbolo lido na fita-2 for “∆”
23. então move-se para direita na fita-1.
24. escreve “S” na fita-1.
25. move-se para esquerda na fita-1.” (fim: u = v)

Com a cópia da segunda cadeia na fita-2, a comparação é realizada em uma só passada pelas duas
cadeias da direita para a esquerda. O tempo requerido para executar essa comparação tende a uma
função em n (comprimento da entrada): f(n) = 9n + 11, no pior caso – onde as duas cadeias são iguais.
Note que um modelo diferente de MT levou a um novo comportamento assintótico, no exemplo M4
tem consumo de tempo linear, e isto nos leva a uma reflexão: o que devemos considerar para definir
a complexidade de tempo?
Em primeiro lugar devemos lembrar que entre os modelos equivalentes à máquina de Turing original,
com uma só fita, como as MT’s determinísticas com múltiplas fitas ou as MT’s não-determinísticas
tem um tempo de processamento menor, na seguinte proporção:
• MT determinística de múltiplas fitas com tempo f(n) = O(n) terá um processamento em tempo
proporcional a O(n2) para a MT determinística simples;
• MT não-determinística com tempo f(n) = O(n) terá um processamento numa MT determinística
simples em tempo proporcional a O(2n).
Outra consideração a fazer é que entre modelos equivalentes, no caso as máquinas determinísticas, a
variação de tempos não será tão acentuada, podemos afirmar que é polinomial, quando comparado
com o modelo não-deterministico, no caso exponencial.
Nossas medições de complexidade de tempo deverão, portanto estar associadas ao modelo inicial: a
máquina de Turing determinística com uma só fita. Ficando as aplicações com os demais modelos para
apresentar soluções mais eficientes como vimos nos exemplos com as MT’s M2, M3 e M4.
Reconhecimento de linguagens
Mais que o aspecto da computabilidade, estamos interessados em saber se o processamento de uma
linguagem é factível sob o ponto de vista prático. Suponha uma máquina de Turing M que computa a
função parcial f: Σ*1 → Σ*2. Dizemos que M computa a função em um tempo polinomial se há um
5
Complexidade Computacional
polinômio p(x) tal que para cada w ∈ Σ*1 para o qual f(w) é definida, M computa f em p(|w|) passos.
Uma propriedade importante das funções que podem ser computadas por MT’s em um tempo
polinomial é que a composição de duas funções também é computada em um tempo polinomial, por
exemplo: sejam as funções parciais f1 e f2 computadas pelas máquinas M1 e M2 respectivamente.
Suponha as expressões p1(x) e p2(x) tais que para as entradas v e w, M1 computa f1(v) em p1(|v|)
passos e M2 computa f2(w) em p2(|w|) passos. Agora considere o tempo requerido para a máquina
composta → M1M2 para computar a função f2 ° f1. Dada entrada v, M1 passará o controle para M2
após executar p1(|v|) passos. Portanto a cadeia produzida por M1 é apresentada a M2 como entrada
com comprimento não maior que p1(|v|) + 1. Assumimos que |v| < p1(|v|) para todas as entradas v de
f1. Por sua vez M2 parará após executar p2(p1(|v|) + 1) passos. Portanto, a computação inteira
realizada na máquina composta → M1M2 requer p1(|v|) + p2(p1(|v|) + 1) passos, que é uma expressão
polinomial em |v|.

Classificando a complexidade
Considere a coleção de todos os algoritmos que resolvem certo problema X: o interesse, nesse
momento, é conhecer se na coleção existe algum eficiente, isto é, de complexidade de tempo
polinomial. Se existir tal algoritmo, o problema X será denominado “tratável” e “intratável” caso
contrário. A idéia seria que um problema tratável pudesse sempre ser resolvido, para entradas e
saídas de tamanho razoável, através de algum processo automático, por exemplo: um computador.
Enquanto isso, um algoritmo de complexidade não polinomial, de algum problema intratável, poderia
levar séculos para computar dados de entrada e saída de tamanhos reduzidos.
De acordo com a definição, um problema seria classificado como tratável, exibindo-se algum
algoritmo de complexidade polinomial, que o resolvesse. Por outro lado, para verificar que é
intratável, há necessidade de se provar que todo algoritmo que o resolva não possui complexidade
polinomial. Conforme veremos, existe uma classe de problemas para os quais todos os algoritmos
conhecidos são de complexidade exponencial, mas por outro lado, não se conhecem provas, até o
momento, de que o fato seja comum a todos os possíveis algoritmos para esses problemas. De um
modo geral, nosso estudo sobre a questão da tratabilidade de problemas ficará restrita a uma classe
especial: os problemas de decisão. De forma introdutória, vamos apresentar uma classificação para
problemas computacionais baseada em sua complexidade de tempo:
1. Problemas que podem ser resolvidos por algoritmos de complexidade linear, isto é, O(n), se n é o
tamanho das instâncias.
2. Problemas que podem ser resolvidos por algoritmos de complexidade não linear, mas polinomial,
isto é, O(p(n)) onde p(n) é um polinômio de grau maior do que um.
3. Problemas que aparentemente não possuem complexidade intrínseca polinomial, mas não se
conhecem algoritmos de complexidade polinomial.
4. Problemas que intrinsecamente requerem, para sua solução, um número de operações igual a uma
função exponencial no tamanho das instâncias, por que eles exigem a geração de um número
exponencial de subproblemas.
A classificação de problemas nas três primeiras classes depende do “estado da arte”. Um exemplo,
até 1971 o problema de se verificar a planaridade de um grafo pertencia à classe 2, mas a
descoberta de um novo algoritmo deslocou-o para a classe 1. Da mesma maneira, é possível que
algoritmos de complexidade polinomial sejam descobertos na classe 3, deslocando-os para a classe 2.
Os problemas de determinação de árvore espalhada mínima, caminho de custo mínimo, entre outros,
pertencem atualmente, à classe 2. Essa classe é denominada P, em abreviação de polinomial, que por
sinal foi a classe que abrigou a maioria de nossos estudos até aqui. Na classe 4 temos problemas
como a geração de todas as árvores espalhadas de um grafo onde ainda não temos evidências de
descoberta de novos algoritmos que possam deslocar tais problemas para uma das duas primeiras
classes: são intrinsecamente exponenciais no tamanho das instâncias. E por último a classe 3, dos
problemas NP, constitui-se um enigma e é o objeto de nossos estudos nessa segunda parte do curso.
6
Complexidade Computacional
Problemas de decisão
De um modo geral, um problema algorítmico pode ser caracterizado por um conjunto de todos os
possíveis dados do problema e, uma questão solicitada, denominada objetivo do problema. Resolver o
problema algorítmico consiste em desenvolver um algoritmo, cuja entrada são os dados específicos
retirados desse conjunto e cuja saída, denominada solução, responda ao objetivo do problema. Os
dados específicos que constituem uma entrada formam uma instância do problema. Isto é, um
problema possui tantas instâncias diferentes, quantas as variações possíveis de seus dados. Como
exemplo considere o problema “elaborar um algoritmo para encontrar uma clique de tamanho ≥ k num
grafo G”. Lembrando que uma clique de G, de tamanho k, é um sub-grafo completo de G, com k
vértices; pode ser colocado no seguinte formato:
Dados: um grafo G e um inteiro k > 0;
Objetivo: encontrar em G uma clique de tamanho ≥ k, se existir
Assim, o conjunto de dados do problema algorítmico acima consiste no conjunto de todos os pares
(G, k), onde G é um grafo arbitrário e k um inteiro positivo arbitrário. Um par específico (G, k)
constitui uma instância do problema. Um subgrafo específico de G com k ou mais vértices, se
existir, é uma solução do problema.
Supõe-se que cada instância do problema seja apresentada ao algoritmo, segundo uma codificação
conveniente. O comprimento total dessa codificação constitui o tamanho da entrada do algoritmo.
Esse tamanho é o parâmetro empregado para as medidas de complexidade. Por esse motivo, são
inaceitáveis as codificações de instâncias que sejam desnecessariamente longas. De um modo geral,
esta se torna desnecessariamente longa quando:
(i) contém partes irrelevantes ao problema, ou
(ii) um certo inteiro p da instância está codificado no sistema unário (isto é, no sistema de
numeração de base 1, no qual cada inteiro p é codificado por p 1’s consecutivos).
Essa última condição exprime que são aceitáveis números na base 2, por exemplo. Naturalmente, a
proporção que a base cresce, o tamanho da codificação decresce. Contudo, a relação entre os
tamanhos das codificações de um mesmo número nas bases b1 e b2, respectivamente, pode ser
expressa por um polinômio, quando b1, b2 ≥ 2. Enquanto isso, a relação é exponencial quando as bases
são 2 e 1, o que justifica a condição (ii). A justificativa para (i) é óbvia.
Como exemplo, seja o problema acima de encontrar no grafo G uma clique de tamanho k > 0. Uma
possível instância do problema é o grafo da figura a seguir, o número 3. Seja G caracterizado pelo
conjunto de arestas:

Uma codificação aceitável par a entrada seria, por exemplo: /1,10/1,11/1,100,10,11/11,100//11//


Nessa codificação cada aresta (v, w) é codificada como /bv, bw/, onde bv, bw são respectivamente as
representações binárias dos vértices v e w. O inteiro k aparece como //bk//, sendo bk a
representação binária de k. O tamanho dessa instância é 35.
Há certas classes gerais de problemas algorítmicos. Por exemplo, existem os chamados problemas de
decisão, os de localização e os de otimização. Num problema de decisão, o objetivo consiste em
decidir a resposta SIM ou NÃO a uma questão. Num problema de localização, o objetivo é localizar
uma estrutura S que satisfaça um conjunto de propriedades dadas. Se as propriedades a que S deve
satisfazer envolverem critérios C de otimização, então o problema torna-se de otimização. Observe
que é possível se formular um problema de decisão cujo objetivo é indagar se existe ou não a
mencionada estrutura S, satisfazendo às propriedades dadas. Isto é, existem triplas de problemas,
7
Complexidade Computacional
um de decisão, outro de localização e outro de otimização que podem ser associados entre si,
respectivamente.
Problema de decisão: existe estrutura S que satisfaça propriedades P ?

Problema de localização: encontrar estrutura S que satisfaça propriedade P


Problema de otimização: encontrar estrutura S que satisfaça critérios de otimização

Como exemplo, considere o problema do caixeiro viajante. Seja um grafo completo, tal que cada
aresta e possui um peso c(e) 0. Um percurso de caixeiro viajante é simplesmente um ciclo
hamiltoniano de G. O peso de um percurso é a soma dos pesos das arestas que o formam. Um
percurso de caixeiro viajante ótimo é aquele cujo peso é mínimo.
Por exemplo, no grafo da figura a seguir os valores dos pesos estão indicados junto às arestas. Um
percurso de caixeiro é por exemplo: a, b, c, d, a com peso igual a 16. Enquanto em um percurso ótimo
temos: a, b, d, c, a de peso igual a 11.

Os seguintes problemas associados podem ser formulados:

problema de decisão
dados: um grafo G e um inteiro k > 0
objetivo: verificar se g possui um percurso de caixeiro de peso ≤ k
problema de localização
dados: um grafo G e um inteiro k > 0
objetivo: localizar em G, um percurso de caixeiro de peso ≤ k
problema de otimização
dados: um grafo G
objetivo: localizar em G, um percurso de caixeiro ótimo

Os três problemas de caixeiro viajante, estão obviamente relacionados. Suponha que o problema de
otimização respectivo seja resolvido e denominado por Q o percurso ótimo encontrado. Então q pode
ser utilizado para resolver o problema de localização associado, da forma; seja C(Q) o peso do
percurso Q. Logo C(Q), pode ser obtido facilmente (somando os pesos das arestas) de Q. Então se
C(Q) ≤ k, Q é também uma solução para o problema de localização. Caso contrário, C(Q) > k e não
existe em G, percurso de caixeiro viajante de peso ≤ k. Obviamente isso resolve também o
problema de decisão associado. Então, para os citados problemas de caixeiro, o problema de decisão
é de dificuldade não maior que o de localização, e este de dificuldade não maior que o de otimização.
Essa característica é compartilhada por outros problemas. Aliás, de certa forma, é natural que
assim o seja. Contudo é bastante menos intuitivo que, também em diversos casos, os problemas de
otimização e localização apresentam ambos, dificuldade não maior do que o de decisão associado.
Os problemas em discussão nessa etapa serão, em sua maioria, problemas de decisão. Uma
justificativa para a escolha é que, em geral, o problema de decisão é o mais simples dentre os três
associados. Por isso, alguma prova de sua possível intratabilidade pode se estendida aos outros
casos. A notação Χ(D, Q) poderá ser utilizada para representar um problema de decisão Χ. D
representa o conjunto de dados de entrada e Q o objetivo de Χ. O objetivo será denominado decisão
e, conforme mencionado, consta de uma questão a ser respondida com SIM ou NÃO.

Você também pode gostar