1 of 168

Técnicas de Análise de Algoritmo

Algoritmos Gulosos

Alysson Milanez

2 of 168

Na aula passada...

2

3 of 168

Na aula passada...

Programação Dinâmica

3

4 of 168

Algoritmos Gulosos

4

5 of 168

Algoritmos Gulosos

Em geral, os algoritmos relacionados a problemas de otimização funcionam através de uma sequência de passos, com um conjunto de opções (escolhas) em cada passo.

5

6 of 168

Algoritmos Gulosos

Para muitos problemas de otimização, é um exagero utilizar a programação dinâmica para descobrir as melhores escolhas: algoritmos mais simples e eficientes farão a mesma coisa. Um algoritmo guloso sempre faz a escolha que parece ser a melhor no momento (escolha mais apetitosa).

6

7 of 168

Algoritmos Gulosos

Isto é, ele faz uma escolha ótima para as condições locais, na esperança de que essa escolha leve a uma solução ótima para a situação global.

7

8 of 168

Algoritmos Gulosos

Os algoritmos gulosos nem sempre produzem soluções ótimas, mas para muitos problemas eles são úteis.

O método guloso é bastante eficiente e funciona bem para uma ampla variedade de problemas.

8

9 of 168

Um problema de seleção de atividade

9

10 of 168

Um problema de seleção de atividade

Nosso primeiro exemplo é o problema de programar um recurso entre diversas atividades concorrentes. Descobriremos que um algoritmo guloso fornece um método simples para selecionar um conjunto de tamanho máximo de atividades mutuamente compatíveis.

10

11 of 168

Um problema de seleção de atividade

Vamos supor que temos um conjunto S = {a1, a2, ..., an} de n atividades propostas que desejam usar um recurso, como uma sala de conferências, o qual só pode ser utilizado por uma única atividade de cada vez.

11

12 of 168

Um problema de seleção de atividade

Cada atividade ai tem um tempo de início si e um tempo de término fi, onde 0 <= si < fi < ∞. Se selecionada, a atividade ai ocorre durante o intervalo de tempo meio aberto [si, fi). As atividades ai e aj são compatíveis se os intervalos [si, fi) e [sj, fj) não se superpõem (si >= fj ou sj >= fi).

12

13 of 168

Um problema de seleção de atividade

O problema de seleção de atividade consiste em selecionar um subconjunto de tamanho máximo de atividades mutuamente compatíveis. Por exemplo, consideremos o seguinte conjunto S de atividades, que colocamos em ordem monotonicamente crescente de tempo de término:

13

14 of 168

Um problema de seleção de atividade

Para este exemplo, o subconjunto {a3, a9, a11} consiste em atividades mutuamente compatíveis. Porém, ele não é um subconjunto máximo, pois o subconjunto {a1, a4, a8, a11} é maior. De fato, o conjunto {a1, a4, a8, a11} é um subconjunto maior de atividades mutuamente compatíveis; outro subconjunto maior é {a2, a4, a9, a11}.

14

15 of 168

Um problema de seleção de atividade

Resolveremos esse problema em várias etapas. Começamos formulando uma solução de programação dinâmica para esse problema, na qual combinamos soluções ótimas para dois subproblemas, a fim de formar uma solução ótima para o problema original. Consideraremos diversas escolhas ao determinar quais subproblemas usar em uma solução ótima.

15

16 of 168

Um problema de seleção de atividade

Então, observaremos que só precisamos considerar uma escolha - a escolha gulosa - e que, ao optarmos pela escolha gulosa, um dos subproblemas tem a garantia de ser vazio, de forma que só resta um subproblema não vazio. Com base nessas observações, desenvolveremos um algoritmo guloso recursivo para resolver o problema de tempo de atividades.

16

17 of 168

Um problema de seleção de atividade

Completaremos o processo de desenvolver uma solução gulosa convertendo o algoritmo recursivo em um iterativo. Embora as etapas que usaremos nesse exemplo sejam mais complexas que o comum para um algoritmo guloso, a ideia é ilustrar o relacionamento entre algoritmos gulosos e programação dinâmica.

17

18 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

Começaremos desenvolvendo uma solução de programação dinâmica para o problema. Nosso primeiro passo é encontrar a subestrutura ótima, e depois usá-la para construir uma solução ótima para o problema a partir de soluções ótimas para subproblemas.

18

19 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

Sabemos da necessidade de definir um espaço de subproblemas apropriado. Vamos começar definindo conjuntos

Sij = {ak ∈ S: fi <= sk < fk <= sj},

19

20 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

de forma que Sij seja o subconjunto de atividades em S que podem começar após a atividade ai terminar, e que terminam antes da atividade aj começar.

20

21 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

De fato, Sij consiste em todas as atividades compatíveis com ai e aj e que também são compatíveis com todas as atividades que não terminam depois de ai terminar e com todas as atividades que não começam antes de aj começar.

21

22 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

Para representar o problema inteiro, adicionamos atividades fictícias a0 e an+1, e adotamos as convenções de que f0 = 0 e sn+1 = ∞. Então S = S0,n+1, e os intervalos para i e j são dados por 0 <= i, j <= n+1.

22

23 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

Podemos restringir ainda mais os intervalos de i e j como a seguir. Vamos supor que as atividades estejam dispostas em ordem monotonicamente crescente de tempo de término:

f0 <= f1 <= f2 <= ... <= fn < fn+1 (1)

23

24 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

Sij = ∅ sempre que i >= j. Por quê?

Suponhamos que exista uma atividade ak ∈ Sij para algum i >= j, de forma que ai segue aj na sequência ordenada.

24

25 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

Assim, teríamos fi <= sk < fk <= sj < fj. Logo, fi < fj, o que contradiz a hipótese de que ai segue aj na sequência ordenada.

25

