Professional Documents
Culture Documents
1
CONTEÚDO
0. INTRODUÇÃO
1. RECURSÃO
1.1 Conceitos básicos
1.2 Problemas clássicos
1.3 Análise da Recursão
1.4 Exercícios Propostos
2. BACKTRACKING
2.1 Conceitos básicos
2.2 Problemas clássicos
2.3 Jogos
2.4 Exercícios Propostos
3. PROGRAMAÇÃO DINÂMICA
3.1 Conceitos básicos
3.2 Problemas clássicos
3.3 Exercícios Propostos
4. MÉTODO GULOSO
4.1 Conceitos básicos
4.2 Problemas clássicos
4.3 Exercícios Propostos
5. PROBLEMAS NP-COMPLETOS
2
0. INTRODUÇÃO
3
executada do algoritmo e determina-se uma função t(n), que dá a variação do
tempo de execução em função de n, o tamanho da entrada.
Exs:
4
0.2 Bibliografia recomendada
5
1. RECURSÃO
Fibonacci(p): (A1.2)
Início:
Se (p ≤ 1) Então
Retornar p
Senão
Retornar Fibonacci(p-1) + Fibonacci(p-2);
Fim;
6
Há quatro outras visões sobre a técnica de recursão, que certamente
complementam essa visão inicial:
a) A técnica pode ser vista como uma maneira de se resolver problemas
de "trás para frente", isto é: a solução enfatiza os passos finais para a
solução do problema, supondo problemas menores resolvidos. No caso Fatorial,
para se obter Fatorial(n), a idéia é multiplicar Fatorial (n-1) por n .
Note que todo algoritmo recursivo tem uma solução não recursiva
equivalente. Muitas vezes, entretanto, a expressão recursiva é mais natural e
mais clara, como para os exemplos mostrados a seguir.
7
1.2 Problemas clássicos
A B C
8
Mover 1 de A p/ C; → Mover 2 de A p/ B; → Mover 1 de C para B;
(Resolvido o problema para 2 pratos em B);
Mover 3 de A para C;
(Resolver novamente o problema de 2 pratos, só que p/ C):
Mover 1 de B p/ A; → Mover 2 de B p/ C; → Mover 1 de A p/ C;
1.2.2 Quicksort
É considerado o melhor algoritmo de ordenação e foi um dos primeiros
algoritmos recursivos propostos. A idéia é fazer, sucessivamente, partições em
subvetores, de forma que a parte esquerda contenha sempre elementos
menores ou iguais aos da direita. O problema simples é quando o tamanho de
uma partição é 1. A partição baseia-se em escolher um elemento como um pivô,
fazendo-se trocas para colocar maiores ou iguais de um lado e menores ou
iguais do outro.
Passo 1 2 3 4 5 6 7 8 9 10 11 12
9
E X E M P L O F A C I L
E X E M P L O F A C I L Partição (1,12)
1 E L E I C A F O L P M X (1,7) (8,12)
E L E I C A F Partição (1, 7)
2 E F E A C I L (1,5) (6,7)
E F E A C Partição(1,5)
3 C A E F E (1,2) (3,3) (4,5)
C A Partição (1,2)
4 A C (1,1) (2,2)
F E Partição(4,5)
5 E F (4,4) (5,5)
I L Partição(6,7)
6 I L (6,6) (7,7)
O L P M X Partição(8,12)
7 O L M P X (8,10) (11,12)
O L M Partição(8,10)
8 L O M (8,8) (9,10)
O M Partição(9,10)
9 M O (9,9) (10,10)
P X Partição(11,12)
10 P X (11,11) (12,12)
A C E E F I L L M O P X Situação Final
a) Análise do Algoritmo
a.1) Complexidade:
Melhor caso = Vetor Ordenado; NC =~nlog2n = O(n log2n)
Pior caso = Há várias possibilidades. Vetor em “Zig Zag”, p.
Ex. NC =~ n2/2 = O(n2)
Caso Médio: NC =~ nlog2n = O(n log2n)
a.2) Estabilidade: Algoritmo não estável
a.3) Situações Especiais: Algoritmo de uso amplo, extremamente
rápido.
a.4) Memória necessária: pilha de recursão (log2n).
b) Observações:
10
Número de trocas: 16
b.2) Este é um dos mais antigos e estudados algoritmos na
Informática, tendo sido desenvolvido inicialmente por Hoare, em 1962.
b.3) Notar o mecanismo de partição (1,5), (1,2) e (11,12). No
primeiro caso, o subvetor é dividido em 3 partes (ao final J = 2, I = 4); no
segundo caso, o vetor é subdividido em 2 partes (ao final J = 1, I = 2); no
terceiro caso, o vetor também é subdvidido em 2 partes (mas ao final J = 10
(!?) e I = 12).
11
iniciais. O elemento perdedor vai para o vetor de mínimos e o vencedos, para o
de máximos. Verifica-se, então, o menor dos mínimos e o maior dos máximos. O
total de comparações, é então:
n/2 + n/2 -1 + n/2 - 1 ≅ 3n/2 - 2.
Comb(n,p) = Comb(n,p-1)*(n-p+1)/p,
12
Fim;
1.2.5 Torneio
Um problema de organização de torneios com n competidores, onde
todos competidores jogam entre sí, é planejar as rodadas de forma que haja o
menor número delas. Quando n é par, queremos que haja (n - 1) rodadas, e em
cada rodada todos os times jogam. Quando n é impar então há n rodadas, sendo
que em cada rodada jogam (n - 1) times e um deles fica "bye". Neste último
caso, pode-se considerar que exista mais um time no grupo, que será
considerado o emparelhamento "bye", de forma que admitiremos que haja
sempre um número n par de competidores.
Uma solução para o problema pode ser construir uma matriz R, n x n,
onde a primeira coluna da matriz contenha os times e as colunas 2 a n, indiquem
as rodadas de 1 a (n - 1). Cada elemento R(i, j) da matriz indica que R(i, j) é o
adversário do time i na rodada j - 1. Vejamos um exemplo para n = 4.
r1 r2 r3 r4
1 2 3 4
2 1 4 3
3 4 1 2
4 3 2 1
a) R[i,1] ← i
b) Todas as linhas são permutações dos elementos 1,2...n
c) Todas as colunas são permutações dos elementos 1, 2...n
d) Se j > 1, R[i,j] = t ⇒ R[t,j] = i
r1 r2 r3 r4 r5 r6 r7 r8
1 2 3 4 5 6 7 8
13
2 1 4 3 6 5 8 7
3 4 1 2 7 8 5 6
4 3 2 1 8 7 6 5
5 6 7 8 1 2 3 4
6 5 8 7 2 1 4 3
7 8 5 6 3 4 1 2
8 7 6 5 4 3 2 1
I III
II IV
A recursão é a seguinte:
A matriz é dividida em 4 quadrantes iguais. O quadrantes I é preendhido
recursivamente. Os demais quadrantes são assim obtidos:
a) O quadrante II é copiado do quadrante I, com o acréscimo de n/2 a
cada elemento.
b) O quadrante III é uma cópia do II.
c) O quadrante IV é uma cópia do I.
A recursão se encerra quando se chega ao tamanho 1. Então é preenchido com
o número 1.
Notar que essa solução preserva as propriedades necessárias à matriz.
14
Se (m = 1) Então
R[1,1] ← 1;
Senão
p ← m/2;
Torneio(p);
Para i de 1 até p:
Para j de 1 até p:
R[i + p, j] ← R[i, j] + p;
R[i, j + p] ← R[i + p, j];
R[i + p, j + p] ← R[i, j];
Fp;
Fp;
Fim;
r1 r2 r3 r4 r5 r6 r7 r8
1 2 3 4 5 6 7 8
15
2 1 4 3 6 7 8 5
3 4 1 2 7 8 5 6
4 3 2 1 8 5 6 7
5 6 7 8 1 4 3 2
6 5 8 7 2 1 4 3
7 8 5 6 3 2 1 4
8 7 6 5 4 3 2 1
16
Torneio (m): (A1.8)
Início:
Se (m = 1) Então R[1, 1] ← 1
Senão Se (m ímpar) Então
Torneio (m+1);
Se (m = n) Então
Considera o emparelhamento com (m+1) como “bye”;
Senão
p ← m/2;
Torneio (p);
Se (p ímpar) Então q ← p + 1
Senão q ← p;
Para i de 1 a p
Para j de 1 a q:
R[i + p, j] ← R[i, j] + p;
Se (R[i + p, j] = m) Então R[i + p, 0] ← j;
Fp;
Para j de 1 até p:
R[i, j + q] ← p + 1 + (i + j – 2) mod p;
R[p + 1 + (i + j – 2) mod p, j + q] ← i;
Fp;
Fp;
Se (p ímpar) Então
Para i de 1 a ma:
R[i, R[i, 0] ] ← R[i, q + 1];
Para j de (q + 1) a (q + p):
R[i, j] ← R[i, j+1];
Fp;
Fp;
Fim;
17
c) Preenche-se os quadrantes II, III e IV conforme a segunda solução
mostrada anteriormente para potências de 2, ou seja: o quadrante II
é uma cópia do quadrante I, adicionando-se n/2 a todos os elementos.
Caso o elemento seja “bye”, o correspondente também é tornado
“bye”. O quadrante III é preenchido com permutações circulares dos
números n/2+1 a n. O quadrante IV é preenchido de maneira forçada.
d) Quando n é da forma n = 4k+2, temos alguns emparelhamentos “bye”
indesejados nos quadrantes I e II, conforme descrito. Além disso a
solução contém n rodadas. Mas podemos eliminar a primeira coluna
dos quadrantes III e IV. Podemos observar que esta coluna contém
os números com a seguinte ordenação: n/2+1, n/2+2...n, 1, 2, ...n/2.
Cada par (n/2+i, i), contendo um elemento do quadrante III e outro
do quadrante IV, pode ser movido para a coluna do “bye” pois, pela
simetria envolvida, essa coluna é a mesma para o par. Assim,
consegue-se eliminar uma coluna e temos uma solução em n-1 rodadas,
que era o objetivo inicial.
r1 r2
1 2
2 1
r1 r2 r3 r4
18
1 2 3 -
2 1 - 3
3 - 1 2
r1 r2 r3 r4 r5 r6 r7
1 2 3 - 4 5 6
2 1 - 3 5 6 4
3 - 1 2 6 4 5
4 5 6 - 1 3 2
5 4 - 6 2 1 3
6 - 4 5 3 2 1
r1 r2 r3 r4 r5 r6 r7
1 2 3 4 5 6
2 1 5 3 6 4
3 6 1 2 4 5
4 5 6 1 3 2
5 4 2 6 1 3
6 3 4 5 2 1
r1 r2 r3 r4 r5 r6
1 2 3 4 5 6
2 1 5 3 6 4
3 6 1 2 4 5
4 5 6 1 3 2
5 4 2 6 1 3
6 3 4 5 2 1
19
Para obter a solução final (n = 5), elimina-se a linha 6 e transforma-se os
emparelhamentos com 6 para emparelhamentos “bye”, obtendo, então:
r1 r2 r3 r4 r5 r6
1 2 3 4 5 -
2 1 5 3 - 4
3 - 1 2 4 5
4 5 - 1 3 2
5 4 2 - 1 3
20
1.3.1 Balanceamento
Bubblesort(m); (A1.10)
Início:
Se (m > 1) Então
Para j de 1 a (m -1):
Se (Vet[j] > Vet[j+1]) Então
Troca(Vet[j], Vet[j+1]);
Fp;
Bubblesort(m -1);
Fim;
Chamada externa: Bubblesort(n);
21
Quando um procedimento recursivo divide um problema grande em mais
de um subproblema menor, a técnica costuma ser denominada Divisão e
Conquista.
...
1
n-1 n-2
T(0) = 0
T(1) = 1
22
T(n) = T(n-1) + T(n-2)
T(0) = 1
T(1) = 1
T(n) = 1 + T(n-1) + T(n-2) = 2*Fib(n+1) - 1.
1.3.3 Memoization
23
centavos fixando-se sempre a primeira moeda como uma moeda de 5 e
utilizando a solução para (n-5) com moedas de 1 e 5. De forma análoga para os
demais. A parcela T(6, n - 100) corresponde aos tipos de troco que sempre tem
uma moeda de 100, agregada às diversas possibilidades para trocos de
(n - 100) centavos usando todos os tipos de moedas possíveis. Para
completarmos a idéia recursiva, temos que estabelecer:
Não é preciso analisar muito para se perceber que essa formulação leva
a um algoritmo exponencial. Entretanto, se criarmos uma matriz T, m x n, onde
elemento da matriz tem o significado da recursão acima, podemos estabelecer
o algoritmo abaixo, que tabela os resultados necessários na matriz, de forma
que a recursão para cada par de valores só será chamada uma vez. O algoritmo
é o seguinte:
24
Moedas (q, p); (A 1.11)
Início:
Se (p < 0) Então k ← 0;
Senão Se (p = 0) Então k ← 1;
Senão Se (T[q, p] = 0) Então
k ← 0;
Para i de 1 a q:
k ← k + Moedas(i, p - V[i]);
Fp;
T[q, p] ← k;
Senão k ← T[q, p];
Retornar (k);
Fim;
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
2 1 2 3
3 2
4
5
6 6
25
1.4 Exercícios Propostos
Escrever algoritmos recursivos para os problemas descritos a seguir.
1.1 - (ALGOC)
Quer-se fazer a geração de constantes numéricas usando apenas as
seguintes operações:
ONE - gera o número 1
MONE - gera o número -1
DUP - duplica o valor
ADD - soma 1 ao valor
(Maratona ACM - 1998 - América do Sul)
26
1.10 - (Desenho de aproximação de segmento)
Desenhar uma aproximação do segmento de linha que une dois pontos de
coordenadas (x1, x2) e (y1, y2) usando subsegmentos apenas de coordenadas
inteiras.
1.13 - (Fractais)
Pesquisar algoritmos para desenhos de fractais. (Ver "Algorithms +
Data Structures = Programs", de N. Wirth, Prentice_Hall, 1976.
1.14 - (Torneio)
Fazer um algoritmo alternativo para Torneio, quando n é um quadrado
perfeito.
27
2. BACKTRACKING
Config(); (A2.1)
Início:
Para cada elemento envolvido, efetuar:
Se o elemento pode entrar na configuração, Então
Colocar o elemento na configuração;
Se a configuração tem a propriedade desejada Então
Imprimir a configuração;
Senão
Config();
Retirar o elemento da configuração;
Fim;
28
elementos inviáveis na configuração, melhor, pois menor aprofundamento é
necessário.
Perm; (A2.2)
Início:
Para i de 1 a n
Se (i ∉ P) Então:
P ← P + i;
Se (|P| = n) Então
Imprimir P;
Senão
Perm();
P ← P - i;
Fp;
Fim;
29
Perm; (A2.2)
Início:
Para i de 1 a n
Se (not S[i] ) Então:
np ← np + 1;
P[np] ← i;
S[I] ← True;
Se (np = n) Então
Imprimir P;
Senão
Perm;
np ← np - 1;
S[I] ← False;
Fp;
Fim;
1 2 3 4 5
.....................................................
2 3 4 5
.................................................
3 4 5
...............................
4 5
5
O número de folhas da árvore é 5! e cada caminho da raiz até uma das
folhas obtem uma permutação diferente.
De uma maneira geral, a árvore de recursão de backtracking tende a ser
uma árvore do mesmo tipo da árvore de recursão de Permutação, onde o
30
número de possibilidades é exponencial. Isto esclarece a observação feita
inicialmente de que é importante evitar possibilidades inviáveis, o mais cedo
possível. Em termos da árvore de recursão, isso significa "podar" todo um ramo
da árvore, sempre que se descobre uma inviabilidade em uma configuração
parcial, o que leva a uma maior eficiência do algoritmo. No ítem relativo aos
Jogos, algumas técnicas de "poda" serão explicadas.
31
2.2.1 Geração de combinações
As combinações t a t dos n elementos de um conjunto podem ser
apresentadas em ordem lexicográfica. Uma forma de fazer isso é modificar a
solução dada para a geração de permutações, colocando-se a restrição de que
cada novo elemento só entra na configuração, se for maior que o último que
entrou. Outra forma é utilizar um parâmetro que indica o valor mínimo dos
elementos que podem entrar na configuração. Esse algoritmo é o seguinte:
Comb(j); (A2.3)
Início:
Para i de j a n:
nc ← nc + 1; P[nc] ← i;
Se (nc = t) Então
Imprimir P;
Senão
Comb(i+1);
nc ← nc - 1;
Fp;
Fim;
32
2.2.2 Damas pacíficas
Este é um quebra-cabeças que utiliza os conceitos e o tabuleiro do jogo
de xadrez. O objetivo é colocar 8 damas no tabuleiro, de forma que elas não se
"ataquem", segundo as regras desse jogo. O exemplo abaixo ilustra uma das 92
possíveis soluções (das quais apenas 12 são soluções não simétricas):
1 ℜ
2 ℜ
3 ℜ
4 ℜ
5 ℜ
6 ℜ
7 ℜ
8 ℜ
1 2 3 4 5 6 7 8
Externamente:
Zerar TAB;
l ← 0;
Damas_pacificas();
2.2.3 Torneio
Podemos retornar ao problema de organização de torneios com n
competidores, discutido no ítem 1.2.5. O seguinte algoritmo resolve o
problema:
Torneio: (A2.5)
Início:
jog ← menor jogador não emparelhado na rodada;
rod ← (NE+1)/n
Para i de (jog+1) até n:
Se (Pode_emparelhar(jog, i, rod)) Então
NE ← NE+2;
R[jog, rod] ← i; R[i, rod] ← jog;
Se (NE = n2) Então
Imprime R;
Senão
Torneio;
R[jog, rod] ← 0; R[i, rod] ← 0;
NE ← NE - 2;
Fp;
Fim;
Externamente:
R[*,*] ← 0;
Para j de 1 até n: R[j,1] ← j; Fp;
NE ← n; Torneio();
34
c r1 r2 r3 r4 r5 r6 r7
1 2 3 4 5 6 8 7
2 1 4 3 8 7 5 6
3 4 1 2 6 5 7 8
4 3 2 1 7 8 6 5
5 6 7 8 1 3 2 4
6 5 8 7 3 1 4 2
7 8 5 6 4 2 3 1
8 7 6 5 2 4 1 3
35
Gera_subconjuntos (pi): (A2.6)
Início:
Para i de pi a n:
ns ← ns + 1; S[ ns] ← i;
Imprimir SubConj;
Se (i < n) Então
Gera_subconjuntos (i + 1);
ns ← ns - 1;
Fp;
Fim;
Externamente:
ns ← 0; Gera_subconjuntos(1);
36
Soma_subconjuntos(j): (A2.7)
Início:
i ← j;
Enquanto (i ≤ n) e ((soma + C[i]) ≤ s):
nr ← nr + 1; soma ← soma + C[i]; subConj[nr] ← i;
Se (soma = s) Então
Imprimir subConj;
Senão
Soma_subconjuntos(i+1);
subConj[nr] ← 0; soma ← soma - C[i]; nr ← nr - 1;
i ← i+1;
Fe;
Fim;
Externamente:
soma ← 0; nr ← 0;
Ordena C;
Soma_subconjuntos(1);
37
contendo um dos conjuntos nulos e o outro todos os elementos. Gradualmente
vai-se obtendo soluções cada vez melhores e cada melhoria é armazenada, em
substituição à melhor solução anteriormente. A melhor partição guardada é a
solução e ela é exibida. O algoritmo é mostrado a seguir.
Part_aproximada(pi): (A2.8)
Início:
i ← pi;
Enquanto (i ≤ n) e ((soma + C[i] ) < msmp):
nr ← nr + 1; soma ← soma + C[i]; Part[nr] ← i;
Se (abs(tot – 2*soma) < dif) Então
Guarda; dif ← abs(tot – 2*soma);
msmp ← max(soma, tot - soma);
Part_aproximada(i + 1);
Part[nr] ← 0; soma ← soma - C[i]; nr ← nr - 1;
i ← i+1;
Fe;
Fim;
Externamente:
soma ← 0; nr ← 0; tot ← ΣC[i]; dif ← tot; msmp ← tot;
Ordena C; Part_aproximada(1);
Imprime solução guardada;
38
2.3 Jogos
Uma das aplicações típicas da técnica de Backtracking é a programação
de jogos, cujo desenvolvimento permitiu a utilização do método em vários
outros tipos de problema. Considere jogos tais como xadrez, damas, jogo-da-
velha, onde há 2 jogadores. Os jogadores fazem jogadas alternadas e o estado
do jogo pode ser representado pela posição do tabuleiro.
o o
x o (1) O computador joga
x x
39
Uma técnica para analisar o jogo consiste em atribuir às folhas 1 quando
o computador ganha, 0 quando há empate ou -1 quando o oponente ganha. Esses
valores podem ser propagados árvore acima, através do critério denominado
MiniMax, que é o seguinte: cada nó pai recebe o valor máximo dos filhos quando
é uma posição de jogada do computador, e o valor mínimo quando é uma posição
de jogada do oponente.
Esses números refletem a melhor estratégia para cada competidor.
Quando for a vez do computador e o nó tiver valor 1, então há uma estratégia
vencedora, dada por um caminho de valor 1 em todos os nós .
Para jogos mais complicados, como o xadrez, por exemplo, a valoração
dos nós assume uma gama mais ampla, podendo ir de –999 a 999, por exemplo,
como será visto adiante.
O algoritmo para o jogo consiste, portanto, em se construir a árvore,
usando a ténica MiniMax e depois escolher a jogada usando a mesma.
O cálculo dos valores dos nós pode ser feito pelo algoritmo seguinte, que
basicamente é um percurso em pós-ordem na árvore de jogo, onde F
representa um nó da árvore e Modo tem valor Min, quando o nó é de jogada do
oponente ou Max, quando é de jogada do computador:
40
2.3.2 Poda Heurística
Alguns jogos, entretanto, têm um número de alternativas absurdamente
grande, o que inviabiliza a construção e exame de toda a árvore. As folhas
muitas vezes representarão apenas configurações parciais do jogo.
No caso do xadrez a ordem de grandeza do número de folhas é 35100. O
que se faz, então é a introdução de Heurísticas para avaliação de posições do
tabuleiro, somente aprofundando a análise de situações mais críticas e
aplicando o método MiniMax para a árvore "podada" obtida e considerando,
agora, que a valoração poderá variar numa gama mais ampla do que a vista para
o jogo da velha, já que não se pode mais aplicar o critério absoluto de posição
perdida/ganha/empatada, uma vez que a avaliação não parte dos nós finais.
A perícia de um jogo para xadrez reside, portanto, na qualidade das
heurísticas utilizadas. A avaliação heurística de uma configuração do tabuleiro
é empírica, levando em conta o balanço ponderado das peças presentes e o
valor de elementos estratégicos e táticos. O sucesso desse tipo de programa
evidencia que existem boas heurísticas para a situação. Entretanto fica
também evidente que nenhum programa é imbatível por um ser humano, visto
que o programa não joga de forma exata.
Quando se usa poda heurística, a cada jogada é construída uma nova
árvore, que só é usada para a decisão do próximo lance.
Max x
h1 (32)
41
De forma análoga à descrita, o exame de muitos trechos da árvore pode
ser abandonado, tornando sua avaliação mais rápida. Na realidade, a construção
da árvore pode ser simultânea à avaliação, o que significa que pode não ser
necessária a construção de toda a árvore do jogo.
Dessa forma a poda Alfa-beta pode tornar o jogo mais eficiente.
42
2.4 Exercícios Propostos
Escrever algoritmos para os problemas descritos a seguir, usando
backtracking:
2.4 - (Vasos)
Dados 3 vasos contendo água, com capacidades (c1, c2, c3), situação
inicial (s1, s2, s3), quer-se determinar qual o número mínimo de operações de
transferência, para se atingir o objetivo (o1, o2, o3) dado. Cada transferência
ou enche o vaso de destino ou esvazia o de origem e a quantidade de água é
mantida no processo. Observar que pode não haver solução. (Maratona ACM -
2000 - América do Sul)
2.5 – (Expressão)
Fazer um algoritmo para, dados n+1 números inteiros positivos, verificar
se é possível escrever o primeiro como uma combinação linear dos n restantes
(usando somas e subtrações, apenas).
Ex: para {13, 9, 5, 11, 20} é possível, pois 13 = 9 – 5 – 11 + 20; já para {3, 200,
150, 8, 15} não é possível.
43
2.6 - (Quadrado Mágico)
Apresentar um quadrado mágico para dado n. Um quadrado mágico é um
quadrado de lado n, divido em n2 células, preenchido com os números 1 a n2, tal
que a soma das linhas = soma das colunas = soma das diagonais principais.
44
2.12 - (Jogo da Último palito)
Implementar um programa para o Jogo do Último palito, onde dois
jogadores iniciam com uma pilha de 24 palitos e, alternadamente, cada um pode
retirar 1 a 3 palitos, vencendo o último a retirar palitos.
2.13 - (Resta 1)
Implementar um programa para o jogo do Resta 1.
2.14 - (Quebra-cabeças)
Descrever um algoritmo para solucionar o quebra-cabeças que consiste
de um tabuleiro de 4 x 4, com 15 peças contendo os números 1 a 15 e um uma
posição nula. É dada uma configuração inicial e quer-se a chegar à configuração
padrão onde os números estão ordenados no tabuleiro.
45
Fazer um algoritmo para gerar permutações, obtendo as permutações
para n elementos a partir daquelas para (n –1), colocando o n-ésimo elemento
em todas as n posições possíveis de cada permutação gerada com (n -1)
elementos.
Ex. No caso de n = 3 a ordem de geração seria: 321, 231, 213, 312, 132, 123.
46
3. PROGRAMAÇÃO DINÂMICA
47
P(i, j) = 1, se (i = 0 e j > 0) (a)
= 0, se (i > 0 e j = 0) (b)
= (P(i-1, j-1) + P(i-2, j) + P(i, j-2))/3, se (i > 1 e j > 1) (c)
= 1/2, se (i = 1 e j = 1) (d)
= 2/3 + P(1,j-2), se (i = 1 e j > 1) (e)
= P(i, j-2)/3, se (j = 1 e i > 1) (f)
48
P(i, j) = 1, se i ∈ {-1,0} e j > 0 (a)
= 0, se i > 0 e j ∈ {-1,0} (b)
= 1/2, se i = 0, j = 0 (c)
= (P(i-1, j-1) + P(i-2, j) + P(i, j-2))/3, se i ≥ 1 e j ≥ 1 (d)
i+j = 2
i i+j = 3
j
49
Podemos, também, retomar o problema Moedas, do Capítulo 1, que
consiste em se determinar o número de possibilidades de dar troco em moedas
para um valor n, expresso em centavos. Para esse problema, formulamos a
seguinte recursão:
O algoritmo é o seguinte:
Moedas(m, n); (A 3.2)
Início:
Para i de 1 até m: T[i, 0] ← 1; Fp;
Para i de 1 até m:
Para j de 1 até n:
tot ← 0;
Para k de 1 até i:
Se ((j - MO[k]) ≥ 0) Então tot ← tot + T[k, j - MO[k]];
Fp;
T[i, j] ← tot;
Fp;
Fp;
Fim;
50
Esse algoritmo preenche a tabela abaixo, por coluna, da esquerda para a
direita:
m\n 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
2 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5
3 1 1 1 1 1 2 2 2 2 2 4 4 4 4 4 6 6 6 6 6 9
4 1 1 1 1 1 2 2 2 2 2 4 4 4 4 4 6 6 6 6 6 9
5 1 1 1 1 1 2 2 2 2 2 4 4 4 4 4 6 6 6 6 6 9
6 1 1 1 1 1 2 2 2 2 2 4 4 4 4 4 6 6 6 6 6 9
51
Mochila(q, n): (A3.3)
Início:
Se (n = 0) Então
Retornar (V, F)
Senão Se (q = 0) Então
Retornar (F, F)
Senão:
(x, y) ← Mochila (q - 1, n);
Se (x) Então
Retornar (V, F)
Senão:
(x, y) ← Mochila (q -1, n - pq );
Se (x) Então
Retornar (V, V)
Senão
Retornar (F, F);
Fim;
Externamente:
Mochila(t, M);
A B C D E
vol= 7 vol=3 vol=5 vol=9 vol=15
52
Ítem/ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
vol
0 VF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
A (7) VF FF FF FF FF FF FF V FF FF FF FF FF FF FF FF FF FF FF FF FF
V
B (3) VF FF FF V FF FF FF VF FF FF V FF FF FF FF FF FF FF FF FF FF
V V
C (5) VF FF FF VF FF V FF VF V FF VF FF V FF FF V FF FF FF FF FF
V V V V
D (9) VF FF FF VF FF VF FF VF VF V VF FF VF FF V VF V V FF V FF
V V V V V
E (15) VF FF FF VF FF VF FF VF VF VF VF FF VF FF VF VF VF VF V VF V
V V
Algoritmo:
Mochila: (A3.4)
Início:
K[0, 0].x ← V; K[0, 0].y ← F
Para j de 1 a M:
K[0, j].x ← F; K[0, j].y ← F;
Fp;
Para i de 1 a t:
Para j de 0 a M:
Se (K[i - 1, j].x) Então
K[i, j].x ← V; K[i, j].y ← F;
Senão
Se (j ≥ V[i]) e (K[i - 1, j - P[i]].x) Então
K[i, j].x ← V; K[i, j].y ← V;
Senão
K[i, j].x ← F; K[i, j].y ← F;
Fp;
Fp;
Fim;
53
Pode-se verificar que o preenchimento dessa tabelas obedece
exatamente à idéia da recursão mostrada anteriormente. E esta nova
solução para o problema tem complexidade O(t.M).
Uma variante imediata do problema é encontrar a solução mais
próxima do objetivo procurado, que também é resolvida com a mesma
tabela e verificando, na última linha, o valor mais próximo do procurado.
Solução:
Início:
j ← M; i ← t;
Enquanto (j ≠ 0):
Enquanto (K[i, j].y ≠ ‘V’):
i ← i - 1;
Fe;
Escrever V[i]; j ← j – V[i]; i ← i - 1;
Fe;
Fim;
Ítem/ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
vol
0 0
A (7) 0 1
B (3) 0 2 1 2
C (5) 0 2 3 1 3 2 3 3
D (9) 0 2 3 1 3 4 2 3 4 3 4 4 4
E (15) 0 2 3 1 3 4 2 3 4 3 4 4 5 4 5
54
Na verdade basta usar um vetor, ao invés de uma matriz, para a busca
da solução para a Mochila. Esse vetor equivale à última linha da matriz. Seu
preenchimento tem uma sutileza, que é a de percorrer o vetor da direita para
a esquerda. Inicialmente o vetor é preenchido com -1. O algoritmo é o seguinte:
MochilaVetor;
Início
K[0] ← 0;
Para j de 1 a M: K[j] ← -1; Fp;
Para i de 1 a t:
Para j de M-P[i] descendo a 0:
Se (K[j+P[i]] = -1) e (K[j] ≥ 0) Então K[j+P[i]] ← i;
Fp;
Fp;
Fim;
Outra alternativa, ainda, é, na versão que usa matriz, fazer com que
cada linha da matriz indique a quantidade de itens na solução. Assim, a primeira
linha apresentaria as soluções com 1 elemento; a segunda, com dois elementos e
assim sucessivamente.
MochilaPesoValor;
Início:
K[0].me ← 0; K[0].vm ← 0;
Para j de 1 a M: K[j].me ← -1; K[j].vm ← 0; Fp;
Para i de 1 a t:
Para j de M-P[i] descendo a 0:
Se (K[j].me ≥ 0) e (K[j+P[i]].vm < (K[j].vm+V[i]) Então
K[j+P[i]].me ← i; K[j+P[i]].vm ← K[j].vm+V[i];
Fp;
Fp;
55
Fim;
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
0 -1 -1 2 -1 3 -1 1 3 4 2 -1 3 -1 4 3 4 4 5 4 5
0 0 0 6 0 11 0 10 17 12 16 0 21 0 23 27 22 29 21 28 26
Mochila(k): (A3.5)
Início:
Se (k = 0) Então Retornar 0
Senão
MaxVal ← 0; Maxi ← 0
Para i de 1 a n:
MaxAux ← vi + Mochila(k - pi)
Se (MaxVal < MaxAux) Então
MaxVal ← MaxAux;
Maxi ← i;
Marca Maxi;
Mochila(k - pMaxi);
Fim;
Externamente:
Mochila(M);
56
otimiza o problema, para cada peso ≤ M, iniciando com um tipo de ítem e
considerando, gradativamente, cada novo tipo. No exemplo abaixo,
consideramos 3 tipos:
A B C
peso = 5 peso = 8 peso = 11
Peso 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 2 21 2 2 2
0 2 3 4
TMel A A A A A A A A A A A A A A A A A A A A
VMax 6 6 6 6 6 12 12 12 12 12 18 18 18 18 18 2 2 2 2 2
4 4 4 4 4
Peso 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
TMel A A A B B A A A A A A A A A A A A A A A
VMax 6 6 6 7 7 12 12 12 13 13 18 18 18 19 19 24 24 24 25 25
Peso 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
TMel A A A B B A C C A A A A A A B A A C C A
VMax 6 6 6 7 7 12 13 13 13 13 18 19 19 19 20 24 25 26 26 26
57
O seguinte algoritmo implementa esse processo :
58
3.2.3 Produto de Matrizes
Este é um problema que surge em processamentos matriciais pesados,
tais como na área de avaliação de reservas petrolíferas, onde são necessários
até mesmo processadores matriciais, tal a intensidade de cálculos. O problema
está ligado à minimização do número de operações a serem realizadas, quando
se fazem multiplicações sequenciais de várias matrizes. Suponhamos, por
exemplo, que tenhamos que multiplicar as seguintes matrizes:
59
Seja mi,j o total de operações mínimo para o produto Mi....Mj.
Considerando k o índice que identifica a última operação que minimizou mi,j,
devemos ter:
Produto_Matrizes(); (A3.7)
Início:
Para k de 1 a n: m[k,k] ← 0; Fp;
Para d de 1 a (n - 1):
Para i de 1 a (n - d):
j ← i + d;
m[i, j] ← ∞;
Para k de i até (j - 1):
Se ((m[i, k]+m[k+1, j] + r[i -1].r[k].r[j] ) < m[i, j]) Então
m[i, j] ← m[i, k] + m[k+1, j] + r[i-1].r[k].r[j];
mk[i, j] ← k;
Fp;
Fp;
Fp;
Fim;
60
m[i, j] mk[i, j]
1 2 3 4 1 2 3 4
1 0 5.000 5.500 8.000 0 1 1 3
2 0 0 5.000 15.000 0 0 2 3
3 0 0 0 25.000 0 0 0 3
4 0 0 0 0 0 0 0 0
61
Veja-se no exemplo abaixo, duas possíveis triangularizações:
v1 v2
v0
v3
v5 v4
62
utilizar pesquisa binária ou uma árvore de busca cheia, sendo o tempo médio de
busca ≅ log2n. Entretanto, quando as chaves têm probabilidades de acesso
distintas, pode-se construir uma árvore de busca ótima, cujo tempo médio de
acesso varia entre 1 e log2n, dependendo dessas probabilidades.
A situação mencionada ocorre em inúmeras aplicações de processamento
de textos. Uma delas, por exemplo, é a indexação de "sites" na Internet,
baseada em palavras significativas. O trabalho a ser feito é o de obter a
descrição do site e identificar palavras interessantes para indexação. As
palavras são consideradas tão mais interessantes, quanto menor for sua
frequência de acesso em textos da língua. Este trabalho fica mais eficiente se
for mantida uma lista de palavras comuns, que devem ser descartadas na
indexação. Normalmente constrói-se uma árvore binária de busca ótima com
essas palavras comuns.
NO
DE POR
E QUE
E QUE
63
DE NO
Ou,
Subárvore p/ Subárvore p/
T1 T2
chaves ki,... kk-1 chaves kk+1,... kj
64
diversas subárvores e Raiz, que guarda os diversos k indicadores da
minimização de cada subárvore, que são as raizes da subárvore ótima de cada
uma das sequências de chaves.
Árvore_Ótima; (A3.7)
Início:
Para k de 1 a n:
NC[k,k] ← p[k]; NC[k,k-1] ← 0; Raiz[k,k] ← k;
Fp;
Para d de 1 a (n –1):
Para i de 1 a (n- d):
j ← i + d;
NC[i,j] ← Infinito;
s ← 0;
Para k de i até j: s ← s + p[k]; Fp;
Para k de i até j:
Se (NC[i,k-1] + NC[k+1,j] + s ) < NC[i,j] Então
NC[i,j] ← NC[i,k-1] + NC[k+1,j] + s;
Raiz[i,j] ← k;
Fp;
Fp;
Fp;
Fim;
65
A árvore ótima obtida é:
E
DE POR
NO QUE
66
Essas considerações permitem formular o problema recursivamente, de
forma que possa ser tratado como um problema de Programação Dinâmica, da
maneira a seguir, onde D é a matriz de distâncias de edição mínimas:
D(i,0) = i 0 ≤ i ≤ n;
D(0,j) = j 0 ≤ j ≤ m;
D(i, j) = Min { D(i-1, j) + 1, D(i, j-1) + 1, D(i-1, j-1) + ind(i, j) } ,
onde
ind(i,j) = 0, se o caracter Ai = Bj, 1, caso contrário
A A A A A C G
0 1 2 3 4 5 6 7
0 0 1 2 3 4 5 6 7
A 1 1 0 1 2 3 4 5 6
T 2 2 1 1 2 3 4 5 6
A 3 3 2 1 1 2 3 4 5
A 4 4 3 2 1 1 2 3 4
G 5 5 4 3 2 2 2 3 3
C 6 6 5 4 3 4 3 2 3
67
Para i de 0 a n: D[i, 0] ← i; Fp;
Para j de 1 a m: D[0, j] ← j; Fp;
Para i de 1 a n:
Para j de 1 a m:
x ← D[i -1, j] + 1;
y ← D[i, j -1] + 1;
Se (Ai = Bj) Então
z ← D[i -1, j -1]
Senão
z ← D[I -1, j -1] + 1;
D[i,j] ← Min{ x, y, z } ;
Fp;
Fp;
Fim;
68
3.4 Exercícios Propostos
Escrever algoritmos, com solução por Programação Dinâmica, para os
problemas descritos a seguir.
3.3 - (Comboio)
Dada uma ponte com um limite de carga lc , quer-se organizar a travessia
de um comboio com n carros cada um de peso pi e com um tempo ti de
travessia, mantendo a ordem dos carros no comboio, em grupos que respeitem
a carga máxima da ponte, de forma a minimizar o tempo total de travessia.
(Maratona ACM - 1999 - Índia)
3.5 – (Cortes)
Dado um tronco de madeira de tamanho n, que deve ser cortado em k
tamanhos, t1, t2, t3...tk, em uma máquina onde o custo de cada corte é
proporcional ao tamanho da peça cortada, determinar o custo mínimo do
processo de corte para se obter os pedaços desejados.
69
3.7 - (Formatação de textos)
Quer-se formatar um texto contendo n palavras para impressão, com
comprimentos c1,... cn. Cada linha tem capacidade máxima M. Entre duas
palavras é colocado um espaço e cada palavra fica inteiramente numa linha.
Distribuir as palavras no menor número de linhas, de forma a minimizar a soma
dos cubos dos tamanhos dos espaços em branco no final das linhas (exceto a
última).
3.12 - (Partição)
Dados n inteiros, particionar o conjunto em dois outros cuja soma dos
elementos seja igual.
70
Construir o triângulo de Pascal para um inteiro n.
3.17 – (Barcaça)
Tem-se uma barcaça de comprimento cb para transportar veículos entre as
margens de um rio e uma fila com n veículos. Cada veículo i tem o comprimento
ci. Os carros são arrumados na barcaça, por ordem de entrada na fila e há dois
compartimentos na barcaça, onde os carros ficam enfilados em um desses
compartimentos. Explicar a idéia de um algoritmo para determinar o número
máximo de veículos que podem ser transportados.
Ex: cb = 50 Fila: 10, 20, 30, 35, 10. Neste caso pode-se transportar os 4
primeiros veículos.
cb = 50 Fila: 30, 30, 35, 10, 20. Neste caso pode-se transportar os 2
primeiros veículos.
71
4. MÉTODO GULOSO
72
b) Merge ótimo
c) Seleção/Sequenciamento de Tarefas
d) Mochila fracionária
Letra E L M O P X
Código 00 010 011 10 110 111
0 1
0 1 0 1
E 0 1 O 0 1
L M P X
73
sendo que cada aresta esquerda recebe o valor 0 e cada direita recebe 1.
Numa árvore ótima não há links nulos.
A construção da árvore ótima tem que ser de tal forma que se minimize
o caminho externo ponderado da árvore, que é dado por
CM(T) = ∑ fi.li , onde
fi = frequência de acesso do caracter ci
li = tamanho da codificação em bits para o caracter ci
= nível do caracter na árvore - 1
Este problema pode ser resolvido pelo Método Guloso, de forma
"bottom-up", criando a árvore partindo das folhas para a raiz.
Inicialmente todas as folhas (os caracteres) estão isolados e, a cada
passo, faz-se a união das subárvores com caminho externo ponderado mínimo.
Voltando ao exemplo anterior e considerando as seguintes frequências de
acesso:
Letra E L M O P X
Freq. .20 .08 .12 .29 .14 .17
0 1
L M
Subárvo E L+M O P X
re
Freq. .20 .20 .29 .14 .17
0 1
P X
Subárvo E L+M O P+ X
re
Freq. .20 .20 .29 .31
74
0 1
E 0 1
L M
Obtemos:
Subárv. E+(L+M) O P+ X
Freq. .40 .29 .31
0 1
O 0 1
P X
Subárv. E+(L+M) O+(P+X)
Freq. .40 .60
0 1
0 1 0 1
E 0 1 O 0 1
L M P X
Árvore (E+(L+M))+(O+(P+X))
Freq. 1
75
tipo indica o tipo de nó: 0 = nó interno; 1 = folha;
caracter contém o caracter relativo à codificação;
f = contém a frequência acumulada do nó;
O Heap conterá uma estrutura semelhante, a menos dos links.
Inicialmente cria-se o Heap H, a partir dos caracteres a serem
codificados. Todos os nós recebem tipo = 1. A árvore T vai sendo criada
conforme o processo mencionado, usando o Heap H para a ordenação dos nós
segundo a soma das frequências.
Huffman; (A4.1)
Início:
CriaFolhas; CriaHeap(H);
Para i de 1 até (n - 1):
p ← H[1]; Troca (1, n-i+1); DesceHeap (1, n-i);
q ← H[1];
Alocar(z); z↑.tipo ← 0; z↑.le ← p; z↑.ld ← H[1];
z↑.f ← p↑.f + q↑.f;
H[1] ← z;
DesceHeap (1, n-i);
Fp;
Retornar H[1];
Fim;
Externamente:
T ← Huffman;
Decodifica; (A4.2)
Início:
i ← 1; j ← 1;
Enquanto (i ≤ n):
a ← T;
76
Enquanto (a↑.tipo ≠ 1):
Se (Bit[i] = 0) Então a ← a↑.le;
Senão a ← a↑.ld;
i ← i+1;
Fe;
Sai[j] ← a↑.caracter; j ← j+1;
Fe;
Fim;
Como foi dito inicialmente, a correção dos algoritmos gulosos tem que
ser demonstrada. Para tanto, considera-se que uma árvore ótima é a que tem o
custo mínimo, onde o custo da árvore T, é o caminho externo ponderado, dado
por c(T) = ∑fi.li, (frequência x profundidade de cada folha) considerando
apenas as folhas das árvores. A correção do método de Huffman baseia-se no
seguinte lema:
A demonstração deste lema é bem simples. Basta verificar que eles têm que
estar localizados no último nível da árvore, onde existem pelo menos duas
folhas, ou a árvore não seria ótima. Então podem ser colocados como irmãos.
77
Um problema muito semelhante ao anterior é o de se fazer o merge de n
listas ordenadas, de tamanhos diferentes. Quer-se determinar a ordem do
merge, visando minimizar o número de operações.
Dadas duas listas l1 e l2, de tamanhos t1 e t2, respectivamente, o número
de operações para se fazer a intercalação (merge) das mesmas é igual a t1 + t2.
Daí, que o problema tem solução análoga ao anterior. A solução é criar uma
árvore estritamente binária que conterá nas folhas uma referência a cada
lista. A união gradativa das listas é feita escolhendo-se as duas de menor
tamanho, no momento. Vejamos um exemplo:
Lista A B C D E
Tamanho 300 500 150 400 200
Usando-se o enfoque anterior, no primeiro passo, faz-se o merge das
listas C e E:
C E
Lista A B C+E D
Tamanho 300 500 350 400
Agora os dois menores tamanhos são A e (C+E):
C E
Lista A+(C+E) B D
Tamanho 650 500 400
D B
Lista A+(C+E) B+D
Tamanho 650 900
78
Finalmente obtemos a árvore desejada:
A D B
C E
Lista (A+(C+E))+(D+B)
Tamanho 1550
79
Merge(Arv); (A4.3)
Início:
Se (Arv não é folha) Então:
L1 ← Merge (Arv↑.le);
L2 ← Merge (Arv↑.ld);
Retornar (Intercala(L1, L2));
Senão
Retornar Arv↑.L;
Fim;
Externamente:
Merge(I);
tarefa T1 T2 T3 T4 T5 T6 T7 T8
80
di 1 10 2 2 9 12 5 7
df 14 13 3 6 11 15 8 8
tarefa T3 T4 T8 T7 T5 T2 T1 T6
di 2 2 7 5 9 10 1 12
df 3 6 8 8 11 13 14 15
tarefa T3 T8 T5 T6
di 2 7 9 12
df 3 8 11 15
Seleção_Tarefa();
Início:
S ← ∅; r ← -∞;
OrdenaTarefas();
Para i de 1 a n:
Se (di[i] > r) Então
S ← S U {T[i]}; r ← df[i];
Fp;
Fim;
tarefa T1 T2 T3 T4 T5 T6
l 1 1 3 4 3 4
r 7 8 4 6 10 5
O sequenciamento ótimo nesse caso é {T2, T5, T4, T6}, com uma receita
de 29. As tarefas T1 e T3 seriam feitas fora do prazo, com receita 0.
82
Tarefa T5 T2 T1 T4 T6 T3
l 3 1 1 4 4 3
r 10 8 7 6 5 4
S T2 T5 T4 T6
l 1 2 4 4
Sequenciamento_Tarefa();
Início:
OrdenaTarefas(); S ← ∅;
Para i de 1 a n:
Se (ViavelIncluir(S,T[i])) Então
Inclui (S, T[i]);
Fp;
Fim;
83
mostrado acima. O procedimento Inclui (S, T[i]) inclui T[i] em S mantendo S
ordenado por tempo limite.
tarefa T1 T2 T3 T4 T5 T6
l 1 1 3 4 3 4
p 7 8 4 6 10 5
O sequenciamento ótimo nesse caso é {T2, T5, T4, T6, T1, T3}, com uma
penalidade de 11, correspondendo às tarefas T1 e T3, que seriam feitas fora do
prazo.
84
A solução deste problema é exatamente a mesma solução anterior, a
menos do fato de ser necessário incluir no sequenciamento, as tarefas não
viáveis. O algoritmo é análogo e a complexidade também é O(n2).
Tarefa T5 T2 T1 T4 T6 T3
l 3 1 1 4 4 3
p 10 8 7 6 5 4
O conjunto viável seria composto das tarefas {T2, T5, T4, T6}, que podem
ser executadas dentro do prazo e as tarefas T1 e T3 seriam realizadas
atrasadamente, gerando o sequenciamento de penalidade 11 (7 + 4):
Tarefa T2 T5 T4 T6 T1 T3
l 1 3 4 4 1 3
tarefa T1 T2 T3 T4 T5 T6
d 2 1 3 4 3 4
p 7 8 4 6 10 5
O sequenciamento ótimo nesse caso é {T2, T1, T5, T4, T3, T6}, com uma
penalidade total de 178, (penalidades de 0, 7, 30, 36, 40, 65,
respectivamente).
Tarefa T2 T1 T5 T4 T3 T6
d 1 2 3 4 3 4
p 8 7 10 6 4 5
86
Mochila_Fracionaria(); A(4.5)
Início:
OrdenaObjetos();
i ← 0;
S ← ∅;
pesototal ← 0;
Enquanto (pesototal ≤ M) e (i < n):
S ← S U Objeto[i];
pesototal ← pesototal + peso[i];
i ← i+1;
Fe;
Se (pesototal > M) Então
S ← S - Objeto[i-1] + Fração(Objeto[i-1], pesototal-M,M);
Fim;
A B C
peso = 5 peso = 8 peso = 11
valor = 6 valor = 7 valor = 13
87
o que é uma solução errada, pois a solução ótima seria tomar os objetos B e C,
com peso total de 19 e valor total de 20.
88
4.3 Exercícios Propostos
Escrever algoritmos, com solução pelo Método Guloso, para os problemas
descritos a seguir.
4.7 - (Travessia).
Dadas n pessoas que devem atravessar uma ponte e que levam tempos de
travessia distintas, determinar as manobras necessárias para se levar o menor
tempo de travessia, considerando as seguintes restrições:
a) só podem atravessar 1 ou duas pessoas de cada vez.
89
b) está escuro e só há uma lanterna, de forma que sempre que alguém vai
para o outro lado, a lanterna tem que ser devolvida.
A B C D E F G H
200 500 300 600 100 200 300 250
90
5. PROBLEMAS NP-COMPLETOS
Algoritmos 1 2 3 4 5
Complexidade 33n 46nlgn 13n2 3.4n3 2n
Tam: entrada
10 ,00033 ,0015 ,0013 ,0034 ,001
100 ,003 ,03 ,13 3,4 1016 anos
1.000 ,033 ,45 13 ,94 h -
10.000 ,33 6,1 22 min 39 dias -
100.000 3,3 1,3 min 1,5 dias 108 anos -
Exemplos:
-Ordenação de Dados
-Buscas e atualização em árvores
-Buscas em grafos
-Ciclos Eulerianos
-Problemas com solução gulosa, em geral
-Problemas com solução por Programação dinâmica em geral
-Programação linear
Exemplos:
-Torre de Hanoi
-Geração de Permutações
91
5.3 Problemas ainda não classificados
Existe uma grande classe de problemas para os quais não se sabe ainda
se existe algoritmo polinomial ou não .
Exemplos:
-Partição (Soma de subconjuntos)
-Mochila
-Empacotamento
-Programação inteira
-Caixeiro viajante
-Ciclo Hamiltoniano
-Coloração de vértices em grafos
-Satisfatibilidade
-Clique Máxima em grafos
-Conjunto independente máximo em grafos
92
Uma atribuição de cores aos vértices do grafo.
Esse certificado tem tamanho equivalente ao tamanho do grafo (portanto
seu tamanho é polinomial em função da entrada). Além disso, pode-se
verificar, em tempo polinomial se o certificado é correto. (Basta fazer a
atribuição de cores dada pelo certificado e checar se não existem 2
vizinhos com a mesma cor. Além disso deve-se verificar se o número de
cores usada é ≤ k).
Notar que nada é exigido em relação à resposta NÃO.
93
Ex: Ciclo Hamiltoniano reduz-se, polinomialmente, a Caixeiro Viajante.
CH: Dado G, existe ciclo Hamiltoniano em G?
CV: Dado G´, completo, com arestas ponderadas, existe ciclo Hamiltoniano
com peso total ≤ k?
Transformação de CH em CV:
A partir de G para CH, cria-se G´ completo para CV, com os seguintes
pesos: para cada aresta (v,w) de G´, se ela existir em G, seu peso = 1;
senão, seu peso = 2. k é feito = |V(G)|.
Problemas em aberto:
a) P = NP?
94
FIM
95