Marcela Ferreira Análise e Complexidade de Algoritmos
Algoritmos Gulosos Uma estratégia para resolver problemas de otimização são os algoritmos gulosos, os quais escolhem a opção que parece ser a melhor no momento (escolha ótima), e esperam que desta forma consiga-se chegar a uma solução ótima global. Embora nem sempre seja possı́vel chegar a uma solução ótima a partir da utilização de algoritmos gulosos, eles são eficientes em uma ampla variedade de problemas.
Os algoritmos gulosos tomam decisões com base apenas na
informação disponı́vel, sem se preocupar com os efeitos futuros de tais decisões, isto é, eles nunca reconsideram as decisões tomadas, independentemente das consequências. Não há, portanto, necessidade de avaliar as alternativas nem de empregar procedimentos elaborados permitindo que decisões anteriores sejam desfeitas. Devido a tais caracterı́sticas, de forma geral eles são fáceis de se ”inventar”e implementar, e são eficientes quando funcionam. Marcela Ferreira Análise e Complexidade de Algoritmos Para possibilitar uma ”noção geral”de como trabalham os algoritmos gulosos, vamos abordar um exemplo.
Suponha que tenhamos disponı́veis moedas com valores de 100,
25, 10, 5 e 1.
O problema é criar um algoritmo que para conseguir obter um
determinado valor com o menor número de moedas possı́vel (problema do troco).
Suponha que é preciso ”dar um troco”de $2.89. A melhor solução,
isto é, o menor número de moedas possı́vel para obter o valor, consiste em 10 moedas: 2 de valor 100, 3 de valor 25, 1 de valor 10 e 4 de valor 1.
De forma geral, agimos tal como um algoritmo guloso: em cada
estágio adicionamos a moeda de maior valor possı́vel, de forma a não passar da quantia necessária.
Marcela Ferreira Análise e Complexidade de Algoritmos
Embora seja uma afirmação difı́cil de provar, é verdade que com os valores dados das moedas, e tendo-se disponı́vel uma quantidade adequada de cada uma, o algoritmo sempre irá fornecer uma solução ótima para o problema. Entretanto, ressalta-se que para diferentes valores de moedas, ou então quando se tem uma quantidade limitada de cada uma, o algoritmo guloso pode vir a não chegar em uma solução ótima, ou até mesmo não chegar a solução nenhuma (mesmo esta existindo).
Marcela Ferreira Análise e Complexidade de Algoritmos
Marcela Ferreira Análise e Complexidade de Algoritmos Caracterı́sticas Gerais dos Algoritmos Gulosos
• há um problema a ser resolvido de maneira ótima, e para
construir a solução existe um conjunto de candidatos. No caso do problema do troco, os candidatos são o conjunto de moedas (que possuem valor 100, 25, 10, 5 e 1), com quantidade de moedas suficiente de cada valor;
• durante a ”execução”do algoritmo são criados dois conjuntos:
um contém os elementos que foram avaliados e rejeitados e outro os elementos que foram analisados e escolhidos;
• há uma função que verifica se um conjunto de candidatos produz
uma solução para o problema. Neste momento, questões de otimalidade não são levadas em consideração. No caso do exemplo, esta função verificaria se o valor das moedas já escolhidas é exatamente igual ao valor desejado.
Marcela Ferreira Análise e Complexidade de Algoritmos
• uma segunda função é responsável por verificar a viabilidade do conjunto de candidatos, ou seja, se é ou não possı́vel adicionar mais candidatos a este conjunto de tal forma que pelo menos uma solução seja obtida. Assim como no item anterior, não há preocupação com otimalidade. No caso do problema do troco, um conjunto de moedas é viável se seu valor total não excede o valor desejado;
• uma terceira função, denominada função de seleção, busca
identificar qual dos candidatos restantes (isto é, que ainda não foram analisados e enviados ou para o conjunto dos rejeitados ou dos aceitos) é o melhor (o conceito de melhor dependerá do contexto do problema). No exemplo, a função de seleção seria responsável por escolher a moeda com maior valor entre as restantes;
• por fim, existe a função objetivo, a qual retorna o valor da
solução encontrada. No exemplo, esta função seria responsável por contar o número de moedas usadas na solução. Marcela Ferreira Análise e Complexidade de Algoritmos Vamos agora definir um problema geral: a partir de um conjunto C, é desejado determinar um subconjunto S ⊆ C tal que:
(i) S satisfaça a uma determinada propriedade P ;
(ii) S é máximo (ou mı́nimo, dependendo do contexto do
problema) em relação um critério α, isto é, S é o subconjunto de C que possui o maior (ou o menor) tamanho, de acordo com α que satisfaz a propriedade P .
De uma forma mais detalhada, para resolver o problema, busca-se
um conjunto de candidatos que constituam uma solução, e que ao mesmo tempo otimizem a função objetivo. O conceito de otimização depende do contexto do problema. No caso do problema do troco, deseja-se minimizar o número de moedas.
Marcela Ferreira Análise e Complexidade de Algoritmos
Desta forma, pode-se ver que a função de seleção é usualmente relacionada com a função objetivo. É importante ressaltar que às vezes pode-se ter várias funções de seleção que são plausı́veis, e a escolha correta de uma delas é essencial para que o algoritmo funcione corretamente. O Algoritmo a seguir ilustra o funcionamento de um algoritmo guloso genérico.
Marcela Ferreira Análise e Complexidade de Algoritmos
Marcela Ferreira Análise e Complexidade de Algoritmos Pode-se dizer, portanto, que um algoritmo guloso trabalha da seguinte forma: a princı́pio o conjunto S está vazio, ou seja, não há candidatos escolhidos. Então, a cada passo, utiliza-se a função de seleção para determinar qual é o melhor candidato (lembrando que a função de seleção considera apenas os elementos que ainda não foram avaliados).
Caso o conjunto ampliado de candidatos não seja viável, ignora-se
o termo que está sendo avaliado no momento.
Por outro lado, se tal conjunto é viável, adiciona-se o elemento em
questão ao conjunto S. O elemento considerado, sendo ele aceito ou rejeitado, não é mais considerado pela função de seleção nos passos posteriores.
Cada vez que o conjunto de candidatos escolhidos (S) é ampliado,
é verificado se a solução do problema foi obtida.Quando um algoritmo guloso trabalha corretamente, a primeira solução encontrada da maneira aqui descrita é ótima. Marcela Ferreira Análise e Complexidade de Algoritmos Exemplo. Intercalar duas listas, com comprimentos m1 e m2 . O comprimento da lista resultante será a soma m1 + m2 dos seus comprimentos, e o número de comparações necessárias para essa intercalação (no pior caso) é m1 + m2 − 1.
Consideremos, por exemplo, o caso de intercalar três listas L1 , L2
e L3 , com respectivos comprimentos 15, 10 e 5.
Uma maneira de proceder, seria: primeiro intercalar L1 com L2 e,
depois o resultado L1 L2 com L3 .
O número de comparações para produzir L1 L2 é 15 + 10 - 1, e
(15 + 10) + 5 - 1 para intercalá-la com L3 .
No total, teremos (15 + 10 - 1) + (15 + 10 + 5 - 1), ou seja, 53
comparações.
Marcela Ferreira Análise e Complexidade de Algoritmos
Uma outra maneira de proceder é: primeiro intercalar L2 com L3 e, depois L1 com o resultado L2 L3 . O número de comparações é 10 + 5 - 1, para produzir L2 L3 , e 15 + (10 + 5 ) - 1, para intercalar L1 com L2 L3 . Assim, teremos um número total de (10 + 5 - 1) + (15 + 10 + 5 - 1), ou seja 43 comparações.
Esse exemplo ilustra bem o fato de que a ordem em que são
realizadas as intercalações pode influir substancialmente no número de comparações requeridas.
O problema consiste, então, em determinar uma sequência ótima
de intercalações, isto é, requerendo número total mı́nimo de comparações.
Exercı́cio. Determine o número total de comparações para
intercalar as três listas L1 , L2 , L3 acima, primeiro intercalando L1 e L3 e, depois, o resultado com L2 .
Marcela Ferreira Análise e Complexidade de Algoritmos
Agora vamos examinar mais de perto o efeito da sequência escolhida no número de comparações requeridas pasra intercalar n listas Li com comprimentos mi , para i = 1, ..., n.
Consideremos, no caso de n = 3 listas, as duas maneiras acima.
Na primeira maneira, intercalamos L1 com L2 , usando
(m1 + m2 ) − 1 comparações e produzindo L1 L2 com comprimento m1 + m2 , a qual é intercalada com L3 , usando (m1 + m2 + m3 ) − 1 comparações. Nesse caso, o número total de 0 comparações t , vem a ser
(m1 + m2 ) − 1 + (m1 + m2 + m3 ) − 1 ou seja,
2.(m1 + m2 ) + m3 − 2
Marcela Ferreira Análise e Complexidade de Algoritmos
Na segunda maneira, intercalamos L2 com L3 , usando m2 + m3 − 1 comparações e produzindo L2 L3 com comprimento m2 + m3 , com a qual L1 é intercalada, usando (m1 + m2 + m3 ) − 1 comparações. 00 O número total t de comparações vem a ser
(m2 + m3 ) − 1 + (m2 + m3 + m1 ) − 1 ou seja,
2.(m2 + m3 ) + m1 − 2
Marcela Ferreira Análise e Complexidade de Algoritmos
0 00 Agora, se (m1 + m2 ) ≤ (m2 + m3 ), então teremos t ≤ t ; e nesse caso, devemos escolher a primeira maneira.
É isso que sugere a estratégia gulosa: escolher a que dá menor
contribuição ao resultado final.
Um algoritmo guloso para esse problema de sequência ótima de
intercalações seleciona, a cada passo um par com soma mı́nima dentre os presentes para fazer parte da solução. Ao final, termeos uma sequência ótima de intercalações.
Marcela Ferreira Análise e Complexidade de Algoritmos
Consideremos 5 listas, com comprimentos 10, 20, 30, 40 e 50 a intercalar. 1. Inicialmente escolhemos as duas de menores comprimentos (10 e 20) e as substituı́mos pelo resultado de sua intercalação (obtida com 10 + 20 - 1 comparações), ficando com 4 listas, com comprimentos 30, 30, 40 e 50. 2. Repetindo o processo, escolhemos as de comprimentos 30 e 30 e as substituı́mos pelo resultado de sua intercalação (obtida com 30 + 30 - 1 comparações), ficando com 3 listas, com comprimentos 60, 40 e 50. 3. Agora, escolhemos as de comprimentos 40 e 50 e as substituı́mos pelo resultado de sua intercalação (obtida com 40 + 50 - 1 comparações), ficando com 2 listas, com comprimentos 60, 90. 4. Finalmente, substituı́mos essas duas listas pelo resultado de sua intercalação (obtida com 60 + 90 - 1 comparações), ficando com uma lista : o resultado final.
Marcela Ferreira Análise e Complexidade de Algoritmos
Marcela Ferreira Análise e Complexidade de Algoritmos Na linha 1 a saı́da é inicializada. É selecionado, através da função gulosa, um par de elementos, na linha 3, que são marcados (removidos) na linha 4, atualizando a entrada, na linha 6, e guardando o tamanho da lista e a solução, na linha 7. Nas linhas 9 e 10 o processo é finalizado, recuperando-se a solução.
Marcela Ferreira Análise e Complexidade de Algoritmos
A seguir vamos examinar o emprego da estratégia gulosa para desenvolver algoritmos para alguns problemas tı́picos.
Problema de caminhos de custo mı́nimo em grafo orientado a partir
de fonte. O problema consiste em determinar um caminho de custo mı́nimo a partir de um vértice fonte a cada vértice do grafo.
Uma estratégia gulosa para determinar caminhos de custo mı́nimo
a partir de um vértice fonte usa um conjunto de vértices intermediários, que é incrementado em cada passo.
Marcela Ferreira Análise e Complexidade de Algoritmos
Exemplo. Caminhos de custo mı́nimo a partir de fonte em grafo. Consideremos grafo orientado G = (V, E) abaixo
O grafo G tem 5 vértices V = {a, b, c, d, e} e 8 arestas com a
seguinte matriz de custos:
Marcela Ferreira Análise e Complexidade de Algoritmos
Consideremos como fonte v0 o vértice a. Uma estratégia gulosa razoável se baseia em um conjunto I de vértices a serem usados como intermediários nos caminhos. Inicialmente, o conjunto I de vértices intermediários é vazio, e o vetor distâncias é a primeira linha da matriz de custos acima, ou seja:
Em cada passo, seleciona-se como novo intermediário um vértice
que tenha distância mı́nima e são atualizadas as distâncias. Marcela Ferreira Análise e Complexidade de Algoritmos No passo 1, o vértice selecionado como novo intermediário é b. Com isso, obtemos novos caminhos a partir de a passando por b: até c com comprimento 3 + 3 = 6 < ∞, e até d com comprimento 3 + 2 = 5 < ∞ e até e com compriemnto 3 + 7 = 10 ¡ 11.
Desse modo, obtém-se a seguinte sequência de passos:
Marcela Ferreira Análise e Complexidade de Algoritmos