26 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

Concluímos que, se ordenarmos as atividades em ordem monotonicamente crescente de tempo de término, nosso espaço de subproblemas é selecionar um subconjunto de tamanho máximo de atividades mutuamente compatíveis de Sij, para 0 <= i < j <= n+1, sabendo que todos os outros Sij são vazios.

26

27 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

Agora, usamos nossa subestrutura ótima para mostrar que podemos construir uma solução ótima para o problema a partir de soluções ótimas para subproblemas. Qualquer solução para um subproblema não vazio Sij inclui alguma atividade ak e qualquer solução ótima contém dentro dela soluções ótimas para instâncias de subproblemas Sik e Skj.

27

28 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

Desse modo, podemos construir um subconjunto de tamanho máximo de atividades mutuamente compatíveis em Sij dividindo o problema em dois subproblemas, encontrando subconjuntos de tamanho máximo Aik e Akj de atividades mutuamente compatíveis para esses subproblemas.

28

29 of 168

Um problema de seleção de atividade

A subestrutura ótima do problema de seleção de atividades

E formando nosso subconjunto de tamanho máximo Aij de atividades mutuamente compatíveis como

Aij = Aik U {ak} U Akj (2)

Uma solução ótima para o problema inteiro é uma solução para S0,n+1.

29

30 of 168

Um problema de seleção de atividade

Uma solução recursiva

O segundo passo é definir recursivamente o valor de uma solução ótima. Para esse problema, seja c[i,j] o número de atividades em um subconjunto de tamanho máximo de atividades mutuamente compatíveis em Sij. Temos c[i,j] = 0 sempre que Sij = ∅; em particular, c[i,j] = 0 para i >= j.

30

31 of 168

Um problema de seleção de atividade

Considerando Sij não vazio: se ak é usado em um subconjunto de tamanho máximo de atividades compatíveis de Sij, também usamos subconjuntos de tamanho máximo de atividades compatíveis para os subproblemas Sik e Skj. Usando a equação (2), temos

c[i,j] = c[i,k] + c[k,j] + 1.

31

32 of 168

Um problema de seleção de atividade

Essa equação recursiva pressupõe que conhecemos o valor de k, o que não ocorre. Há j-i-1 valores possíveis para k: k = i+1, ..., j-1. Tendo em vista que o subconjunto de tamanho máximo de Sij deve usar um desses valores de k, checamos todos eles para achar o melhor. Assim, a definição recursiva completa de c[i,j] se torna

(3)

32

33 of 168

Um problema de seleção de atividade

Convertendo uma solução de programação dinâmica em uma solução gulosa

Agora seria o momento de escrevermos um algoritmo de PD bottom-up, baseado na equação (3). No entanto, existem duas observações importantes que nos permitem simplificar a solução.

33

34 of 168

Um problema de seleção de atividade

Teorema 1

Considere qualquer subproblema não vazio Sij, e seja am a atividade em Sij com o tempo de término mais antigo:

fm = min{fk: ak ∈ Sij}.

34

35 of 168

Um problema de seleção de atividade

Então,

1. A atividade am é usada em algum subconjunto de tamanho máximo de atividades compatíveis de Sij.

2. O subproblema Sim é vazio, de forma que a escolha de am deixa o subproblema Smj como o único que pode ser não vazio.

35

36 of 168

Um problema de seleção de atividade

Por que o Teorema 1 é valioso?

36

37 of 168

Um problema de seleção de atividade

Vimos que a subestrutura ótima varia na quantidade de subproblemas usados em uma solução ótima para o problema original e em quantas escolhas temos na determinação de quais subproblemas usar. Em nossa solução de PD, dois subproblemas são usados em uma solução ótima, e existem j-i-1 escolhas ao se resolver o subproblema Sij.

37

38 of 168

Um problema de seleção de atividade

O Teorema 1 reduz ambas quantidades de forma significativa: apenas um subproblema é usado em uma solução ótima e, quando resolvemos o subproblema Sij, só precisamos considerar uma escolha: a que tem o tempo de término mais antigo em Sij. Felizmente, podemos determinar com facilidade que atividade é essa.

38

39 of 168

Um problema de seleção de atividade

Além de reduzir o número de subproblemas e o número de escolhas, o Teorema 1 produz outro benefício: podemos resolver cada subproblema de cima para baixo, em vez de usar a solução de baixo para cima de PD.

39

40 of 168

Um problema de seleção de atividade

Para resolver o subproblema Sij, escolhemos a atividade am de Sij com o tempo de término mais antigo e adicionamos a essa solução o conjunto de atividades usadas em uma solução ótima para o subproblema Smj. Como sabemos que, tendo escolhido am, certamente usaremos uma solução para Smj em nossa solução ótima para Sij, não precisamos resolver Smj antes de resolver Sij.

40

41 of 168

Um problema de seleção de atividade

Para resolver Sij, podemos primeiro escolher am como a atividade em Sij com o tempo de término mais antigo, e depois resolver Smj.

41

42 of 168

Um problema de seleção de atividade

Observe também que existe um padrão para os subproblemas que resolvemos. Nosso problema original é S = S0,n+1. Suponha que escolhemos am1 como a atividade em S0,n+1 com o tempo de término mais antigo. Nosso próximo subproblema é Sm1,n+1.

42

43 of 168

Um problema de seleção de atividade

Agora, suponha que escolhemos am2 como a atividade em Sm1,n+1 com o tempo de término mais antigo. Nosso próximo subproblema é Sm2,n+1. Continuando, vemos que cada subproblema terá a forma Smi,n+1 para algum número de atividade mi. Cada subproblema consiste nas últimas atividades a terminar, e o número de tais atividades varia de um subproblema para outro.

43

44 of 168

Um problema de seleção de atividade

