A greedy algorithm starts with a solution to a very small subproblem
and augments it successively to a solution for the big problem.
The augmentation is done in a 'greedy' fashion, that is,
paying attention to short-term or local gain,
without regard to whether it will lead to a good long-term or global solution.
As in real life,
greedy algorithms sometimes lead to the best solution,
sometimes lead to pretty good solutions,
and sometimes lead to lousy solutions.
The trick is to determine when to be greedy.
— Ian Parberry, Problems on Algorithms
Esta página é uma introdução à estratégia gulosa (= greedy strategy) de solução de problemas computacionais. Poderíamos apresentar o assunto como paradigma guloso de concepção de algoritmos, ou como método guloso de projeto de algoritmos.
Estratégia gulosa é aquela usada por um montanhista que decide
caminhar sempre para cima
,
na direção de maior subida
,
na esperança de assim chegar ao pico mais alto da montanha.
(Como todos sabemos, essa estratégia nem sempre produz
o resultado esperado.)
Um algoritmo guloso escolhe, em cada iteração,
o objeto mais apetitoso
que vê pela frente.
(A definição de apetitoso
é estabelecida a priori,
antes da execução do algoritmo.)
O objeto escolhido passa a fazer parte da solução
que o algoritmo constrói.
Um algoritmo guloso é míope
:
ele toma decisões com base nas informações disponíveis na iteração corrente,
sem olhar as consequências que essas decisões terão no futuro.
Um algoritmo guloso jamais se arrepende ou volta atrás:
as escolhas que faz em cada iteração são definitivas.
Embora algoritmos gulosos pareçam naturalmente corretos, a prova de sua correção é, em geral, difícil e sutil. Para compensar, algoritmos gulosos são muito eficientes. Mas os problemas que admitem solução gulosa são um tanto raros.
Veja o capítulo 16 do livro CLRS.
Tenho n arquivos digitais no meu computador. Cada arquivo i tem ti megabytes. Quero gravar o maior número possível de arquivos num pendrive que tem capacidade para c megabytes. O problema pode ser formulado assim: encontrar o maior subconjunto X do intervalo 1 .. n que satisfaça a restrição
∑i ∈ X ti ≤ c. | (A) |
(Note que queremos maximizar ⎮X⎮ e não a soma ∑i ∈ X ti.)
Exemplo A: A instância do problema do pendrive definida pelos parâmetros n = 8, t = (10, 15, 20, 20, 30, 35, 40, 50) e c = 90 tem muitas soluções:
10 | 15 | 20 | 20 | 30 | 35 | 40 | 50 |
☑ | ☑ | ☑ | ☑ | - | - | - | - |
☑ | ☑ | ☑ | - | ☑ | - | - | - |
☑ | - | ☑ | ☑ | - | ☑ | - | - |
- | ☑ | ☑ | ☑ | - | ☑ | - | - |
O seguinte algoritmo de caráter guloso resolve o problema. Em cada iteração, se a capacidade do pendrive não estiver esgotada, o algoritmo escolhe o menor dos arquivos que ainda não foram gravados. O algoritmo é guloso pois abocanha o menor arquivo sem antes pesar os prós e contras dessa escolha. Embora a estratégia pareça natural, não é óbvio que ela produz o resultado desejado.
Se os arquivos são dados em ordem crescente de tamanho, ou seja, se t1 ≤ t2 ≤ … ≤ tn, é muito fácil descrever o algoritmo em código:
Pendrive (t, n, c) ▷ t1 ≤ … ≤ tn |
1 . i := 1 |
2 . enquanto i ≤ n e ti ≤ c |
3 .ooo c := c − ti ▷ escolhe i |
4 .ooo i := i + 1i |
5 . devolva { 1, … , i − 1 } |
Embora a lógica
do algoritmo pareça muito natural,
não é óbvio que ela leva à solução do problema.
A prova da correção do algoritmo é dificultada
pela fato de que cada instância do problema pode ter muitas soluções.
A correção do algoritmo Pendrive decorre da seguinte observação: toda instância do problema do pendrive tem uma solução X que é um segmento inicial do intervalo 1 .. n, ou seja,
alguma solução X tem a forma 1 .. k | (B) |
para algum k ≤ n. Para provar essa proposição usaremos um conceito auxiliar: diremos que o topo de um conjunto X é o maior elemento de X. Agora escolha uma solução X do problema que tenha o menor topo possível. Seja k esse topo. Mostraremos a seguir que X = { 1, … , k }.
Suponha por um momento que X ≠ { 1, … , k } e seja h um número em { 1, … , k } − X. Como th ≤ tk, temos (∑i ∈ X ti) − tk + th ≤ ∑i ∈ X ti. Logo, o conjunto (X − { k }) ∪ { h } satisfaz a restrição (A) e tem o mesmo tamanho que X, sendo portanto uma solução do problema. Como o topo do novo conjunto é menor que o topo de X, temos uma contradição. A contradição mostra que X = { 1, … , k }. Portanto, a proposição (B) está correta, CQD.
para i de 1 a n, enquanto a soma dos ti for menor que c …)?
Pendrive (t, n, c) ▷ t1 ≤ … ≤ tn |
1 . s := 0 |
2 . para i := 1 até n |
3 .ooo se s > c |
4 .oooooo devolva { 1, … , i−1 } |
5 .ooo senão s := s + ti |
6 . devolva { 1, … , i } |
Eis uma pequena lista de problemas que admitem soluções gulosas (ou seja, podem ser resolvidos por algoritmos gulosos):
One thing you will notice about greedy algorithms is that they are
usually easy to design,
easy to implement, easy to analyze,
and they are very fast,
but they are almost always difficult to prove correct.
— Ian Parberry, Problems on Algorithms
Às vezes é difícil distinguir um algoritmo guloso de um algoritmo de programação dinâmica. A seguinte lista grosseira de características pode ajudar. Um algoritmo guloso
Um algoritmo de programação dinâmica
ótimo corrente),