E4 Coal
E4 Coal
E4 Coal
ALGORITMOS
Ariel Da Silva Dias
Sumário
INTRODUÇÃO������������������������������������������������� 3
ALGORITMOS GULOSOS�������������������������������� 4
Problema do agrupamento de alunos��������������������������������� 8
ALGORITMO DA MOCHILA�������������������������� 11
ALGORITMOS DE PROGRAMAÇÃO
DINÂMICA���������������������������������������������������� 15
Recursão e programação dinâmica����������������������������������� 17
Programação dinâmica e série de Fibonacci�������������������� 17
BACKTRACKING������������������������������������������ 26
Exemplo de uso do backtracking��������������������������������������� 27
Problema das n-rainhas������������������������������������������������������ 29
Encontrar caminhos Hamiltonianos em um grafo������������ 32
CONSIDERAÇÕES FINAIS���������������������������� 33
2
INTRODUÇÃO
Em um projeto de algoritmo não existe uma “bala
de prata” que seja a cura para todos os problemas
de computação. Diferentes problemas requerem
o uso de diferentes tipos de técnicas. Um bom
programador usa todas essas técnicas com base
no tipo de problema. Algumas técnicas comumen-
te usadas são a divisão e conquista, algoritmos
gulosos (que curiosamente não são algoritmos,
mas sim uma técnica), programação dinâmica e
backtracking.
3
ALGORITMOS GULOSOS
O algoritmo guloso ou ganancioso é uma estraté-
gia de solução de problemas que toma decisões
localmente ótimas em cada estágio na esperança
de alcançar uma solução globalmente ótima. Esse
algoritmo simples e intuitivo pode ser aplicado
para resolver qualquer problema de otimização
que exija o resultado ótimo máximo ou mínimo,
além disso, a melhor coisa sobre esse algoritmo
é que é fácil de entender e implementar.
4
y Etapa 1: em um determinado problema, encontre
a melhor subestrutura ou subproblema.
y Etapa 2: determine o que a solução incluirá
(por exemplo, maior soma, caminho mais curto).
y Etapa 3: crie um processo iterativo para analisar
todos os subproblemas e criar uma solução ideal.
5
repetimos o processo escolhendo a segunda opção
novamente e chegamos ao posto de combustível
mais distante possível e assim faremos até che-
garmos ao nosso destino.
6
10
11 while (destinoNaoEncontrado()) {
12 val ultimaParadaAbastecimento =
posicaoParada
13
14 while (destinoNaoEncontrado() and isNextSto-
pReachable()) {
15 posicaoParada += 1
16 }
17
18 if (posicaoParada ==
ultimaParadaAbastecimento)
19 return -1
20
21 if (destinoNaoEncontrado()) {
22 numeroAbastecimentos++
23 }
24 }
25
26 return numeroAbastecimentos
27 }
28
29 private fun isNextStopReachable(): Boolean {
30 return ((matrizParadasCombustivel[posicaoParada
+ 1] - matrizParadasCombustivel[posicaoParada]) <
maximaDistanciaCarroPodePercorrer)
31 }
32
33 private fun destinoNaoEncontrado() = posicaoParada
<= numeroParadas
34
7
35 }
PROBLEMA DO AGRUPAMENTO DE
ALUNOS
Declaração do problema: Você tem um grande
salão cheio de crianças de várias idades e vários
professores dispostos a lecionarem para diferen-
tes turmas.
y Você deve agrupar essas crianças/alunos em
um grupo em que a diferença de idade entre dois
alunos quaisquer seja de no máximo 1.
y Você também tem um número limitado de
professores e, portanto, deve estar formando o
número mínimo de grupos possível.
8
y Você, então, atribuirá um professor a um grupo
para ensinar.
9
Portanto, semelhante ao problema anterior do
combustível, encontramos uma abordagem O(n)
se as idades forem dadas de maneira ordenada,
caso contrário, se você incluir a classificação dos
números, é um O(nLogn) – o que ainda é um enor-
me melhoria comparando a O(2n), e isso faz uma
enorme diferença. Talvez o problema de tempo
exponencial mais famoso em NP, por exemplo, seja
encontrar fatores primos de um grande número.
Verificar uma solução requer apenas multiplicação,
mas resolver o problema parece exigir sistemati-
camente experimentar muitos candidatos.
10
ALGORITMO DA MOCHILA
Esse é um dos problemas clássicos da computação:
imagine que você tem que planejar uma longa via-
gem e tem uma mochila para caber os vários itens
que precisa carregar, por exemplo, uma seleção
de frutas que vai lhe nutrir durante toda a viagem.
11
para outros itens, descubra o próximo melhor PMV
e repita essas etapas.
12
No algoritmo da tabela 2, a função mochila recebe
quatro parâmetros:
y n número de itens;
y v[i] valor do i-ésimo item;
y w[i] peso do i-ésimo item;
y W capacidade máxima da mochila.
13
plesmente escolher os itens em ordem decrescente
até que a mochila esteja cheia, o que, portanto,
torna nossa solução O(N log N).
14
ALGORITMOS DE
PROGRAMAÇÃO
DINÂMICA
A programação dinâmica é uma excelente abordagem
que pode ser aplicada a uma classe de problemas
para obter uma solução eficiente e ótima.
15
devido ao recálculo dos mesmos valores). Aqui, as
soluções para pequenos problemas são calculadas,
o que cria a solução para o problema geral.
16
RECURSÃO E PROGRAMAÇÃO
DINÂMICA
A recursão é uma maneira de encontrar a solução
expressando o valor de uma função em termos de
outros valores dessa função direta ou indiretamente
– tal função é chamada de função recursiva, logo,
segue uma abordagem de cima para baixo.
PROGRAMAÇÃO DINÂMICA E
SÉRIE DE FIBONACCI
A série de Fibonacci é uma sequência de números
de tal forma que cada número é a soma dos dois
anteriores, começando em 0 e 1, definida pela
fórmula:
F(n)=F(n-1)+F(n-2)
17
Podemos utilizar um método recursivo para calcular
o Fibonacci, observe na tabela 3 a seguir.
18
Agora observe o código da tabela 4, no qual não
se usa recursão. Aqui, foi criada uma lista vazia
de comprimento (n+1) e definimos o caso base
de F(0) e F(1) nas posições de índice 0 e 1. Essa
lista é criada para armazenar os valores calculados
correspondentes usando um loop for para valores
de índice 2 até n.
19
ALGORITMOS DE DIVISÃO
E CONQUISTA
Muitos algoritmos úteis são recursivos em estrutura,
ou seja, para resolver um determinado problema,
eles chamam a si mesmos recursivamente uma ou
mais vezes para lidar com subproblemas intima-
mente relacionados. Esses algoritmos geralmente
seguem um algoritmo de divisão e conquista. O
algoritmo de divisão e conquista envolve três eta-
pas em cada nível de recursão:
y Dividir: trata-se do primeiro passo da estratégia
de dividir e conquistar. Como sugerido pelo nome,
nesta etapa dividimos o problema em subproble-
mas menores até que o problema seja pequeno
o suficiente para ser resolvido. Nessa etapa, os
subproblemas tornam-se menores, mas ainda
representam parte do problema real, e, nesse
momento, usamos a recursão para implementar
o algoritmo de divisão e conquista. Um algoritmo
recursivo chama a si mesmo com valores de entra-
da menores ou mais simples – chamamos isso de
caso recursivo –, assim, quando implementamos a
etapa de divisão, determinamos o caso recursivo
que dividirá o problema em subproblemas menores.
y Conquistar: A seguir temos o passo “conquistar”,
em que resolvemos diretamente os subproblemas.
Até agora, já dividimos a entrada nas menores
20
partes possíveis e agora vamos resolvê-las reali-
zando operações básicas. Implementamos a etapa
de conquista com recursão especificando o caso
base recursivo. Uma vez que os subproblemas se
tornam pequenos o suficiente para que não seja
mais recursiva, dizemos que a recursão finalizou
e que chegamos ao caso base. Quando chegamos
ao caso base, resolvemos o subproblema.
y Combine: Por fim, chegamos ao último passo
da estratégia de dividir e conquistar – combinar.
Nessa etapa, combinamos a solução dos subpro-
blemas para resolver todo o problema e a saída
retornada da resolução do caso base será a en-
trada de subproblemas maiores. Então, depois de
chegarmos ao caso base, começaremos a resolver
subproblemas maiores com a entrada retornada
de subproblemas menores. A partir disso, mescla-
mos a saída da etapa de conquista para resolver
subproblemas maiores. Vamos propagar de baixo
para cima até resolvermos todo o problema original.
21
Podemos dizer que f(n) é o trabalho realizado fora
da chamada recursiva.
TORRE DE HANOI
Figura 1: Torre de Hanoi com três peças.
A B C
22
Suponha que peguemos alguns blocos, N = 3, o
processo de mover os blocos da Torre A para a
Torre C seria o seguinte:
y Primeiro, mova o menor bloco para a Torre C.
Esta etapa mostra a divisão do problema dividindo
o número de blocos de N = 3 a N = 2. Portanto, agora
podemos assumir que nosso subproblema está
movendo dois blocos da Torre A para a Torre C;
y Em seguida, mova o bloco do meio para a Torre
B. Semelhante ao passo anterior, dividimos ainda
mais o subproblema de N = 2 para N = 1 no ponto
de origem;
y Nessa etapa, começamos a resolver os subpro-
blemas substituindo o menor bloco pelo bloco do
meio na Torre B;
y Em seguida, mova o bloco maior para a Torre
C da Torre A e mova o bloco menor para esvaziar
a Torre A da Torre B;
y Por fim, combinamos a solução e obtemos o
resultado final movendo o bloco do meio sobre o
bloco maior da Torre B para a Torre C e movendo
o bloco menor da Torre A para a Torre C.
23
3 print(“Mova o disco 1 da origem”,origem,”ao
destino”,destino)
4 Return
5 torreHannoi(N-1, origem, aux, destino)
6 print(“Mova o disco”,N,”da origem”,origem,”para o
destino”,destino)
7 torreHannoi(N-1, aux, destino, origem)
8
9 N=3
10 torreHannoi(N,’A’,’B’,’C’)
DIVISÃO E CONQUISTA E
PROGRAMAÇÃO DINÂMICA
O algoritmo de divisão e conquista tem muitos be-
nefícios que o tornam um algoritmo de resolução
de problemas muito útil, afinal, ajuda a resolver
problemas difíceis em 3 etapas simples usando o
algoritmo top-down recursivamente.
24
Por sua vez, a programação dinâmica trata-se
de uma técnica algorítmica que está intimamente
relacionada à abordagem de dividir e conquistar,
no entanto, enquanto a abordagem de dividir e
conquistar é essencialmente recursiva e, por-
tanto, apenas de cima para baixo (top-down), a
programação dinâmica funciona de baixo para
cima (bottom-up).
25
BACKTRACKING
Backtracking é um algoritmo geral para resolver
alguns problemas computacionais, mais nota-
velmente problemas de satisfação de restrições,
que constrói adicionalmente candidatos para as
soluções e abandona os retrocessos de um can-
didato assim que determina que ele não pode ser
concluído para uma solução razoável. O algoritmo
de backtracking é usado em várias aplicações,
incluindo o problema das N-rainhas, o problema
do passeio do cavaleiro, problemas de resolução
de labirintos e a busca por todos os caminhos de
Hamilton em um grafo.
26
Um algoritmo de backtracking usa o método de
busca em profundidade, isso quer dizer que quan-
do o algoritmo começa a explorar as soluções, a
função abundante é aplicada para que o algoritmo
possa determinar se a solução proposta satisfaz
as restrições. Se isso acontecer, ele continuará pro-
curando, caso contrário, a ramificação é removida
e o algoritmo retorna ao nível anterior.
EXEMPLO DE USO DO
BACKTRACKING
Estamos pegando um exemplo muito simples aqui
para explicar a teoria por trás de um processo de
backtracking. Queremos dispor três letras a, b, c
de tal forma que c não possa ficar ao lado de a.
27
Figura 2: árvore de espaço de estados
a c
b
b c a c a b
c b c a b a
28
ável, o problema pode retornar aos checkpoints e
seguir outro caminho para encontrar uma solução.
29
8 def rainha(col):
9 if col < N:
10 for lin in range(N):
11 if linha[lin] or diagAsc[lin+col] or
diagDesc[lin-col+N-1]:
12 Continue
13 solucao[col] = lin
14 linha[lin] = diagAsc[lin+col] = diagDesc[lin-
-col+N-1] = True
15 rainha(col+1)
16 linha[lin] = diagAsc[lin+col] = diagDesc[lin-
-col+N-1] = False
17 else:
18 print solução
19 global numSol
20 numSol = numSol + 1
21
22 rainha(0)
23 print “Num.Solucoes= “, numSol
30
backtracking para recuperar todas essas soluções
conforme ilustra a figura 3.
x x x x
x x x x
x x x x
x x x x
x x x x
x x
x x
x x
x x
x x
31
ENCONTRAR CAMINHOS
HAMILTONIANOS EM UM GRAFO
Um caminho hamiltoniano é um caminho de grafo
que conecta dois vértices do grafo que visitam cada
vértice exatamente uma vez. Se existir um cami-
nho hamiltoniano com extremidades adjacentes,
o ciclo do grafo resultante é um ciclo hamiltoniano
ou somente hamiltoniano.
32
CONSIDERAÇÕES FINAIS
Nesse e-book você pôde compreender que existem
técnicas específicas para resolver os problemas
computacionais. Nós iniciamos o seu estudo co-
nhecendo os algoritmos gulosos, que são aqueles
que escolhem a melhor solução local e se movem
em direção ao objetivo final, esperando que essa
estratégia se aproxime da solução ótima global.
33
nessa técnica. Trata-se de uma técnica que con-
tinua sendo vital para resolver vários tipos de
problemas, mesmo que a complexidade de tempo
desse algoritmo possa ser alta, pois pode precisar
explorar todas as soluções existentes.
34
Referências Bibliográficas
& Consultadas
BORIN, V. P. Estrutura de dados. Curitiba:
Contentus, 2020. [Biblioteca Virtual].