Também há um padrão nas atividades que escolhemos. Como sempre escolhemos a atividade com o tempo de término mais antigo em Smi,n+1, os tempos de término das atividades escolhidas sobre todos os subproblemas serão estritamente crescentes ao longo do tempo. Além disso, podemos considerar cada atividade apenas uma vez de modo geral, na ordem monotonicamente crescente de tempos de término.

44

45 of 168

Um problema de seleção de atividade

A atividade am que escolhemos ao resolver um subproblema é sempre aquela com o tempo de término mais antigo que pode ser programado legalmente. A atividade escolhida é portanto uma escolha "gulosa" no sentido de que, intuitivamente, ela deixa tanta oportunidade quanto possível para que as atividades restantes sejam programadas. Isto é, a escolha gulosa é a que maximiza a quantidade de tempo restante não programado.

45

46 of 168

Um algoritmo recursivo

46

47 of 168

Um algoritmo guloso recursivo

Nossa solução recursiva direta: RECURSIVE-ACTIVITY-SELECTOR. Ele toma os tempos de início e término das atividades, representados como arranjos s e f, bem como os índices iniciais i e j do subproblema Si,j que ele deve resolver. O procedimento retorna um conjunto de tamanho máximo de atividades mutuamente compatíveis em Si,j.

47

48 of 168

Um algoritmo guloso recursivo

Supomos que as n atividades de entrada estão ordenados por tempo de término monotonicamente crescente, de acordo com a equação (1). Se não, podemos ordená-las nessa ordem no tempo O(n log n), dividindo os intervalos arbitrariamente. A chamada inicial é RECURSIVE-ACTIVITY-SELECTOR(s,f,0,n+1).

48

49 of 168

Um algoritmo guloso recursivo

RECURSIVE-ACTIVITY-SELECTOR(s,f,i,j)

m = i+1

while m < j and sm < fi do // Encontrar a 1a atividade em Sij.

m = m+1

if m < j then

return {am} <- RECURSIVE-ACTIVITY-SELECTOR(s,f,m,j)

else return {}

49

50 of 168

Um algoritmo guloso recursivo

Em uma dada chamada recursiva RECURSIVE-ACTIVITY-SELECTOR(s,f,i,j), o laço while procura pela primeira atividade em Sij. O laço examina ai+1, ai+2, ..., aj-1, até encontrar a primeira atividade am que seja compatível com ai; tal atividade tem sm >= fi.

RECURSIVE-ACTIVITY-SELECTOR(s,f,i,j)

m = i+1

while m < j and sm < fi do // Encontrar a 1a atividade em Sij.

m = m+1

50

51 of 168

Um algoritmo guloso recursivo

Se o laço terminar porque encontra tal atividade, o procedimento retorna a união de {am} com o subconjunto de tamanho máximo de Smj retornado pela chamada recursiva RECURSIVE-ACTIVITY-SELECTOR(s,f,m,j).

RECURSIVE-ACTIVITY-SELECTOR(s,f,i,j)

...

if m < j then

return {am} <- RECURSIVE-ACTIVITY-SELECTOR(s,f,m,j)

51

52 of 168

Um algoritmo guloso recursivo

Como outra alternativa, o laço pode terminar porque m >= j, e nesse caso examinamos todas as atividades cujos tempos de término são anteriores a de aj sem encontrar uma que seja compatível com ai. Nesse caso, Sij = {}, e então o procedimento retorna {}.

RECURSIVE-ACTIVITY-SELECTOR(s,f,i,j)

...

else return {}

52

53 of 168

Um algoritmo guloso recursivo

Supondo que as atividades já foram ordenadas por tempo de término, o tempo de execução da chamada RECURSIVE-ACTIVITY-SELECTOR(s, f, 0, n+1) é Θ(n). Em todas as chamadas recursivas, cada atividade é examinada exatamente uma vez no teste do laço while. Em particular, a atividade ak é examinada na última chamada feita em que i < k.

53

54 of 168

Um algoritmo iterativo

54

55 of 168

Um algoritmo guloso iterativo

Podemos converter facilmente nosso procedimento recursivo em um iterativo. O procedimento RECURSIVE-ACTIVITY-SELECTOR termina com uma chamada recursiva a ele próprio (recursivo de final), seguida de uma operação de união.

55

56 of 168

Um algoritmo guloso iterativo

Em geral, é uma tarefa direta transformar um procedimento recursivo de final para uma forma iterativa; de fato, alguns compiladores de certas linguagens de programação executam essa tarefa automaticamente. RECURSIVE-ACTIVITY-SELECTOR funciona para qualquer subproblema Sij, mas vimos que só é preciso considerar subproblemas para os quais j = n+1, isto é, subproblemas que consistem nas últimas atividades a terminar.

56

57 of 168

Um algoritmo guloso iterativo

O procedimento GREEDY-ACTIVITY-SELECTOR é uma versão iterativa do procedimento RECURSIVE-ACTIVITY-SELECTOR. Ele também pressupõe que as atividades de entrada estão ordenadas por tempo de término monotonicamente crescente. Ele reúne atividades selecionadas em um conjunto A e retorna esse conjunto quando termina.

57

58 of 168

Um algoritmo guloso iterativo

GREEDY-ACTIVITY-SELECTOR(s,f)

n = comprimento[s]

A = {1}

i = 1

for m = 2 to n do

if sm >= fi then

A = A ∪ {am}

i = m

return A

58

59 of 168

Um algoritmo guloso iterativo

O procedimento funciona da seguinte maneira: a variável i indexa a adição mais recente a A, correspondendo à atividade ai na versão recursiva. Como as atividades são consideradas em ordem de tempo monotonicamente crescente, fi é sempre o tempo de término máximo de qualquer atividade em A. Isto é,

fi = max{fk : ak ∈ A}. (4)

59

