Trabalho4 SérgioSilvaMucciaccia
Trabalho4 SérgioSilvaMucciaccia
Trabalho4 SérgioSilvaMucciaccia
29 de novembro de 2019
Sumário
Índice
Sumário............................................................................................................................................2
Introdução........................................................................................................................................3
Representação das soluções.............................................................................................................4
Estrutura de vizinhança....................................................................................................................5
Detalhes de implementação.............................................................................................................6
Resultados computacionais..............................................................................................................7
Notas sobre o tempo de execução....................................................................................................9
Referências.....................................................................................................................................10
Introdução
O recozimento simulado é uma meta-heurística de pesquisa local popular usada para tratar
problemas de otimização discretos e, em menor grau, problemas de otimização contínua. O
principal recurso do recozimento simulado é que ele fornece um meio de escapar dos ótimos locais,
permitindo movimentos de escalada (ou seja, movimentos que pioram o valor da função objetivo)
na esperança de encontrar um ótimo global. Neste trabalho, será apresentada uma implementação
em C++ do recozimento simulado, sua definição e descrição e os resultados obtidos da execução da
heurística em 3 diferentes datasets disponíveis na literatura. Algumas decisões tomadas para a
implementação de recozimento simulado em termos de cronogramas de resfriamento, funções de
vizinhança e solução inicial serão discutidas.
s_atual = criarSoluçãoInicial();
s_melhor = s_atual;
PARA i DE 1 ATÉ i_max FAÇA
s_i = criarVizinho(s_atual);
temp = calcularTemperatura(i, tmax);
SE custo(s_i) < custo(s_atual) ENTÃO
s_atual = s_i;
SENÃO
SE exp( (custo(s_atual) – custo(s_i)) / temp ) > Rand() ENTÃO
s_atual = s_i;
FIM SE;
FIM SE;
SE custo(s_i) < custo(s_melhor) ENTÃO
s_melhor = s_i;
FIM SE;
FIM PARA;
RETORNA s_melhor;
Route #1: 1
Route #2: 8 5 3
Route #3: 9 12 10 6
Route #4: 11 4 7 2
Cost 247
0 1 0 8 5 3 0 9 12 10 6 0 11 4 7 2 0
As seguintes regras devem ser obedecidas para que um vetor de solução seja válido:
Zeros em sequência são desconsiderados pelos métodos no vetor de solução. Isso permite que rotas
sejam adicionadas ou retiradas apenas com permutações do vetor. Por exemplo, o seguinte vetor de
solução é válido e tratado da mesma maneira que o vetor anterior:
0 1 0 8 5 3 0 0 9 12 10 6 0 11 4 7 2 0 0
Essa representação tem a vantagem de facilitar a implementação das vizinhanças 2-opt, 3-opt e 4-
opt, além de facilitar o cálculo do custo e da validade da solução que podem ser feitos com
complexidade O(n).
Estrutura de vizinhança
O algoritmo pode ser utilizado com as vizinhanças 2-opt, 3-opt e 4-opt. A vizinhança 2-opt gerou os
melhores resultados, pois, o aumento na velocidade de execução compensa o número menor de
vizinhos. Devido a este fato, a vizinhança 2-opt foi escolhida para realizar os benchmarks do
algoritmo nos sets A, B e F. O movimento 2-opt entre as arestas {12, 10} e {4, 7} pode ser visto nos
vetores de solução a seguir:
Vetor original:
0 1 0 8 5 3 0 9 12 10 6 0 11 4 7 2 0
Para verificar se o vizinho obtido efetuando este movimento 2-opt tem um custo menor que a
solução atual, não é necessário gerar a solução vizinha. Basta apenas somar o comprimento das
novas arestas {12, 4} e {10, 7} e subtrair do comprimento das arestas antigas {12, 10} e {4 ,7}.
Para saber se o novo vizinho é uma solução válida, também não é necessário gerar a solução
vizinha. Basta verificar se a demanda total das rotas alteradas permanece menor que a capacidade.
Portanto, apenas 2 rotas precisam ser verificadas, como ilustrado a seguir.
Com isso, não é necessário fazer uma cópia do vetor de solução na memória para verificar as
condições de aceitação do movimento no SA. Basta escolher um movimento 2-opt e verificar se
este movimento gerará uma solução válida, se a solução for válida é possível prever o custo da
solução gerada pelo movimento e utilizar o valor obtido na previsão para decidir se o SA aceitará ou
não o movimento. Apenas se o movimento for aceito ele será executado na solução. Portanto, para
efetuar o SA só são necessários dois vetores de solução na memória. Um que guardará a melhor
solução até o momento e outro em que serão aplicados os movimentos. Assim, o algoritmo
consegue rodar na maior instância da CVRPLIB, a Flanders2.vrp, que possui 30 mil cidades,
gastando menos de 1 MB de memória.
A previsão de custo da solução gerada por um movimento 2-opt não depende do número de cidades
da solução, pois independente do tamanho apenas 4 arestas serão avaliadas. Do mesmo modo, a
previsão de validade depende apenas do tamanho das duas rotas avaliadas e não do total de cidades
na solução. Como uma iteração do SA não envolve analisar toda a vizinhança, mas somente gerar
um movimento aleatório, esta implementação se mostra bem interessante para uso em problemas
muito grandes, pois sua complexidade cresce linearmente com o número de cidades do problema.
Detalhes de implementação
A linguagem C++ foi escolhida pela sua performance. A seguir estão os comparativos de execução
do mesmo algoritmo em 2 benchmarks feitos pelo grupo benchmarksgame-team.
Benchmark k-nucleotide.
Linguagem Tempo (segundos) Memória (bytes)
C++ (g++) 3,81 156348
C (gcc) 6,00 130132
Java (OpenJ9) 8,43 345684
Python 3 72,24 199856
Benchmark n-body
Linguagem Tempo (segundos) Memória (bytes)
C (gcc) 7,30 8
C++ (g++) 7,92 1116
Java (OpenJ9) 21,95 35604
Python 3 840,10 8176
O código foi feito em c++11 e o compilador utilizado foi o g++ 9.2.1. O código foi compilado com
as flags “-std=c++11”, “-pthread” e “-O3”. O algoritmo foi executado em uma máquina com um
processador Intel(R) Core(TM) i5-2400 CPU @ 3.10GHz, com 6144 KB de cache e 4 cores. A
memória ram total é de 8 GB.
Resultados computacionais
O algoritmo foi executado 5 vezes por 500 milhões de iterações em cada instância, sendo que
nenhuma das execuções demorou mais de 300 segundos.
Custo t(s) Custo t(s) Custo t(s) Custo t(s) Custo t(s) Média Opt Diff
A-n32-k5.vrp 784 98 784 110 784 101 784 90 784 102 784 784 0
A-n33-k5.vrp 661 100 661 101 661 102 661 98 661 84 661 661 0
A-n33-k6.vrp 742 107 742 102 742 94 742 104 742 118 742 742 0
A-n34-k5.vrp 778 107 778 121 778 116 778 120 778 117 778 778 0
A-n36-k5.vrp 799 125 799 118 799 124 799 126 799 129 799 799 0
A-n37-k5.vrp 669 113 669 119 669 114 669 119 669 125 669 669 0
A-n37-k6.vrp 949 125 949 121 949 123 949 126 949 134 949 949 0
A-n38-k5.vrp 730 128 730 122 730 132 730 117 730 117 730 730 0
A-n39-k5.vrp 822 142 822 143 822 144 822 144 822 142 822 822 0
A-n39-k6.vrp 831 138 831 134 831 137 831 135 833 132 831,4 831 0
A-n44-k7.vrp 937 141 937 140 937 149 937 150 937 143 937 937 0
A-n45-k6.vrp 944 157 944 153 944 149 945 152 948 141 945 944 0
A-n45-k7.vrp 1146 162 1146 166 1146 161 1146 169 1146 156 1146 1146 0
A-n46-k7.vrp 914 159 914 157 914 155 914 150 914 151 914 914 0
A-n48-k7.vrp 1073 165 1073 169 1073 164 1073 166 1073 163 1073 1073 0
A-n53-k7.vrp 1010 175 1011 166 1014 169 1017 175 1017 193 1013,8 1010 0
A-n54-k7.vrp 1167 196 1170 185 1167 197 1167 199 1167 197 1167,6 1167 0
A-n55-k9.vrp 1073 196 1073 198 1073 194 1073 204 1073 201 1073 1073 0
A-n60-k9.vrp 1354 219 1358 223 1355 224 1358 234 1361 229 1357,2 1354 0
A-n61-k9.vrp 1034 213 1035 220 1035 208 1035 209 1035 206 1034,8 1034 0
A-n62-k8.vrp 1308 219 1308 219 1308 221 1308 219 1308 218 1308 1288 20
A-n63-k10.vrp 1316 219 1320 224 1317 229 1319 231 1321 229 1318,6 1314 2
A-n63-k9.vrp 1628 218 1629 234 1629 236 1629 237 1629 238 1628,8 1616 12
A-n64-k9.vrp 1401 230 1402 230 1402 222 1405 212 1411 211 1404,2 1401 0
A-n65-k9.vrp 1174 229 1178 226 1178 230 1178 231 1179 227 1177,4 1174 0
A-n69-k9.vrp 1163 227 1167 244 1167 225 1164 242 1169 224 1166 1159 4
A-n80-k10.vrp 1763 262 1764 272 1769 252 1770 260 1779 262 1769 1763 0
Com os testes realizados, foi percebido que, no pior caso, o tempo de execução do Simulated
Annealing cresce linearmente com o número de vértices para um número fixo de iterações. Como
mostra o quadro, os tempos de execução não dependem apenas do número de vértices, mas também
da capacidade dos veículos e da posição dos vértices. Ou seja, podem existir problemas com poucos
vértices que são mais difíceis de se obter boas soluções que problemas com muitos vértices.
Um fato interessante é que, para a instância Flanders2, uma única busca local na vizinhança 2-opt,
utilizando-se da estratégia Hill Climbing, demorou cerca de 8 horas pra terminar a execução. Isso
praticamente inviabiliza a utilização do Iterated Local Search 2-opt em um tempo razoável. Para
problemas muito grandes, é bem mais interessante utilizar algoritmos em que a complexidade de
resolução não cresce muito rapidamente com o tamanho do problema.
As heurísticas Ant Colony Optimization e Genetic Algorithm tem uma complexidade que aumenta
muito rápido com o tamanho do problema. A primeira, porque a matriz de feromônios possui uma
complexidade que aumenta com o quadrado do número de cidades e a segunda, porque precisa fazer
muitas cópias das soluções para realizar as operações de Cross Over. Das 12 heurísticas estudadas é
provável que o SA será uma das poucas heurísticas que consegue obter bons resultados na instância
Flanders2 em um tempo razoável.
Em uma iteração do SA 2-opt, o movimento aleatório gerado pode ser rejeitado ou aceito. Caso
rejeitado, o tempo de execução da iteração é muito pequeno, pois devido à previsão de validade e à
previsão de custo, o tempo de execução é bastante reduzido, tornando a complexidade desta etapa
próxima a O(1). Já se o movimento for aceito, será necessário executar um 2-opt na solução e essa
operação cresce linearmente com o tamanho da solução, apresentando complexidade aproximada a
O(n/2). Se o movimento gerar uma solução melhor do que a melhor solução atual, a solução atual
deve ser copiada e armazenada como a melhor solução, o que demanda visitar todos os vértices, ou
seja, uma complexidade O(n).
Portanto, uma desvantagem do algoritmo deste trabalho é que é muito difícil prever seu tempo de
execução. Se o algoritmo for feito de modo a parar depois de X segundos, o cronograma de
temperatura pode estar no meio, o que vai afetar bastante a qualidade da solução obtida. O melhor
que pode ser feito nesta situação é calcular o pior caso do algoritmo e escolher o número de
iterações para que o pior caso fique a baixo dos X segundos. Mas dessa forma é provável que o
algoritmo termine muito antes e não aproveite de maneira adequado o tempo disponível para
execução.
Referências
[1] KIRKPATRICK, S; GELATT, C. D; VECCHI, M. P. Optimization by Simulated Annealing.
Science, v. 220. n. 4598. pp. 671-680. Maio, 1983.
[3] GLOVER, Fred; KOCHENBERGER, Gary A. Handbook of Metaheuristics. New York, USA:
Kluwer Academic Publishers. 2003. 556p.
[4] TALBI, El-Ghazali. Metaheuristics: From Design to implementation. Hoboken, New Jersey:
John Wiley & Sons, Inc. 2009. 593p.