Análise e Técnicas de Algoritmos
Período 2003.1
Problemas de Otimização e Estratégia Gulosa
- Problemas que podem apresentar diversas soluções.
- A solução é uma sequência de decisões.
- Um valor pode ser associado para cada solução.
- Achar a solução com valor ótimo.
- Descrição: Seja
um conjunto de
tarefas.
Associamos à cada tarefa um tempo de execução. Fazer
o escalonamento dessas tarefas.
- Problema: Minimizar o tempo médio de finalização
das tarefas.
- Sequência de Decisões: Escolher primeira tarefa,
escolher a segunda tarefa, ...
- Problema solucionado em diferentes passos.
- As decisões são tomadas de forma isolada, em cada passo.
- Estratégia de pegar o melhor no momento (solução ótima
local).
- Quando o algoritmo termina, espera-se que tenha ocorrido a
melhor solução.
- Com este tipo de algoritmo, podemos achar uma solução ótima
para alguns problemas, mas não para todos.
- Sejam
e
o montante total.
- Algoritmo Greedy: No passo
, escolher
, tal
que
e
e subtrair
de
para
o próximo passo.
- Podemos provar que, para este conjunto de moedas
, o algoritmo
greedy fornece uma solução ótima.
- Para
, não funciona
(exemplo:
).
- Seja um conjunto de tarefas
com
tempos de execução
, respectivamente.
- Considerar um único processador e alocação não-preemptiva.
- Qual a melhor forma de alocar essas tarefas para minimizar
o tempo médio de execução?
- Solução 1: ordem de chegada.
- Solução 2: ordem crescente do tempo de execução.
- Usar
para
ilustrar.
- A solução 2 sempre apresenta solução ótima:
- Vamos supor que temos a seguinte sequência como
solução.
.
-
.
-
.
-
.
- Vamos supor que exista uma ordenação em que
e
.
- Se fizermos a troca, o custo total vai decrescer.
- Mesma definição do problema anterior.
- Diferença: considera
processadores.
- Assumir que os tempos estão em ordem crescente de tempo
de execução.
- Exemplo:
e
.
- Solução: começar a alocação em ordem, fazendo um rodízio
entre os processadores.
- Outros arranjos podem ser efetuados
- Prova é semelhante.
- Mesma definição do problema anterior.
- O objetivo é minimizar o tempo final, ou seja, o tempo em
que as tarefas terminam.
- No problema anterior foi 40 e 38.
- Solução:
.
- O tempo médio não é mínimo mas, a sequência inteira
termina mais cedo.
- Esse problema é mais complicado de se resolver.
- Seja
um conjunto de
atividades
que desejam usar um mesmo recurso.
- Cada atividade
tem um tempo de começo (
) e um tempo
de término (
),
.
- Duas atividades
e
são compatíveis se elas não se
sobrepõem, ou seja, se
ou
.
- Assumir que as atividades estão em ordem crescente do tempo
de término.
- Arrays para representar
e
.
- Prova de que está correto: mostrar que existe uma solução ótima
que começa com a atividade
.
- Um algoritmo guloso obtém uma solução ótima para um problema,
fazendo uma sequência de escolhas.
- Nem sempre produz uma solução ótima.
- Não existe uma regra geral que indique que um algoritmo guloso
resolva um determinado problema de otimização.
- Bom indício é dado por 2 elementos:
- Propriedade de escolha gulosa.
- Sub-estrutura ótima.
Considere
objetos com pesos
e uma mochila de
capacidade
. Se uma fração
do objeto
for colocada na mochila, um lucro de
é conseguido. Como maximizar
o lucro.
Vamos usar o seguinte exemplo:
Solução 1: Maximizar o lucro a cada passo
Escolher o objeto com
máximo e colocar na mochila (
). No
último passo, quando a mochila está quase cheia, escolher o objeto
, tal
que
complete a capacidade da mochila e
seja máximo.
- Escolher o de maior lucro (
). O lucro até então
é 25 e a capcidade restante da mochila é 2.
- O objeto 2 não cabe por inteiro. Usar
que dá um lucro de
3,2.
- A solução
permite um lucro de 28,2.
Solução 2: Conserva a capacidade
Escolher os objetos em ordem crescente de peso (
), maximizando a
capacidade restante.
-
. A capacidade restante é 10.
-
. Não é possível mais selecionar objetos.
- A solução
permite um lucro de 31.
Solução 3: Maximiza o lucro por unidade de peso
Escolher os objetos em ordem decrescente de
.
- O objeto que apresenta maior lucro por unidade de peso é o 2, seguido do
3 e do 1.
- Logo:
, capacidade restante é 5.
-
, capacidade restante é 0.
- A solução
permite um lucro de 31,5.
O Algoritmo
Primeiro ordenar os objetos em ordem decrescente
.
É possível provar que o terceiro algoritmo sempre fornece a solução ótima
para este problema da mochila.
Prova do Terceiro Algoritmo
O que devemos provar é que a solução fornecida pelo terceiro algoritmo
corresponde à solução com o maior lucro.
É importante, entretanto, ressaltar que pode existir várias soluções ótimas.
- Vamos supor que
é a solução fornecida pelo
algoritmo.
Se,
, essa solução é claramente a
ótima. Nesse caso, seria a única solução.
Se não for este o caso, vamos supor que
é o menor número onde
.
Do algoritmo temos:
-
-
-
Logo,
.
- Seja
uma solução de máximo lucro.
Temos que provar que
tem o mesmo lucro de
.
Se
, não tem o que fazer.
Caso contrário, vamos considerar que
é o menor número onde
.
Podemos então dizer que:
-
-
A estratégia da prova consiste em transformar
em
, mantendo o
lucro. Para tanto, vamos transformar
em uma solução mais intermediária
e que se parece mais com
, ou seja,
-
Antes de prosseguirmos com a prova, vamos fazer algumas considerações.
vamos investigar os seguintes casos:
Caso 1:
.
Nesse caso,
. Portanto,
deve ser menor do que
pois
.
Caso 2:
.
Pela definição de
,
. Se
,
Isso contradiz o fato de que
é uma solução. Logo,
.
Caso 3:
.
Nesse caso,
e
:
Também não é possível, logo o caso 3 nunca acontece.
Portanto, podemos assumir que
.
Voltando à prova, para sair de
para
temos que aumentar
fazendo
igual a
. e diminuir
como necessário, para fazer
com que o lucro se mantenha. Vamos dizer que a nova solução é
.
Portanto,
-
-
-
Então,
e
têm o mesmo lucro.
, entretanto, se parece mais com
pois
as
primeiras entradas de
são as mesmas de
.
O procedimento pode ser repetido até transformar
em
. Logo
também
é uma solução ótima.
Nessa seção vamos considerar uma aplicação de algoritmos gulosos, conhecida
como compressão de arquivos.
- Vamos considerar um arquivo de 100.000 caracteres, onde apenas os
caracteres
,
,
,
,
,
e
ocorrem com freqüência
de 15000, 25000, 22000, 7000, 5000, 23000, 3000.
- Usando um código para representar cada caracter:
- Se usarmos ASCII Extendido (8 bits), o número total de bits é
800.000.
- Se usarmos um código de tamanho fixo, 3 bits para representar
7 caracteres, o número total de bits é 300.000.
- Se usarmos um código de tamanho variável:
,
,
,
,
,
e
,
o número total de bits é 255.000.
- É possível reduzir 15% da solução 2 e 68% da solução 1.
A redução no número de bits foi possível pois a estratégia utilizada
considerou um código de tamanho variável, onde os caracteres com
freqüência maior eram codificados com um número menor de bits. Logo, se todos
os caracteres ocorrem com a mesma freqüência, é possível que não ocorra
nenhuma redução no número de bits utilizados.
Códigos de Huffman
- Utilizam códigos prefixos:
- Códigos onde nenhum código de caracter é prefixo de outro.
- Sempre é possível achar um código ótimo de compressão.
- Codificação e decodificação são fáceis.
- Utilização de uma árvore binária, com folhas representando os caracteres.
0 significa esquerda e 1 direita.
O Algoritmo de Huffman
- Vamos assumir que o número de caracteres é
.
- Manter uma floresta de árvores.
- O peso de uma árvore é a soma das freqüências de suas folhas.
vezes, selecionar as duas árvores de menor peso e formar
uma nova árvore.
- No início do algoritmo, existem
árvoes de apenas um nó.
- No final temos apenas uma única árvore e á a árvore com o código
de Huffman.
Vamos considerar a seguinte configuração inicial:
Resultado após o primeiro merge.
Resultado após o segundo merge.
Resultado após o terceiro merge.
Resultado após o quarto merge.
Resultado após o quinto merge.
Resultado final.
O código prefixo ótimo é:
Como apresentado, algoritmos gulosos são utilizados para resolver
problemas de otimização. Os algoritmos que se baseiam em uma estratégia
gulosa são, em geral, simples e de fácil implementação. A maioria dos
problemas de otimização que são resolvidos utilizando uma estratégia
gulosa, tem em geral a seguinte forma:
Uma instância consiste de um conjunto de objetos
e um relacionamento entre eles.
Uma solução é um subconjunto
destes objetos. A estratégia gulosa define qual a forma de escolher
os objetos da solução. Vale acrescentar que alguns subconjuntos
não são permitidos. Essa restrição é imposta pelo relacionamento
existente entre os objetos.
Cada subconjunto válido (sem conflito) como
solução tem associado um custo. Em geral, o custo é definido pelo
número de objetos do subconjunto, pela soma dos custos individuais
dos objetos, ou através de uma função mais complexa sobre os
elementos do subconjunto.
Dada uma instância de um problema, o objetivo é
maximizar (minimizar) o custo da solução.
Se uma solução força bruta é usada neste caso, é necessário considerar
todas as possíveis soluções de uma dada instância, computar o
custo de cada solução e escolher a de maior (menor) custo. Em geral, este
tipo de algoritmo é de ordem exponencial.
A escolha gulosa é aquela que salta aos olhos,
ou a que primeiro vem na mente quando imaginamos um algoritmo para o problema.
Dado o conjunto de objetos de entrada, a escolha gulosa seleciona um dos
objetos, seguindo um critério simples (aquele que parece ser o melhor).
Com base nestas definições, é possível
definir elementos que comumente fazem parte de uma solução gulosa, na
tentativa de definir uma cara genérica para uma solução gulosa.
Vamos considerar o
exemplo do cálculo do trôco, em um sistema monetário que apresenta o seguinte
conjunto de moedas
. Uma possível solução que
utiliza estratégia gulosa é apresentada abaixo:
A solução é obtida partindo-se de um conjunto vazio de moedas e, em cada etapa,
escollhendo a maior moeda possível. É possível identificar neste algoritmo os
seguintes elementos:
- Uma lista de candidatos, definido pelo conjunto
.
- Uma lista de elementos escolhidos, definido pelo conjunto
.
- Uma função SELEÇÃO que seleciona um dos candidatos. No nosso
exemplo é a uma função que retorna a maior moeda.
- Uma função VIABILIDADE que verifica a viabilidade da escolha. No exemplo, a
função que testa se a soma dos valores da nova moeda escolhida com
os valores das moedas previamentes escolhidas não ultrapassa o valor
do montante.
- Uma função SOLUÇÃO que verifica se um determinado conjunto de candidatos
é uma solução para o problema. Em nosso exemplo, é a função
que testa se a soma dos elementos escolhidos é igual ao montante
.
Além dos elementos listados, em algumas soluções é possível identificar mais
dois elementos: um conjunto de candidatos rejeitados e uma função que define
o valor de uma solução (serve para definir a eficiência de uma solução. No exemplo
do cálculo do trôco, essa função retorna o número de moedas utilizadas para
fornecer o trôco).
Um algoritmo guloso genérico define inicialmente um conjunto vazio de
candidatos escolhidos. Em cada etapa um novo elemento é escolhido da
lista de candidatos e adicionado ao conjunto de candidatos escolhidos.
Esta escolha é guiada pela função SELEÇÃO que através do crivo
da função VIABILIDADE define se o elemento selecionado deve ser considerado
ou rejeitado. Se o elemento for rejeitado ele não deve ser considerado
novamente. Se o elemento é adicionado ao conjunto dos elementos escolhidos,
a função SOLUÇÃO verifica se o novo conjunto corresponde a solução
do problema.
Jorge C. A. de Figueiredo
2003-05-27