60 of 168

Um algoritmo guloso iterativo

GREEDY-ACTIVITY-SELECTOR(s,f)

n = comprimento[s]

A = {1}

i = 1

for m = 2 to n do

if sm >= fi then

A = A ∪ {am}

i = m

return A

60

61 of 168

Um algoritmo guloso iterativo

As linhas 2 e 3 selecionam a atividade a1, inicializam A para conter apenas essa atividade e inicializam i para indexar essa atividade.

GREEDY-ACTIVITY-SELECTOR(s,f)

n = comprimento[s]

A = {1}

i = 1

61

62 of 168

Um algoritmo guloso iterativo

O laço for encontra a atividade mais antiga a terminar em Si,n+1. O laço considera cada atividade am por sua vez e adiciona am a A se ela é compatível com todas as atividades selecionadas anteriormente; tal atividade é a mais antiga a terminar em Si,n+1.

62

63 of 168

Um algoritmo guloso iterativo

Para ver se a atividade am é compatível com cada atividade presente no momento em A, é suficiente pela equação (4) verificar se seu tempo de início sm não é anterior ao tempo de término de fi da atividade mais recentemente adicionada a A. Se a atividade é compatível, ela é adicionada a A e i é definido como m.

63

64 of 168

Um algoritmo guloso iterativo

GREEDY-ACTIVITY-SELECTOR(s,f)

n = comprimento[s]

A = {1}

i = 1

for m = 2 to n do

if sm >= fi then

A = A ∪ {am}

i = m

return A

64

65 of 168

Um algoritmo guloso iterativo

O conjunto A retornado pela chamada GREEDY-ACTIVITY-SELECTOR(s,f) é justamente o conjunto retornado pela chamada RECURSIVE-ACTIVITY-SELECTOR(s,f,0,n+1).

65

66 of 168

Um algoritmo guloso iterativo

Como a versão recursiva, o procedimento GREEDY-ACTIVITY-SELECTOR programa um conjunto de n atividades no tempo Θ(n), supondo-se que as atividades já tenham sido ordenadas inicialmente por seus tempos de término.

66

67 of 168

Elementos da estratégia gulosa

67

68 of 168

Elementos da estratégia gulosa

Um algoritmo guloso obtém uma solução ótima para um problema fazendo uma sequência de escolhas.

Para cada ponto de decisão do algoritmo, a opção que parece melhor no momento é escolhida.

68

69 of 168

Elementos da estratégia gulosa

Essa estratégia de heurística nem sempre produz uma solução ótima mas, algumas vezes ela funciona (conforme para o problema de seleção de atividades). Discutiremos agora algumas propriedades gerais de métodos gulosos.

69

70 of 168

Elementos da estratégia gulosa

O processo que seguimos no exemplo de seleção de atividades para desenvolver um algoritmo guloso foi um pouco mais complicado que o normal.

70

71 of 168

Elementos da estratégia gulosa

Seguimos estas etapas:

1. Determinar a subestrutura ótima do problema.

2. Desenvolver uma solução recursiva.

3. Verificar que, em qualquer fase da recursão, uma das escolhas ótimas é a escolha gulosa.

71

72 of 168

Elementos da estratégia gulosa

4. Mostrar que todos os subproblemas induzidos por ter sido feita a escolha gulosa, exceto um, são vazios.

5. Desenvolver um algoritmo recursivo que implemente a estratégia gulosa.

6. Converter o algoritmo recursivo em um algoritmo iterativo.

72

73 of 168

Elementos da estratégia gulosa

Seguindo estas etapas, vimos em detalhes as características de PD de um algoritmo guloso.

Contudo, na prática, normalmente simplificamos as etapas anteriores durante o projeto de um algoritmo guloso. Desenvolvemos nossa subestrutura com uma intenção de fazer uma escolha gulosa que deixe apenas um subproblema para resolver de forma ótima.

73

74 of 168

Elementos da estratégia gulosa

Por exemplo, no problema de seleção de atividades, primeiro definimos os subproblemas Sij, onde tanto i quanto j variavam. Em seguida, descobrimos que, se sempre fizéssemos a escolha gulosa, poderíamos restringir os subproblemas à forma Si,n+1.

74

75 of 168

Elementos da estratégia gulosa

Alternativamente, poderíamos ter adaptado a subestrutura ótima com uma escolha gulosa em mente. Isto é, poderíamos ter descartado o segundo subscrito e definido subproblemas da forma Si = {ak ∈ S: fi <= sk}. Então, poderíamos ter provado que uma escolha gulosa (a primeira atividade a terminar em Si), combinada com uma solução ótima para o conjunto restante Sm de atividades compatíveis, produz uma solução ótima para Si.

75

76 of 168

Elementos da estratégia gulosa

De modo mais geral, projetamos algoritmos gulosos de acordo com a seguinte sequência de etapas:

1. Moldar o problema de otimização como um problema no qual fazemos uma escolha e ficamos com um único subproblema para resolver.

76

77 of 168

Elementos da estratégia gulosa

2. Provar que sempre existe uma solução ótima para o problema original que faz a escolha gulosa, de modo que a escolha gulosa é sempre segura.

77

78 of 168

Elementos da estratégia gulosa

3. Demonstrar que, tendo feito a escolha gulosa, o que resta é um subproblema com a propriedade de que, se combinarmos uma solução ótima para o subproblema com a escolha gulosa que fizemos, chegamos a uma solução ótima para o problema original.

78

79 of 168

Elementos da estratégia gulosa

Apesar disso, embaixo de todo algoritmo guloso, quase sempre há uma solução de programação dinâmica (PD) mais incômoda.

Então surge a pergunta:

Como saber se um algoritmo guloso resolverá um determinado problema de otimização?

79

80 of 168

Elementos da estratégia gulosa

