Docsity
Docsity

Prepare-se para as provas
Prepare-se para as provas

Estude fácil! Tem muito documento disponível na Docsity


Ganhe pontos para baixar
Ganhe pontos para baixar

Ganhe pontos ajudando outros esrudantes ou compre um plano Premium


Guias e Dicas
Guias e Dicas


Algoritmos - Tópicos Avançados, Resumos de Algoritmos e Programação

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

Tipologia: Resumos

2021

Compartilhado em 10/03/2021

RodrigoFigueiredo
RodrigoFigueiredo 🇧🇷

1 documento

1 / 95

Toggle sidebar

Esta página não é visível na pré-visualização

Não perca as partes importantes!

bg1
1
IME 04-06319 - ALGORITMOS
Paulo Eustáquio Duarte Pinto
Universidade Estadual do Rio de Janeiro
Instituto de Matemática
Departamento de Informática e Ciência da Computação
Rio de Janeiro, agosto de 2008
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21
pf22
pf23
pf24
pf25
pf26
pf27
pf28
pf29
pf2a
pf2b
pf2c
pf2d
pf2e
pf2f
pf30
pf31
pf32
pf33
pf34
pf35
pf36
pf37
pf38
pf39
pf3a
pf3b
pf3c
pf3d
pf3e
pf3f
pf40
pf41
pf42
pf43
pf44
pf45
pf46
pf47
pf48
pf49
pf4a
pf4b
pf4c
pf4d
pf4e
pf4f
pf50
pf51
pf52
pf53
pf54
pf55
pf56
pf57
pf58
pf59
pf5a
pf5b
pf5c
pf5d
pf5e
pf5f

Pré-visualização parcial do texto

Baixe Algoritmos - Tópicos Avançados e outras Resumos em PDF para Algoritmos e Programação, somente na Docsity!

IME 04-06319 - ALGORITMOS

Paulo Eustáquio Duarte Pinto

Universidade Estadual do Rio de Janeiro Instituto de Matemática Departamento de Informática e Ciência da Computação

Rio de Janeiro, agosto de 2008

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

  1. BACKTRACKING 2.1 Conceitos básicos 2.2 Problemas clássicos 2.3 Jogos 2.4 Exercícios Propostos
  2. PROGRAMAÇÃO DINÂMICA 3.1 Conceitos básicos 3.2 Problemas clássicos 3.3 Exercícios Propostos
  3. MÉTODO GULOSO 4.1 Conceitos básicos 4.2 Problemas clássicos 4.3 Exercícios Propostos
  4. PROBLEMAS NP-COMPLETOS

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.

O limite superior descrito anteriormente é definido pela conceituação O(t(n)), definida da seguinte forma: Sejam f, h duas funções reais positivas de variável inteira n. Diz-se que f é O(h), escrevendo-se f = O(h), quando existir uma constante c > 0 e um valor inteiro n 0 , tal que n > n 0 => f(n) ≤ c.g(n).

Exs:

f = n^3 – 1 => f = O (n^3 ) = O (n^4 ) f = 5 + 10 log n + 3 log^2 n => f = O (log^2 n )

Por convenção, os algoritmos que tenham complexidade de pior caso iguais ou inferiores a O(nk) são considerados eficientes. Algoritmos cuja complexidade sejam, por exemplo, O(2n) são considerados ineficientes.

Outro ponto importante é o seguinte: dado um problema, pode-se ter encontrado um algoritmo para resolvê-lo. Surge a pergunta se esse é o melhor algoritmo possível. Algumas vezes essa resposta pode ser obtida com a ajuda cos conceitos dados a seguir.

Def: Dadas as funções sobre variáveis inteiras f e g. Dizemos que f é ΩΩΩΩ (g) , se existirem uma constante c e um número n 0 tal que

f(n) ≥ c.g(n) , para todo n > n 0.

Se P é um problema, então dizemos que o limite inferior para P é dado por uma função h tal que a complexidade de pior caso de qualquer algoritmo que resolva P é Ω (h).

Desta forma, quando conseguimos determinar o limite inferior para um problema e, ao mesmo tempo, conseguimos um algoritmo cuja complexidade de pior caso seja igual a esse limite, então estamos diante de um algoritmo ótimo, pois não se pode conseguir algoritmo com complexidade mais baixa que o mesmo.

0.2 Bibliografia recomendada

Esta apostila está fortemente baseada no primeiro livro indicado abaixo e não pretende substituir um livro texto, necessário para se complementar a compreensão de cada tema abordado. Os seguintes livros são indicados:

