1. O documento discute o problema da mochila valiosa, no qual objetos devem ser selecionados para levar na mochila considerando peso e valor.
2. Algoritmos como força bruta, programação dinâmica e gulosos são implementados para resolver o problema de forma exata ou aproximada.
3. Testes são realizados para comparar o desempenho dos algoritmos em termos de tempo de execução e qualidade da solução.
METAHEURÍSTICA GRASP APLICADA AO PROBLEMA DO SEQUENCIAMENTO DE TAREFAS
PROBLEMA DA MOCHILA VALIOSA COM VALOR MINIMO DE UTILIDADE
1. UNIVERSIDADE ESTADUAL DO CEARÁ
CENTRO DE CIÊNCIAS E TECNOLOGIA
CURSO DE GRADUAÇÃO EM CIÊNCIA DA COMPUTAÇÃO
JOÃO GONÇALVES FILHO & FELIPE DE ALMEIDA XAVIER
TRABALHO DE COMPLEXIDADE: PROBLEMA DA
MOCHILA VALIOSA
PROFESSOR: VALDISIO VIANA
FORTALEZA - CEARÁ
2012
2. RESUMO
Este trabalho aborda o Problema da Mochila Valiosa, em que temos que decidir dentre os objetos disponíveis quais levar para a viagem na mochila, sendo que não podemos ultrapassar
o peso suportado pela mochila e devemos levar o máximo possível de objetos mais valiosos.
Foram implementados diversos algoritmos para resolver o problema, indo desde um algoritmo
exato, como por Força Bruta, até heurísticas, como os algoritmos Gulosos por Peso, Utiliadade
e Custo Relativo como também um Algoritmo Genético. No final deste trabalho expomos os
testes realizados com várias instâncias e um gráfico comparando os algoritmos.
4. 3
1
DEFINIÇÃO DO PROBLEMA DA MOCHILA
Figura 1: Problema da mochila. Fonte: Wikipédia
No problema clássico 0-1 da mochila (knapsack problem), temos n itens que podem ser
carregados na mochila, onde cada item j possui associado um peso p j e um valor de utilidade
u j e a mochila possui capacidade c para transportar os itens, nesse trabalho a soma das utilidade
dos itens deve ser pelo menos d.
Então temos um problema de otimização combinatória onde queremos carregar itens
que somem o máximo possível de utilidade sem estourar a capacidade da mochila. Na modelagem desse problema utilizamos um vetor de tamanho n, onde cada elemento do vetor x j é um
item, se x j = 1, então o item está na bolsa, senão não está caso x j = 0.
Para exemplificar, suponha que Bob esteja indo viajar e sua mochila so comporte 15kg
como mostrado na Figura 2, então ele precisa decidir quais itens irá levar consigo, querendo ele
somar o máximo possível de dinheiro de seus itens, tendo em mente que ele precisa lever no
mínimo d reais em de itens.
Podemos modelar o problema utilizando programação linear inteira da seguinte forma:
Maximizar z = ∑n u j x j
j=1
Sujeito a d ≤ ∑n p j x j ≤ c
j=1
x j ∈ {0, 1}, j ∈ {1, ...n}
O problema da mochila é NP-Díficil (MARTELLO; PISINGER; TOTH, 2000) existem várias
soluções propostas, tanto exatas como de heurísticas, aqui abordaremos o de força bruta, com
heurística gulosa, com programação dinâmica e algoritmo genético.
5. 4
2
ALGORITMOS PARA O PROBLEMA DA MOCHILA
Dividimos os algoritmos implementados em duas categorias exatos e heurísticas. Os
exatos irão retornar o valor ótimo para o problema, mas terá um custo computacional muito mais
alto do que usando uma heurística que tenta chegar o mais próximo possível da solução ótima,
os algoritmos gulosos e o de programação dinâmica pode ser visto em (VIANA; MOREIRA,
2011).
2.1
Força Bruta
Esse algoritmo exato é o mais trivial de se pensar, a ideia dele é bem simples, teste todas as possibilidades que existem e guarde a melhor, essa será a solução ótima para o problema.
Ele pode ser implementado de várias formas, mas nesse trabalho utilizamos contagem
de números binários para poder fazer o teste de todas as possibilidades. Como uma solução
para mochila representada como vetor de 0’s e 1’s, inicializamos o vetor com 0’s, sendo essa a
primeira solução, então para gerar as demais possibilidades é feito um incremento de mais um
em binário até que o algoritmo passe por todos os possível valores do vetor.
Podemos ver abaixo o pseudocódigo do algoritmo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
entrada :
c: capicidade total da mochila
n: numero de itens da mochila
d: valor minimo de peso carregado
P [1... n ]: vetor dos pesos dos itens
U [1... n ]: vetor dos valores de utilidade dos itens
saida :
um vetor X [1.. n] que representa a solucao encontrada ,
o valor da solucao encotrada e o somatorio das valores
de utilidade que estao na mochila
forcaBruta (c ,n ,d ,P [] , U [])
totaldeIteracoes = potencia (2 , n ) - 1
melhorSolucao = 0
melhorNumero = 0
para i = 0 ate totaldeIteracoes
pegaBinario (X ,n , i) // coloca em X a solucao corrente
solucaoCorrente = pegaSolucao (X ,b ,c , d)
// retorna o valor da solucao para o vetor atual de X
6. 5
21
22
23
24
25
26
27
// caso nao seja uma solucao valida , ou seja menor que d ,
// retorna -1
se ( solucaoCorrente > melhorSolucao )
melhorSolucao = solucaoCorrente
melhorNumero = i
pegaBinario (X ,n , melhorNumero )
retorne X
É fácil notar que a complexidade do pior caso desse algoritmo é de O(2n ), pois como
temos um vetor binário de tamanho n, existirão todas essas possibilidades. Por exemplo para n =
3 temos 8 possibilidades, para vermos isso basta aplicarmos um pouco de análise combinatória,
para cada posição do vetor temos dois valores possíves 0 e 1, como o tamanho é 3, então
calculamos 2 ∗ 2 ∗ 2 = 8. Esse algoritmo é inviável, quando n já chegar a valores altos, o tempo
de resposta desse algoritmo será de anos.
2.2
Programação Dinâmica
A ideia do uso da programação dinâmica é de reaproveitar soluções anteriores para
conseguir as posteriores, guardando elas em uma tabela. Para conseguirmos montar essa tabela
transformamos uma recursão em iteração.
Para isso definimos a tabela t da seguinte forma: t[i,Y ], onde i = 0, 1..n é a quantidade
de itens para essa solução e Y = 0, 1...c é a quantidade máxima de peso carregado para essa
solução. Assim notamos que t[0,Y ] = 0 para todo Y , agora se i > 0 temos que:
t[i,Y ] = A se p[i] > Y e
t[i,Y ] = max(A,B) se p[i] <= Y , onde
A = t[i − 1,Y ] e B = t[i − 1,Y − p[i]] + u[i]
Quando p[i] > Y indica que esse item tem uma peso maior que a capacidade da mochila
para essa solução, então ela tem o mesmo valor que anterior, por isso atribuimos à ela o valor
máximo anterior t[i − 1,Y ]. Caso contrário existe capicidade na mochila então ne caso atribuimos à B o valor de solução máxima com Y − p[i] de capacidade e acrescentamos a utilidade do
item i, somando u[i]. O pseudocódigo desse algoritmo é mostrado abaixo:
1
2
3
4
5
c: capicidade total da mochila
n: numero de itens da mochila
d: valor minimo de peso carregado
P [1... n ]: vetor dos pesos dos itens
U [1... n ]: vetor dos valores de utilidade dos itens
7. 6
6
7 saida : um vetor X [1.. n] que representa a solucao encontrada ,
8
o valor da solucao encotrada e o somatorio das valores
9
de utilidade que estao na mochila
10
11 PGDinamica (c ,n ,d ,P [] , U [])
12
melhorSolucao = 0
13
14
para y = 0 ate c
15
t [0][ y ] = 0
16
para i = 1 ate n
17
a = t[i -1][ y]
18
se p[i ] > Y
19
b = 0
20
senao
21
b = t [i -1][ y -P [i ]] + U [i]
22
t[i ][ y ] = max (a ,b )
23
24
// essa parte monta X
25
26
para i = n ate 1
27
se t [i ][ y] = t [i -1][ y ]
28
X[ i] = 0
29
senao
30
X[ i] = 1
31
y = y - P[i ]
32
melhorSolucao = melhorSolucao + U [i]
33
retorne X
Para montar o vetor X, é feito o 1o se, que indica que solução da posição [i, j] é igual
a de [i − 1, y], então é por que não houve inclusão de item entre essas duas soluções nesse caso
X[i] = 0, indicando que esse item não foi colocado, caso contrário ele é colocado setando 1, e
então diminuindo Y de P[i], indicando que i está na mochila, logo seu peso deve ser descontado
de Y. Assim prossegue até ter percorrido todos os itens.
Enxergamos que as dimensões da tabela é nXc e ela que indica a complexidade desse
algoritmo, ou seja ele possui O(nc), isso indica que temos uma grande melhoria em relação ao
de força bruta, mas ainda assim existem alguns problemas, podemos ter problemas com poucos
itens, porém com algoritmo com custo elevado, isso vai depender da proporção de c escolhida,
além disso podemos temos que checar o uso da memória que pode ser muito extenso.
8. 7
2.3
Algoritmo Guloso
Diferente dos anteriores o algoritmo guloso não garante a solução ótima, ele consegue
uma aproximada, em alguns casos ele pode até conseguir retornar a solução ótima. O algoritmo
busca o colocar os melhores itens até que não haja mais espaço na mochila, para saber quais são
os melhores itens, o algoritmo guloso pode se basear, no valor de utilidade do item, no peso do
p
item, ou no custo relativo que é u jj .
Para colocarmos os itens na mochila, precisamos fazer uma ordenação, se nos basearmos pelo valor de utilidade, então nesse caso ordenamos o vetor de utilidades, em ordem
decrescente e assim vamos percorrendo esse vetor e "colocando"os itens na mochila enquanto
houver capacidade e não acabar os itens. Podemos ver a implementação no código abaixo (o
modo de fazer o algoritmo se baseando no peso e no custo relativo é de maneira semelhante)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
entrada :
c: capicidade total da mochila
n: numero de itens da mochila
d: valor minimo de peso carregado
P [1... n ]: vetor dos pesos dos itens
U [1... n ]: vetor dos valores de utilidade dos itens
saida :
um vetor X [1.. n] que representa a solucao encontrada ,
o valor da solucao encotrada e o somatorio das valores
de utilidade que estao na mochila
gulosoPorUtilidade (c ,n ,d ,P [] , U [])
ordeneBaseadoNoValorUtilidade (P [] , U [] , n )
X = {0 ,0 ,...0}
pesoLivreCorrente = c
melhorSolucao = 0
para i = 1 ate n
se P [i] <= pesoLivreCorrente
pesoLivreCorrente = pesoLivreCorrente - P[ i]
X[ i] = 1
melhorSolucao = melhorSolucao + U [i]
se melhorSolucao >= d
retorne X
senao
retorne naoEncontrouSolucao
Pode ser visto que pode ser ele não consiga encontrar nenhuma solucação devido a
9. 8
restrição que a soma dos valores de utilidade deve ser pelo menos d, em nossos testes, tanto
o guloso baseado peso como custo relativo, tentam executar o por valor de utilidade, caso eles
não consigam atigir d, mas se o de valor de utilidade não conseguir solução, então realmente
não é retornado nenhuma solução.
A complexidade desse algoritmo fica dependente do método de ordenação usado, se
usarmos por exemplo o MergeSort, teremos complexidade de O(nlogn + n) que é mesmo que
O(nlogn). Em nossa implementação utilizamos o QuickSort que apesar de ter uma complexidade no pior caso de O(n2 ), ele possui no caso médio Θ(nlogn) e utilizando técnicas para evitar
pior caso, ele consegue resultados semelhantes ao MergeSort ou até melhores como obtidos em
(HOARE, 1962)
10. 9
2.4
Algoritmo Genético
Algoritmo Genético é uma técnica inspirada pela biologia evolutiva, sendo uma classe
particular de algoritmos evolutivos, que é utilizada para encontrar soluções para problemas de
otimização combinatória, cujo objetivo é descobrir a melhor combinação dos recursos disponíveis para otimizar seu uso.
Algoritmos Genéticos são implementados como uma simulação de computador em que
as melhores soluções são encontradas em uma população que vai evoluindo a cada geração. O
processo de evolução geralmente se inicia com uma população criada aleatoriamente. A cada
geração, as soluções na população sofrem cruzamentos e mutações, suas adaptações então são
avaliadas e alguns indivíduos são selecionados para a próxima geração.
A seguir temos o processo do Algoritmo Genético:
Inicializa População
Avalia População
Seleciona Reprodutores
Cruza Selecionados
Muta Resultantes
Avalia Resultantes
Atualiza População
Deve Parar?
Não
Sim
FIM
Figura 2: Processo de AG
Explicando o algoritmo implementado:
O Algoritmo Genético para o Problema da Mochila Valiosa (PMV) foi implementado
seguindo a estrutura como mostrado abaixo:
Cromossomo
O cromossomo do AG para a Mochila foi montado utilizando os próprios itens da
11. 10
Mochila, onde cada alelo representa a presença de um objeto na Mochila com 0 ou 1. Assim,
para um PMV com 5 objetos temos a representação de um cromossomo:
10110
De acordo com o cromossomo acima, a Mochila estaria carregando os objetos 1, 3 e 4;
Gerar População Inicial
A população inicial é criada de forma aleatória. Como a representação de um indivíduo
é de forma binária, é sorteado um número aleatoriamente entre 1 e 2Nob jetos . Em seguida, esse
número é transformado em binário para fazer parte da população inicial. Com isso, conseguimos buscar um indivíduo em todo o espaço de possíveis soluções. No entando, o cromossomo
só é considerado válido se a soma do peso dos objetos contidos na Mochila não ultrapassa c e a
soma do valor desses objetos seja pelo menos d (Checagem de Validade). O valor do tamanho
da população inicial é parametrizado.
Fitness
A função Fitness é baseada no somatório dos Valores dos objetos contidos em cada
Mochila. Ela é formulada através uma regra de três simples, onde para cada indivíduo temos:
f itnessi = Valori ∗100
Sumvalores
i
i ∈ N, onde N é a quantidade de alelos do cromossomo, o que nos dá um valor entre
zero e cem.
Taxa
A função da taxa é utilizada para conseguirmos classificar os indivíduos em termos de
proporção baseado agora no valor do fitness de cada indivíduo. Também é formulado através
de uma regra de três simples, onde para cada um calculamos:
ratei =
f itnessi ∗100
Sum f itnessi
i ∈ N, onde N é a quantidade de alelos do cromossomo, o que nos dá um valor entre
zero e cem, sendo que agora, o somatório das taxas nos dá um valor fechado em cem, para
podermos aplicar o próximo passo.
Roleta
A função da roleta foi implementada justamente utilizando o valor da taxa de cada
indivíduo, como essas taxas são valores que somam um total de cem, é possível determinar
um valor máximo para cada indivíduo da população que será utilizado para a seleção desses
indivíduos para o cruzamento. Assim, uma população com quatro indivíduos, teríamos:
12. 11
1 0 1 1 0, 32.7
1 0 1 1 1, 41.5
1 0 0 1 0, 15.0
0 0 0 1 0, 10.8
onde os valores ao lado do cromossomo representam a taxa de cada um, com isso
conseguimos que o 1a indivíuo tenha um valor máximo de 32.7, o 2o um valor de 74.2, o 3o um
valor de 89.2 e o 4a 100.
Crossover
Antes de realizar o Crossover, é feita uma seleção de pares de indivíduos que irão
participar do cruzamento mútuamente, onde os que possuem maior fitness têm uma melhor
chance de participarem do cruzamento juntos. Assim, para o exemplo anterior do valor máximo,
seriam gerados 4 números aleatórios em que cada um cairia dentro de um espaço de um dos
indivíduos. Com isso, supondo que teríamos sorteado na seguinte ordem 2, 3, 1, 4, o cruzamento
seria aplicado entre os indivíduos 2, 3 e depois 1, 4. O cruzamento irá gerar novos indivíduos
com a mesma quantidade que a população inicial. Ele é baseado em um ponto de corte que é
parametrizado. O cruzamento é então aplicado em cima desse ponto de corte, onde o 1o filho
recebe do ponto de corte ao começo do cromossomo pai 1 junto do ponto de corte ao final do
cromossomo do pai 2. Isso de modo inverso é feito para o filho 2. Dessa forma geramos os
indivíduos novos para a população.
Mutação
A mutação é ralizada por meio da aleatoriedade, onde a quantidade de indivíduos que
sofrerão mutação é dependente do tamanho do cromossomo. A taxa de mutação também é
parametrizada, e é com ela que é decidido se cada alelo escolhido para mutação sofrerá ou não
a mutação naquele cromossomo. A mutação quando aplicada sobre o alelo, ela apenas inverte
seu valor, saindo de zero para um ou de um para zero, dependendo do valor antes da mutação.
Avaliação
A função de avaliação é utilizada justamente para tratar os novos indivíduos gerados.
Ela vai calcular o somatório dos valores de cada objeto para cada cromossomo. Em seguida,
aplica a função fitness, junto com a população inicial para poder classificar todos os indivíduos.
Atualização
A última etapa do processo evolutivo se baseia em atualizar a população escolhendo
os melhores indivíduos que irão para a próxima geração. A função de atualização ordena os
indivíduos baseado na fitness para poder capturar os melhores cromossomos. Um detalhe nessa
etapa é que a possibilidade de repetição de indivíduo pode gerar uma convergência precoce para
13. 12
a solução do problema, onde a população seria preenchida com o mesmo indivíduo repetido.
Para evitar esse problema, nessa etapa de atualização, eliminamos todos os indivíduos repetidos
dentre os indivíduos pai e indivíduos filho. Ao terminar a fase de atualização, a nova população
está pronta para ir para a próxima geração. A quantidade de geração realizadas também é
parametrizada.
Um detalhe de implementação sobre o algoritmo genético é que em um caso, para
efeito de testes, chamamos os algoritmos Gulosos por custo e por volume para gerar a população
inicial, os demais indivíduos foram gerados de forma aleatória como explicado anteriormente.
No próximo tópico poderemos ver o resultado da aplicação dos algoritmos Gulosos na geração
da população inicial. Como o cálculo para o tempo de execução é obtido depois que roda
todas as iterações, não é tão notável a diferença de tempo para encontrar a melhor solução,
mas abservando as execuções do algoritmo, notamos uma rápida convergência para a melhor
solução possível encontrada.
14. 13
3
RESULTADOS DOS TESTES
Para os testes foram utilizadas 11 (0..10) instâncias, onde a primeira tem a seguinte
entrada:
n=30 c=200 d=1000
P= { 12, 11, 1, 22, 5, 12, 12, 13, 14, 90, 10, 10, 99, 17, 1, 2, 3, 5, 20, 7, 3, 12, 23, 43, 45,
2,7,5,1,1 }
U= { 121,11, 22, 499, 112, 122, 133, 144, 190, 110, 120, 199, 171, 10, 2, 322, 153, 320, 177,
333, 212, 203, 43, 45, 27, 75, 251, 101, 100, 100 }
Paras as 10 restantes é usada variação da seguinte entrada:
n=100 c=5000 d=8000
P= { Números randômicos inteiros entre 10 e 100 }
U= { Números randômicos inteiros entre 10 e 1000 }
3.1
Tempo de execução
Para realização dos testes cada instância foi rodada 30 vezes e depois tirada a média
do tempo gasto na execução,
Gráfico do Tempo de Execução
0.09
Dinamica
GulosoUtilidade
GulosoPeso
GulosoRelativo
Genetico
GeneticoGuloso
0.08
Tempo de Execução(s)
0.07
0.06
0.05
0.04
0.03
0.02
0.01
0
0
2
4
6
Número da Instância
Figura 3: Gráfico de teste
8
10
15. 14
calculado a partir do comando time do linux e capturando o real time. Os testes foram executados em uma máquina com processador quadcore, com 2GB de ram com sistema operacional
Ubuntu 12.10. Não calculamos para o força bruta, devido ele gastar muito tempo executando
para instância com n = 100, para o algoritmo genético foi utilizado as seguintes configurações:
Para instâcia 0
população = 10 ponto de corte = 10 número de iterações = 1000 taxa de mutação = 10%
Para instâcias de {1..10}
população = 10 ponto de corte = 40 número de iterações = 1000 taxa de mutação = 10%
Na Figura 3 vemos que em termo de tempo gasto o algoritmo que mais custoso é
o genético, isto é devido ele ter sido configurado para ter 1000 iterações e também por suas
checagens e tratamentos, diferente dos gulosos que o tempo de execução depende apenas de um
método de ordenação mais iterações para colocar os itens na mochila.
Para a de programação dinâmica podemos ver que para instância 0, onde n = 30, ele
consegue um tempo similar aos gulosos, sendo ele exato, isso se deve a capacidade pequena
da mochila dessa instância c = 200, ficamos apenas com 30*200 passos para a de programação
dinâmica.
Mas vemos que nas demais instâncias os gulosos conseguem manter um tempo praticamente fixo, mas a de programação dinâmica sobe um pouco devido c = 5000 ter sofrido um
aumento grande.
3.2
Convergência das soluções
Tirando o algoritmo de programação dinâmica, temos heurísticas para o problema,
então pode ser que não retornem a solução ótima, na tabela seguintes pegamos 3 instâncias e
mostramos a melhor solução achada das 30 execuções de cada algoritmo:
17. 16
BIBLIOGRAFIA
HOARE, C.A.R. Quicksort. The Computer Journal, Br Computer Soc, v. 5, n. 1, p. 10–16,
1962.
MARTELLO, S.; PISINGER, D.; TOTH, P. New trends in exact algorithms for the 0–1 knapsack problem. European Journal of Operational Research, Elsevier, v. 123, n. 2, p. 325–332,
2000.
VIANA, G. V. R.; MOREIRA, F. V. C. Técnicas de divisão e conquista e de programação dinâmica para a resolução de problemas de otimização. Revista Científica da Faculdade Lourenço
Filho, v. 8, n. 1, p. 5–27, 2011.