Em geral, não há como saber, mas existem dois ingredientes que são exibidos pela maioria dos problemas que se prestam a uma estratégia gulosa: a propriedade de escolha gulosa e a subestrutura ótima.

80

81 of 168

Elementos da estratégia gulosa

Se pudermos demonstrar que o problema tem essas propriedades, estaremos no bom caminho para desenvolver um algoritmo guloso para ele.

81

82 of 168

Propriedade de escolha gulosa

82

83 of 168

Propriedade de escolha gulosa

A primeira propriedade chave é a propriedade de escolha gulosa: uma solução globalmente ótima pode ser alcançada fazendo-se uma escolha localmente ótima (gulosa).

Em outras palavras, quando estamos considerando que escolha fazer, efetuamos a escolha que parece melhor no problema atual, sem considerar resultados de subproblemas.

83

84 of 168

Propriedade de escolha gulosa

É nesse ponto que os algoritmos gulosos diferem da programação dinâmica.

Na programação dinâmica, fazemos uma escolha em cada passo, mas a escolha pode depender das soluções para subproblemas. Consequentemente, resolvemos problemas de PD de baixo para cima, progredindo de subproblemas menores para subproblemas maiores.

84

85 of 168

Propriedade de escolha gulosa

Em um algoritmo guloso, fazemos qualquer escolha que pareça melhor no momento e depois resolvemos o subproblema que surge após a escolha ser feita.

85

86 of 168

Propriedade de escolha gulosa

A escolha pode depender das escolhas até o momento, mas não pode depender de nenhuma escolha futura ou das soluções para subproblemas. Desse modo, diferente da PD, que resolve os subproblemas de baixo para cima, uma estratégia gulosa em geral progride de cima para baixo, fazendo uma escolha gulosa após outra, reduzindo de modo iterativo cada instância do problema dado a um problema menor.

86

87 of 168

Propriedade de escolha gulosa

Devemos provar que uma escolha gulosa em cada passo produz uma solução globalmente ótima, e é nesse ponto que pode ser necessária alguma inteligência. Normalmente, como no caso do Teorema 1, a prova examina uma solução globalmente ótima para algum subproblema.

87

88 of 168

Propriedade de escolha gulosa

Em seguida, ela mostra que a solução pode ser modificada para usar a escolha gulosa, resultando em um subproblema similar, embora menor.

88

89 of 168

Propriedade de escolha gulosa

A propriedade de escolha gulosa frequentemente nos oferece alguma eficiência ao fazermos nossa escolha em um subproblema.

Por exemplo, no problema de seleção de atividades, supondo que já ordenamos as atividades em ordem monotonicamente crescente de tempos de término, precisamos examinar cada atividade apenas uma vez.

89

90 of 168

Propriedade de escolha gulosa

Com frequência, pelo pré-processamento da entrada ou usando uma estrutura de dados apropriada (muitas vezes, uma fila de prioridades), podemos fazer escolhas gulosas rapidamente, produzindo desse modo um algoritmo eficiente.

90

91 of 168

Subestrutura ótima

91

92 of 168

Subestrutura ótima

Um problema exibe subestrutura ótima se uma solução ótima para o problema contém dentro dela soluções ótimas para subproblemas.

Essa propriedade é crucial para avaliação do uso de PD, como também de algoritmos gulosos.

92

93 of 168

Subestrutura ótima

Como exemplo de subestrutura ótima, vimos que se uma solução ótima para o subproblema Sij inclui uma atividade ak, então ela também deve conter soluções ótimas para os subproblemas Sik e Skj.

93

94 of 168

Subestrutura ótima

Dada essa subestrutura ótima, demonstramos que, se soubéssemos que atividade usar como ak, poderíamos construir uma solução ótima para Sij selecionando ak juntamente com todas as atividades em soluções ótimas para os subproblemas Sik e Skj.

94

95 of 168

Subestrutura ótima

Com base nisso, pudemos escrever a recorrência que descreveu o valor de uma solução ótima.

95

96 of 168

Subestrutura ótima

Normalmente usamos uma abordagem mais direta relativa à subestrutura ótima quando a aplicamos a algoritmos gulosos. Conforme já dito, nos demos o luxo de pressupor que chegamos a um subproblema tendo feito a escolha gulosa no problema original.

96

97 of 168

Subestrutura ótima

Na realidade, tudo que precisamos fazer é demonstrar que uma solução ótima para o subproblema, combinada com a escolha gulosa já feita, produz uma solução ótima para o problema original. Esse esquema utiliza implicitamente a indução sobre os subproblemas para provar que fazer a escolha gulosa em cada etapa produz uma solução ótima.

97

98 of 168

Estratégia Gulosa

vs

Programação Dinâmica

98

99 of 168

Estratégia Gulosa vs Programação Dinâmica

Pelo fato da propriedade de subestrutura ótima ser explorada tanto por estratégias gulosas quanto por estratégias de programação dinâmica, alguém poderia ser tentado a gerar uma solução de programação dinâmica para um problema quando uma solução gulosa fosse suficiente, ou alguém poderia pensar erroneamente que uma solução gulosa funcionaria, quando de fato fosse necessária uma solução de programação dinâmica.

99

100 of 168

Estratégia Gulosa vs Programação Dinâmica

Para ilustrar as sutilezas entre as duas técnicas, vamos investigar duas variantes de um problema clássico de otimização.

100

101 of 168

Estratégia Gulosa vs Programação Dinâmica

O problema da mochila 0-1 é apresentado como a seguir.

Um ladrão que rouba uma loja encontra n itens: o i-ésimo item vale vi reais e pesa wi quilos, onde vi e wi são inteiros. Ele deseja levar uma carga tão valiosa quanto possível, mas pode carregar no máximo W quilos em sua mochila, para algum inteiro W.