Algorithms R. Sedgewick;Addison-Wesley, 1988 Introduction To Algorithms T. H. Cormen et all; McGraw Hill, 1998 Estrut. de Dados e Seus Algoritmos J.L.Szwarcfiter, LTC 1994

Recomenda-se, também, o acesso aos seguintes sites que abordam problemas das maratonas de programação ACM e da Olimpíada de Informática: http://icpc.baylor.edu http://acm.uva.es http://olympiads.win.tue.nl/ioi http://olimpiada.ic.unicamp.br

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.

b) A técnica pode ser ainda imaginada como o equivalente matemático da indução finita, onde, para se demonstrar fatos matemáticos usam-se duas etapas: b.1.1) Mostra-se que a hipótese vale para valores particulares e pequenos de n (0, 1, etc). b.1.2) Supondo-se a hipótese verdadeira para todos os valores inferiores a n, demonstra-se que ela continua valendo para n. Na recursão, temos que: b.2.1) Exibe-se um algoritmo para resolver casos particulares e pequenos (0, 1, etc). Os problemas pequenos são chamados “ problemas infantís”. b.2.2) Exibe-se um algoritmo para achar a solução do problema “grande” a partir da composição de problemas menores.

c) A técnica é o equivalente procedural da formulação de recorrências (funções recursivas), onde, de forma análoga ao ítem b), uma função é definida em duas partes. Na primeira, é dada uma fórmula fechada para um ou mais valores de n. Na segunda, a definição da função para n é feita a partir da mesma função aplicada a valores menores que n, para valores superiores aos da primeira parte. Outra relação de recursão com recorrência é que muitas propriedades de soluções recursivas são obtidas com o uso de recorrências.

d) Os procedimentos recursivos são aqueles que " chamam a sí mesmo". É claro, então, que a chamada a sí mesmo sempre se dá no contexto de buscar a solução de problemas menores, para compor a solução do maior. Além disso todo procedimento recursivo tem que ter também uma chamada externa.

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.

1.2 Problemas clássicos

Neste tópico são apresentados os seguintes algoritmos clássicos com solução recursiva: a) Torre de Hanói b) Quicksort c) Mínimo e Máximo d) Cálculo de Combinação e) Torneio

1.2.1 Torre de Hanói O problema baseia-se em um jogo infantil, onde há n pratos de tamanhos diferentes, com um furo no meio e três varetas A, B e C, que possibilitam que esses pratos possam ser empilhados em cada uma delas. O empilhamento só pode ser feito colocando-se pratos menores em cima de maiores. Inicialmente todos os pratos estão na vareta A. O problema é levar esses pratos para a vareta C, podendo usar as três varetas como empilhamento temporário.

A B C

Esse problema tem a seguinte solução recursiva:

Hanoi(n, A, B, C); (A1.3) Início: Se (n > 0) Então Hanoi (n-1, A, C, B); Mover topo de A para C; Hanoi (n-1, B, A, C); Fim;

Qualquer solução não recursiva para este problema é muito mais complicada que a mostrada acima. A complexidade desse algoritmo é O(2n), não sendo, portanto, um algoritmo eficiente. Entretanto isso é o melhor que pode ser feito, pois o problema é, em sí, exponencial (!).

No exemplo, com os pratos numerados de 3 a 1, de baixo para cima, a solução seria:

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 =~nlog 2 n = O(n log 2 n) Pior caso = Há várias possibilidades. Vetor em “Zig Zag”, p. Ex. NC =~ n^2 /2 = O(n^2 ) Caso Médio: NC =~ nlog 2 n = O(n log 2 n) 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 (log 2 n).

b) Observações:

b.1) Número de comparações: 42

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

O algoritmo tem complexidade O(n^2 ), pior que de alguns outros algoritmos de ordenação. Entretanto sua grande importância deriva do fato de o pior caso é algo raro de acontecer. A complexidade de caso médio é O(nlogn), e o algoritmo é muito rápido para o caso médio.

1.2.3 Mínimo e Máximo O problema é determinar os valores mínimo e máximo de um conjunto de números S. Esse problema tem uma solução trivial que é se fazer dois “loops” para encontrar separadamente os valores mínimo e máximo, executando exatamente 2n-2 comparações. O seguinte algoritmo recursivo permite uma melhora desse resultado:

MinMax (S); (A1.5) Início: Seja S = [a 1 ,..., an] Se (|S| = 1) Então Retornar (a 1 , a 1 ); Senão Se (|S| = 2) Então Se (a 1 > a 2 ) Então Retornar (a 2 , a 1 ); Senão Retornar (a 1 , a 2 ); Senão m ← Int(|S| /2); (b 1 , c 1 ) ← MinMax(S 1 = [a 1 ,..., am]); (b 2 , c 2 ) ← MinMax(S 2 = [am+1 ,... an]); Retornar (min{ b 1 , b 2 }, max{c1, c 2 }); Fim;

