Método Guloso

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.

O problema do pendrive

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 in e tic
3 .ooo c := cti   ▷ 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 thtk, 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.

Exercícios 1

  1. ★ Qual a diferença entre provar que um algoritmo está correto e descrever os passos do algoritmo em português (para i de 1 a n, enquanto a soma dos ti for menor que c …)?
  2. Qual o tamanho de uma instância do problema do pendrive? Quanto tempo o algoritmo Pendrive consome em função do tamanho das instâncias?
  3. Prove ou desprove as seguintes afirmações sobre o problema do pendrive: (1) um arquivo de tamanho mínimo faz parte de toda solução do problema;  (2) um arquivo de tamanho mínimo faz parte de alguma solução do problema.
  4. Critique a seguinte versão alternativa do algoritmo Pendrive:
    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 }
  5. ★ Escreva um algoritmo eficiente para decidir se um vetor B[1 .. k] é uma subsequência de um vetor A[1 .. n]. (Por exemplo, (5, 6, 4) é subsequência de (9, 5, 6, 3, 9, 6, 4, 7).) Prove que seu algoritmo está correto.
  6. Pares de livros.  Suponha dado um conjunto de livros numerados de 1 a n. Suponha que cada livro i tem um peso racional p[i] no intervalo aberto (0, 1). Queremos acondicionar os livros no menor número possível de envelopes de modo que (1) cada envelope tenha ≤ 2 livros e (2) o peso do conteúdo de cada envelope não passe de 1. Escreva um algoritmo guloso que resolva o problema em tempo Ο(n log n). Aplique seu algoritmo a um exemplo interessante. Prove que seu algoritmo está correto.

Exemplos de algoritmo gulosos

Eis uma pequena lista de problemas que admitem soluções gulosas (ou seja, podem ser resolvidos por algoritmos gulosos):

Gula versus programação dinâmica

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

Exercícios 2

  1. Dado um conjunto { a1, a2, … , an} de números naturais, queremos encontrar uma permutação ( i1, i2, … , in) de (1, 2, … , n) que minimize a soma 1ai1 + 2ai2 + … + (n−1) ain−1 + nain.
  2. Quero dirigir um carro de uma cidade A a uma cidade B ao longo de uma rodovia. O tanque de combustível do carro tem capacidade suficiente para cobrir n quilometros. O mapa da rodovia indica a localização dos postos de combustível ao longo da rodovia. Dê um algoritmo que garanta uma viagem com número mínimo de reabastecimentos.
  3. Escalonamento de tarefas.  Seja 1, … , n um conjunto de tarefas. Cada tarefa consome um dia de trabalho. Durante um dia de trabalho somente uma das tarefas pode ser executada. Os dias de trabalho são numerados de 1 a n. A cada tarefa t está associado um prazo d[t]: a tarefa deveria ser executada em algum dia do intervalo 1 .. d[t]. A cada tarefa t está associada uma multa m[t] ≥ 0. Se uma dada tarefa t é executada depois do prazo d[t], sou obrigado a pagar a multa m[t] (mas a multa não depende do número de dias de atraso).  Nosso problema: programar as tarefas (ou seja, estabelecer uma bijeção entre as tarefas e os dias de trabalho) de modo a minimizar a multa total.  Escreva um algoritmo guloso para resolver o problema. Prove que seu algoritmo está correto. Analise o consumo de tempo.
  4. Suponha que A e B são dois conjuntos, cada um contendo n números naturais. Problema: encontrar uma permutação (a1, a2, … , an) de A e uma permutação (b1, b2, … , bn) de B que maximize o produto 1 ≤ i ≤ n aibi. Dê um algoritmo que resolva o problema. Qual o consumo de tempo de seu algoritmo?