101

102 of 168

Estratégia Gulosa vs Programação Dinâmica

Que itens ele deve levar? (Isso se chama problema da mochila 0-1 porque cada item deve ser levado ou deixado para trás; o ladrão não pode levar uma quantidade fracionária de um item ou levar um item mais de uma vez.)

102

103 of 168

Estratégia Gulosa vs Programação Dinâmica

No problema da mochila fracionária, a configuração é a mesma, mas o ladrão pode levar frações de itens, em vez de ter de fazer uma escolha binária (0-1) para cada item. Você pode imaginar um item no problema da mochila 0-1 como sendo algo semelhante a um lingote de ouro, enquanto um item no problema da mochila fracionária é semelhante ao ouro em pó.

103

104 of 168

Estratégia Gulosa vs Programação Dinâmica

Ambos os problemas de mochilas exibem a propriedade de subestrutura ótima.

No caso do problema 0-1, considere a carga mais valiosa que pesa no máximo W quilos. Se removermos o item j dessa carga, a carga restante deve ser a carga mais valiosa que pese no máximo W - wj que o ladrão pode levar dos n-1 itens originais, excluindo j.

104

105 of 168

Estratégia Gulosa vs Programação Dinâmica

No caso do problema fracionário comparável, considere que, se removermos um peso w de um item j da carga ótima, a carga restante deve ser a carga mais valiosa que pese no máximo W - w que o ladrão pode levar dos n-1 itens originais, mais wj - w do item j.

105

106 of 168

Estratégia Gulosa vs Programação Dinâmica

Embora os problemas sejam semelhantes, o problema da mochila fracionária pode ser resolvido por uma estratégia gulosa, enquanto o problema 0-1 não pode. Para resolver o problema fracionário, primeiro calculamos o valor por quilo vi/wi para cada item.

106

107 of 168

Estratégia Gulosa vs Programação Dinâmica

Obedecendo a uma estratégia gulosa, o ladrão começa levando o máximo possível do item com o maior valor por quilo. Se o suprimento desse item se esgotar e ele ainda puder levar mais, o ladrão levará tanto quanto possível do item com o próximo maior valor por quilo e assim por diante até não poder levar mais nada.

107

108 of 168

Estratégia Gulosa vs Programação Dinâmica

Desse modo, ordenando os itens pelo valor por quilo, o algoritmo guloso é executado no tempo O(n log n).

108

109 of 168

Estratégia Gulosa vs Programação Dinâmica

Para ver que essa estratégia gulosa não funciona no caso do problema da mochila 0-1, consideremos a instância do problema ilustrada a seguir.

109

(a) (b) (c)

110 of 168

Estratégia Gulosa vs Programação Dinâmica

Existem 3 itens, e a mochila pode conter 50 quilos. O item 1 pesa 10 quilos e vale R$ 60. O item 2 pesa 20 quilos e vale R$ 100. O item 3 pesa 30 quilos e vale R$ 120. Desse modo, o valor por quilo do item 1 é R$ 6 por quilo, que é maior que o valor por quilo do item 2 (R$ 5/kg) ou do item 3 (R$ 4/kg).

110

111 of 168

Estratégia Gulosa vs Programação Dinâmica

Assim, a estratégia gulosa levaria o item 1 primeiro. Porém, como podemos ver na análise de caso da figura (b), a solução ótima leva os itens 2 e 3, deixando o item 1 de fora.

111

(b)

112 of 168

Estratégia Gulosa vs Programação Dinâmica

As duas soluções possíveis que envolvem o item 1 não são ótimas.

112

(b)

113 of 168

Estratégia Gulosa vs Programação Dinâmica

Contudo, para o problema fracionário comparável, a estratégia gulosa, que leva o item 1 primeiro, produz uma solução ótima, conforme a figura (c).

113

(c)

114 of 168

Estratégia Gulosa vs Programação Dinâmica

Levar o item 1 não funciona no problema 0-1, porque o ladrão é incapaz de preencher plenamente sua mochila, e o espaço vazio diminui o valor efetivo por quilo de sua carga.

114

115 of 168

Estratégia Gulosa vs Programação Dinâmica

No problema 0-1, quando consideramos um item para inclusão na mochila, devemos comparar a solução para o subproblema no qual o item é incluído com a solução para o subproblema no qual o item é excluído, antes de podermos fazer a escolha.

115

116 of 168

Estratégia Gulosa vs Programação Dinâmica

O problema formulado desse modo ocasiona muitos subproblemas superpostos - um caso legítimo de programação dinâmica e, de fato, a programação dinâmica pode ser usada para resolver o problema 0-1.

116

117 of 168

Códigos de Huffman

117

118 of 168

Códigos de Huffman

Os códigos de Huffman constituem uma técnica amplamente utilizada e muito eficiente para comprimir dados, guardando a relação típica de 20 a 90%, dependendo das características dos dados que estão sendo comprimidos.

118

119 of 168

Códigos de Huffman

O algoritmo guloso de Huffman utiliza uma tabela das frequências de ocorrência dos caracteres para elaborar um modo ótimo de representar cada caractere como uma cadeia binária.

119

120 of 168

Códigos de Huffman

Vamos supor que temos um arquivo de dados de 100.000 caracteres que desejamos armazenar em forma compacta. Observamos que os caracteres no arquivo ocorrem com frequências dadas na figura a seguir.

120

121 of 168

Códigos de Huffman

Isto é, aparecem somente seis caracteres diferentes, e o caractere a ocorre 45.000 vezes.

Há muitas maneiras de representar um arquivo de informações como esse. Consideramos o problema de projetar um código de caracteres binários (ou código) no qual cada caractere é representado por uma cadeia binária única.

121

122 of 168

Códigos de Huffman