Esta solução recursiva necessita apenas de (3n/2 - 2) comparações, mas a prova desse resultado fica como exercício. Esse resultado permite, então formular um algoritmo não recursivo com igual número de comparações, que é o seguinte: criam-se dois vetores, um de mínimos e outro de máximos. Esses vetores são preenchidos a partir da comparação, dois a dois, dos elementos

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.

r 1 r 2 r 3 r 4 1 2 3 4 2 1 4 3 3 4 1 2 4 3 2 1

Uma matriz R desse tipo tem as seguintes propriedades:

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

A essência do emparelhamento é expressa pela propriedade d).

Este problema tem uma solução interessante recursiva quando n é potência de 2. O quadro abaixo ilustra a solução para n = 8:

r 1 r 2 r 3 r 4 r 5 r 6 r 7 r 8 1 2 3 4 5 6 7 8

Pode-se observar a seguinte simetria nessa tabela: dividindo-a em 4 seções iguais, vê-se que a seção esquerda superior é igual à direita inferior e que a esquerda inferior é igual à direita superior. Além disso, a seção esquerda inferior corresponde à esquerda superior, somando-se n/2 aos números respectivos. Pode-se verificar que essa simetria é recursiva, quando se substitui um quadro pela seção esquerda superior, agora com número de elementos dividido por 2. O Esquema abaixo ilustra a composição recursiva:

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.

Isso sugere o algoritmo a seguir, que usa uma matriz R n x n. m indica o tamanho da submatriz, Evidentemente, a chamada externa é: Torneio (n).

Torneio (m): (A1.8) Início:

Finalmente, uma solução recursiva pode ser estabelecida para qualquer número de elementos, a partir da idéia deste último algoritmo. O número de rodadas será (n - 1) quando n for par ou n quando ímpar. Neste último caso pode-se imaginar que há um competidor adicional "bye", que participa dos emparelhamentos, de forma que podemos considerar o número de competidores um número par. Não há maiores dificuldades na recursão quando n é da forma n = 4k, para algum inteiro k, pois a solução acima aplica-se diretamente. A situação problemática é para números da forma: n = 4k + 2, porque, neste caso, a primeira metade da tabela tem tamanho n/2 x (n/2+1), o que geraria uma solução contendo n rodadas, que não é o objetivo perseguido. Vamos verificar, entretanto, que há uma forma de contornar esse incoveniente, eliminando uma das colunas dos quadrantes III e IV.

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;

A recursão pode ser assim explicada:

a) Se n for ímpar, resolve-se o problema para n+1 e, no final, abandona- se a última linha e substitui-se o emparelhamento com o último número pelo emparelhamento “bye”. b) Resolve-se, recursivamente o problema para n/2.

Agora volta-se ao problema para n 1 = 6. Aplicando-se o procedimento mencionado, obtemos:

r 1 r 2 r 3 r 4 r 5 r 6 r 7 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

Como 6 é um número da forma 4k+2, temos uma solução indesejada em 6 rodadas. A seguir, move-se os pares (4, 1), (5, 2), (6, 3), da coluna r 5 para as colunas respectivas de “bye” e depois elimina-se essa coluna, redefinindo as colunas r 6 e r 7 , finalizando a solução para n 1 = 6.

r 1 r 2 r 3 r 4 r 5 r 6 r 7 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

r 1 r 2 r 3 r 4 r 5 r 6 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

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:

r 1 r 2 r 3 r 4 r 5 r 6 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

Um esboço do algoritmo é mostrado a seguir: Torneio (m); Início: Se (m = 1) Então R[1,1] ← 1; Senão Se (m ímpar) Então Torneio(m+1); Transforma emparelhamento com (m+1) em “bye”; Senão Torneio (m/2); Copia Quadrante I p/ Quadrante II, acrescentando m/2; Gera permutação circular no quadrante III, para os elementos (m/2 + 1) a m; Preenche de maneira forçada o Quadrante IV; Se (m/2 ímpar) Então Move os elementos da coluna m/2+2 para a coluna de “bye”; Move uma posição para a esquerda as colunas m/2+3 a m+1; Fim;

1.3 Análise da Recursão Serão apresentados três aspectos importantes para a análise do método de Recursão. O primeiro é o critério de balanceamento, que serve como orientação para se gerar bons programas recursivos, em geral. O segundo é uma ferramenta para análise da complexidade da recursão. O terceiro é uma ferramenta para evitar recursões ineficientes.