Se usarmos um código de comprimento fixo, precisaremos de 3 bits para representar seis caracteres: a = 000, b = 001, ..., f = 101. Esse método exige 300.000 bits para codificar o arquivo inteiro.

Podemos fazer algo melhor?

122

123 of 168

Códigos de Huffman

Um código de comprimento variável pode funcionar consideravelmente melhor que um código de comprimento fixo, fornecendo palavras de código curtas a caracteres frequentes e palavras de código longas a caracteres pouco frequentes.

123

124 of 168

Códigos de Huffman

A figura mostra um código desse tipo; aqui, a cadeia de 1 bit 0 representa a, e a cadeia de 4 bits 1100 representa f.

124

125 of 168

Códigos de Huffman

Esse código exige

(45*1 + 13*3 + 12*3 + 16*3 + 9*4 + 5*4)*1.000 = 224.000 bits

para representar o arquivo, uma economia de aproximadamente 25%. De fato, esse é um código de caracteres ótimo para esse arquivo.

125

126 of 168

Códigos de Huffman - códigos de prefixo

Consideramos aqui apenas códigos nos quais nenhuma palavra de código é também um prefixo de alguma outra palavra de código. Tais códigos são chamados códigos de prefixo.

126

127 of 168

Códigos de Huffman - códigos de prefixo

É possível mostrar que a compressão de dados ótima que se pode obter por meio de um código de caracteres sempre pode ser alcançada com um código de prefixo, e assim não há perda de generalidade em restringirmos nossa atenção a códigos de prefixo.

127

128 of 168

Códigos de Huffman - códigos de prefixo

A codificação é sempre simples para qualquer código de caracteres binários; simplesmente concatenamos as palavras de código que representam cada caractere do arquivo. Por exemplo, com o código de prefixo de comprimento variável da figura, codificamos o arquivo de 3 caracteres abc como 0.101.100 = 10101100, onde usamos "." para denotar a concatenação.

128

129 of 168

Códigos de Huffman - códigos de prefixo

Os códigos de prefixo são desejáveis porque simplificam a decodificação. Como nenhuma palavra de código é um prefixo de qualquer outra, a palavra de código que inicia um arquivo codificado não é ambígua.

129

130 of 168

Códigos de Huffman - códigos de prefixo

Podemos simplesmente identificar a palavra de código inicial, traduzi-la de volta para o caractere original, removê-la do arquivo codificado e repetir o processo de decodificação no restante do arquivo codificado.

130

131 of 168

Códigos de Huffman - códigos de prefixo

O processo de decodificação precisa de uma representação conveniente para o código de prefixo, de forma que a palavra de código inicial possa ser extraída com facilidade. Uma árvore binária cujas folhas são os caracteres dados fornece tal representação.

131

132 of 168

Códigos de Huffman - códigos de prefixo

Interpretamos a palavra de código binária para um caractere como o caminho desde a raiz até esse caractere, onde 0 significa "vá para o filho da esquerda" e 1 significa "vá para o filho da direita".

132

133 of 168

Códigos de Huffman - códigos de prefixo

Um código ótimo para um arquivo é sempre representado por uma árvore binária cheia, na qual cada nó que não é uma folha tem dois filhos.

133

134 of 168

Códigos de Huffman - códigos de prefixo

Tendo em vista que podemos restringir nossa atenção a árvores binárias completas, dizemos que, se C é o alfabeto do qual os caracteres são obtidos e todas as frequências de caracteres são positivas, então a árvore para um código de prefixo ótimo tem exatamente |C| folhas, uma para cada letra do alfabeto, e |C| - 1 nós internos.

134

135 of 168

A construção de um código de Huffman

Huffman criou um algoritmo guloso que produz um código de prefixo ótimo chamado código de Huffman.

No pseudocódigo a seguir, supomos que C é um conjunto de n caracteres e que cada caractere c ∊ C é um objeto com uma frequência definida f[c].

135

136 of 168

A construção de um código de Huffman

O algoritmo constrói de baixo para cima a árvore T correspondente ao código ótimo. Ele começa com um conjunto de |C| folhas e executa uma sequência de |C|-1 operações de "intercalação" para criar a árvore final. Uma fila de prioridade mínima Q, tendo f como chave, é usada para identificar os dois objetos menos frequentes a serem intercalados.

136

137 of 168

A construção de um código de Huffman

O resultado da intercalação de dois objetos é um novo objeto cuja frequência é a soma das frequências dos dois objetos que foram intercalados.

137

138 of 168

A construção de um código de Huffman

HUFFMAN(C)

n = |C|

Q = C

for i = 1 to n-1 do

alocar um novo nó z

esquerda[z] = x = EXTRACT-MIN(Q)

direita[z] = y = EXTRACT-MIN(Q)

f[z] = f[x] + f[y]

INSERT(Q, z)

return EXTRACT-MIN(Q) // retornar a raiz da árvore

138

139 of 168

A construção de um código de Huffman

Em nosso exemplo, o algoritmo de Huffman procede do seguinte modo.

139

140 of 168

A construção de um código de Huffman

Tendo em vista que existem 6 letras no alfabeto, o tamanho da fila inicial é n = 6, e 5 passos de intercalação são exigidos para construir a árvore. A árvore final representa o código de prefixo ótimo. A palavra de código para uma letra é a sequência de etiquetas de arestas no caminho da raiz até a letra.

140

141 of 168

A construção de um código de Huffman

A linha 2 inicializa a fila de prioridades Q com os caracteres em C.

HUFFMAN(C)

n = |C|

Q = C

141

142 of 168

A construção de um código de Huffman

O laço for extrai repetidamente os dois nós x e y de frequência mais baixa da fila e os substitui na fila por um novo nó z representando sua intercalação. A frequência de z é calculada como a soma das frequências de x e y.

for i = 1 to n-1 do

alocar um novo nó z

esquerda[z] = x = EXTRACT-MIN(Q)

direita[z] = y = EXTRACT-MIN(Q)

f[z] = f[x] + f[y]

142

143 of 168

A construção de um código de Huffman

O nó z tem x como seu filho da esquerda e y como seu filho da direita. Depois de n-1 intercalações, o único nó da esquerda na fila - a raiz da árvore de código - é retornado.

return EXTRACT-MIN(Q) // retornar a raiz da árvore

143

144 of 168

A construção de um código de Huffman

A análise do tempo de execução do algoritmo de Huffman supõe que Q é implementada como um heap binário. Para um conjunto C de n caracteres, a inicialização de Q na linha 2 pode ser executada em tempo O(n) usando-se o procedimento BUILD-MIN-HEAP.

144

145 of 168

A construção de um código de Huffman

O laço for é executado exatamente n-1 vezes e, como cada operação de heap exige o tempo O(log n), o laço contribui com O(n log n) para o tempo de execução.

Desse modo, o tempo de execução total de HUFFMAN em um conjunto de n caracteres é O(n log n).

145

146 of 168

Fundamentos teóricos de métodos gulosos

146

147 of 168

Fundamentos teóricos de métodos gulosos

A teoria sobre algoritmos gulosos é útil para determinarmos quando o método produz uma solução ótima. Ela envolve estruturas combinatórias conhecidas como "matróides".

147

148 of 168

Fundamentos teóricos de métodos gulosos

Embora essa teoria não cubra todos os casos aos quais um método guloso se aplica, ela envolve muitos casos de interesse prático. Além disso, essa teoria está sendo rapidamente desenvolvida e estendida para cobrir muitas outras aplicações.

148

149 of 168

Matróides

Um matróide é um par ordenado M = (S, l) que satisfaz às condições a seguir:

1. S é um conjunto finito não vazio.

149

150 of 168

Matróides

2. l é uma família não vazia de subconjuntos de S, chamados subconjuntos independentes de S, tais que, se B ∊ l e A ⊆ B, então A ∊ l. Dizemos que l é hereditário se ele satisfaz a essa propriedade. Observe que o conjunto vazio ∅ é necessariamente um membro de l.

150

151 of 168

Matróides

3. Se A ∊ l, B ∊ l e |A| < |B|, então existe algum elemento x ∊ B - A tal que A U {x} ∊ l. Dizemos que M satisfaz à propriedade de troca.

151

152 of 168

Matróides

O termo matróide se deve a Hassler Whitney. Ele estava estudando matróides de matrícula, nos quais os elementos de S são as linhas de uma dada matriz, e um conjunto de linhas é independente se elas são linearmente independentes no sentido usual.

152

153 of 168

Matróides

Como outro exemplo de matróides, considere o matróide gráfico MG = (SG, lG), definido em termos de um grafo não orientado G = (V, E):

- O conjunto SG é definido como E, o conjunto de arestas de G.

- Se A é um subconjunto de E, então A ∊ lG se e somente se A é acíclico.

153

154 of 168

Matróides

O matróide gráfico está relacionado com o problema da árvore de amplitude mínima.

Teorema: Se G é um grafo não orientado, então MG = (SG, lG) é um matróide.

154

155 of 168

Matróides

Teorema: Todos os subconjuntos independentes máximos em um matróide têm o mesmo tamanho.

155

156 of 168

Algoritmos gulosos em um matróide ponderado

Muitos problemas para os quais uma abordagem gulosa fornece soluções ótimas podem ser formulados em termos de encontrar um subconjunto independente de peso máximo em um matróide ponderado.

156

157 of 168

Algoritmos gulosos em um matróide ponderado

Isto é, temos um matróide ponderado M = (S, l) e desejamos encontrar um conjunto independente A ∊ l tal que w(A) seja maximizado. Chamamos tal subconjunto de subconjunto ótimo do matróide.

157

158 of 168

Resumindo...

158

159 of 168

Algoritmos Gulosos

Como os algoritmos de programação dinâmica, os algoritmos gulosos em geral se aplicam a problemas de otimização em que diversas escolhas devem ser feitas, a fim de se chegar a uma solução ótima. A ideia de um algoritmo guloso é fazer cada escolha de uma maneira ótima para as condições locais (a escolha mais apetitosa).

159

160 of 168

Algoritmos Gulosos

Um exemplo é a troca de moedas: para minimizar o número de moedas necessárias para trocar uma quantia dada, basta selecionar repetidamente a moeda de maior valor de face que não seja maior que a quantia ainda devida.

160

161 of 168

Algoritmos Gulosos

Existem muitos problemas desse tipo para os quais uma abordagem gulosa fornece uma solução ótima muito mais depressa que uma abordagem de programação dinâmica. Contudo, nem sempre é fácil determinar se uma abordagem gulosa será efetiva.

161

162 of 168

Referências

162

163 of 168

Referências

T. Cormen, C. Leiserson, R. Rivest, C. Stein. Algoritmos - Teoria e Prática (tradução da 2ª Ed. Americana), Ed. Campus (2002).

Capítulo 16

163

164 of 168

Referências

E. Horowitz, S. Sahni, S. Rajasekaran. Computer Algorithms.

Capítulo 4

M. Soltys. An Introduction to the Analysis of Algorithms. 2nd edition.

Capítulo 2

164

165 of 168

Outros materiais

165

166 of 168

Links interessantes

166

167 of 168

Links interessantes

Vídeos ilustrando alguns problemas

Map Coloring: https://www.youtube.com/watch?v=vGjsi8NIpSE

Job Scheduling Problem: https://www.youtube.com/watch?v=Hq4VrKqAb88

167

168 of 168

Técnicas de Análise de Algoritmo

Algoritmos Gulosos

Alysson Milanez