Renato Fonseca Furquim Werneck Problema de Steiner em Grafos: Algoritmos Primais, Duais e Exatos DISSERTAÇÃO DE MESTRADO DEPARTAMENTO DE INFORMÁTICA Rio de Janeiro, Agosto de 2001 Renato Fonseca Furquim Werneck Problema de Steiner em Grafos: Algoritmos Primais, Duais e Exatos Dissertação apresentada ao Departamento de Informática da PUC-Rio como parte dos requisitos para a obtenção do título de Mestre em Ciências em Informática. Orientador: Prof. Marcus Poggi de Aragão Departamento de Informática Pontifícia Universidade Católica do Rio de Janeiro Rio de Janeiro, Agosto de 2001 A meus pais Agradecimentos Aos Professores Marcus Poggi de Aragão, Eduardo Uchoa e Celso Ribeiro, por não me deixarem esquecer de que sempre é possível trabalhar um pouco mais. Aos amigos (incluindo o trio acima), por tentarem me convencer de que é possível trabalhar um pouco menos. Aos professores Cid de Souza, Luiz Satoru Ochi, Ruy Milidiú e Eduardo Laber, pela participação na banca examinadora. À CAPES, pelo apoio nanceiro. RESUMO O problema de Steiner em grafos é um dos mais importantes problemas NP-difíceis de otimização combinatória, com aplicações em áreas como projeto de circuitos VLSI, redes, logística e biologia computacional. Esta dissertação apresenta um estudo computacional de diversas estratégias para lidar na prática com instâncias desse problema. Discutem-se métodos de várias categorias: heurísticas construtivas, métodos de busca local, metaeurísticas, heurísticas duais e algoritmos exatos. Cada categoria representa uma maneira particular de se lidar com o problema: variam os tempos de execução, a qualidade das soluções obtidas e as garantias fornecidas. Em cada caso, faz-se uma análise dos principais algoritmos disponíveis na literatura e propõem-se novos procedimentos. O resultado é um conjunto de ferramentas que representa o estado-da-arte da pesquisa na área. Com isso, muitas das instâncias em aberto da literatura foram resolvidas ou tiveram as melhores soluções conhecidas encontradas pelos métodos apresentados. Palavras-chaves: problema de Steiner em grafos, algoritmos, otimização combinatória, heurísticas construtivas, busca local, metaeurísticas, heurísticas duais, branch-and-bound. ABSTRACT The Steiner problem in graphs is one of the most important NP-hard problems in combinatorial optimization, with applications in VLSI design, networks, logistics and computational biology, among others. We present a computational assessment of several strategies to deal with this problem in practice. The methods are divided into a number of classes: constructive heuristics, local search procedures, metaheuristics, dual heuristics and exact algorithms. Each class has its own way of dealing with the problem: running times, solution qualities and guarantees provided dier among them. We discuss the most relevant algorithms described in the literature and propose new ones. The result is a collection of tools that represent the state-of-the-art in practical algorithms for the Steiner problem in graphs. Several open instances in the literature were solved or had their bounds improved by these methods. Keywords: Steiner problem in graphs, algorithms, combinatorial optimization, constructive heuristics, local search, metaheuristics, dual heuristics, branch-and-bound. Sumário 1 Introdução 2 Aspectos Gerais 2.1 Notação e Denições Relevantes . . . . . . 2.2 Estruturas de Dados . . . . . . . . . . . . 2.2.1 Heaps . . . . . . . . . . . . . . . . 2.2.2 Union-Find . . . . . . . . . . . . . 2.3 Algoritmos . . . . . . . . . . . . . . . . . . 2.3.1 Árvore Geradora de Custo Mínimo 2.3.2 Caminhos Mínimos . . . . . . . . . 2.4 Experimentos Computacionais . . . . . . . 2.4.1 Ambiente de Teste . . . . . . . . . 2.4.2 Instâncias de Teste . . . . . . . . . 2.5 Pré-processamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 . 5 . 5 . 5 . 6 . 6 . 7 . 8 . 8 . 8 . 12 3.1 Distance Network Heuristic (DNH) . . . . . . . . . . . . . 3.1.1 Diagrama de Voronoi . . . . . . . . . . . . . . . . . 3.1.2 Implementação da Heurística DNH (DNH-Prim) . . 3.1.3 Implementação Alternativa (DNH-Bor·vka) . . . . 3.1.4 Variante DNHz . . . . . . . . . . . . . . . . . . . . 3.2 Bor·vka . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 Prim . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Implementação em Duas Fases (Prim-T) . . . . . . 3.3.2 Implementação Direta (Prim-D) . . . . . . . . . . . 3.4 Kruskal . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 Implementação Básica (Kruskal-B) . . . . . . . . . 3.4.2 Implementação com Heap de Arestas (Kruskal-E) . 3.4.3 Implementação com Heap de Vértices (Kruskal-V) . 3.5 Multispan . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6 Análise Comparativa . . . . . . . . . . . . . . . . . . . . . 3.6.1 Qualidade das Soluções . . . . . . . . . . . . . . . . 3.6.2 Tempos de Execução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Heurísticas Construtivas vii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 4 14 15 15 17 18 18 19 20 21 22 24 24 25 27 29 30 30 34 3.7 Conclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 4 Busca Local 4.1 Representação da Solução . . . . . . 4.2 Vizinhanças . . . . . . . . . . . . . . 4.2.1 Vértices de Steiner . . . . . . 4.2.2 Caminhos-chaves . . . . . . . 4.2.3 Vértices-chaves . . . . . . . . 4.3 Implementação da Busca Local . . . 4.3.1 Estratégia Geral . . . . . . . . 4.3.2 Busca por Vértices de Steiner 4.3.3 Busca por Caminhos-chaves . 4.3.4 Busca por Vértices-chaves . . 4.3.5 Estratégias Híbridas . . . . . 4.4 Resultados Experimentais . . . . . . 4.5 Conclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . HGP+PR: Estrutura Básica . . . . . . . . . . . . . . Heurísticas Construtivas . . . . . . . . . . . . . . . . Busca Local . . . . . . . . . . . . . . . . . . . . . . . Estratégias de Perturbação . . . . . . . . . . . . . . . Religamento (Path-relinking ) . . . . . . . . . . . . . 5.5.1 Soluções de Elite . . . . . . . . . . . . . . . . 5.5.2 Estratégias de Religamento . . . . . . . . . . 5.5.3 Critérios de Parada . . . . . . . . . . . . . . . 5.6 Resultados Experimentais . . . . . . . . . . . . . . . 5.6.1 Implementação com Parâmetros de Referência 5.6.2 Escalabilidade . . . . . . . . . . . . . . . . . . 5.7 Conclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Metaeurísticas 5.1 5.2 5.3 5.4 5.5 6 Algoritmos Duais 6.1 Formulação e Dualidade . . . . . . . 6.1.1 Formulação Primal . . . . . . 6.1.2 Formulação Dual . . . . . . . 6.1.3 Complementaridade de Folga 6.1.4 Fixação por Custos Reduzidos 6.2 Dual Ascent . . . . . . . . . . . . . . 6.2.1 Soluções Primais . . . . . . . 6.2.2 Implementação . . . . . . . . 6.2.3 Seleção da Componente-raiz . 6.2.4 Resultados Experimentais . . 6.3 Outras Heurísticas Duais . . . . . . . viii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 45 46 46 47 47 48 48 49 50 53 55 55 61 63 63 65 66 67 68 69 69 72 73 73 77 80 82 83 83 84 85 86 92 93 94 95 96 100 6.3.1 Dual Scaling . . . . . . . . 6.3.2 Dual Adjustment . . . . . 6.3.3 Fixação Ativa . . . . . . . 6.3.4 Resultados Experimentais 6.4 Conclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.1 Branch-and-bound e Enumeração Implícita 7.2 Branch-and-Cut . . . . . . . . . . . . . . . 7.2.1 Limites Inferiores . . . . . . . . . . 7.2.2 Soluções Primais . . . . . . . . . . 7.2.3 Branching . . . . . . . . . . . . . . 7.3 Branch-and-Ascent . . . . . . . . . . . . . 7.3.1 Método de Resolução de um Nó . . 7.3.2 Branching . . . . . . . . . . . . . . 7.4 Resultados Experimentais . . . . . . . . . 7.4.1 Incidência . . . . . . . . . . . . . . 7.4.2 OR-Library . . . . . . . . . . . . . 7.4.3 VLSI . . . . . . . . . . . . . . . . . 7.5 Conclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Algoritmos Exatos 8 Conclusão A Resultados do Pré-processamento . . . . . . . . . . . . . . . 100 101 102 104 105 107 107 108 108 109 110 110 111 111 112 113 118 120 122 125 128 ix Lista de Figuras 3.1 Tempos de execução em função do número de terminais . . . . . . . . . . . 3.2 Tempos de execução em função do número de terminais (métodos mais rápidos) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1 Comparação entre a heurística de Prim e árvore geradora do grafo de distâncias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1 Pseudocódigo do GRASP híbrido com perturbações e religamento . . . . . 6.1 Condição para a xação de (u; v) por custo reduzido . . . . . . . . . . . . . 6.2 Dual Ascent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3 Dual Scaling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.4 Dual Adjustment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.5 Fixação Ativa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.1 Solução ótima da instância alue7080 . . . . . . . . . . . . . . . . . . . . . . x 40 40 54 65 87 93 101 102 103 123 Lista de Tabelas 3.1 Qualidade relativa das diversas heurísticas estudadas . . . . . . . . . . . . 32 3.2 Desvios percentuais médios em relação às melhores soluções primais conhecidas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.3 Complexidade dos algoritmos construtivos estudados . . . . . . . . . . . . 34 3.4 Tempos médios de execução (absolutos) dos métodos construtivos . . . . . 35 3.5 Tempos médios de execução das heurísticas construtivas em relação a DNHPrim . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 3.6 Piores tempos de execução dos métodos construtivos em relação a DNH-Prim 37 4.1 Estratégias formadas pelas buscas por nós de Steiner e caminhos-chaves . . 57 4.2 Estratégias que utilizam a busca por nós-chaves . . . . . . . . . . . . . . . 58 4.3 Desvios percentuais médios em relação às melhores soluções conhecidas . . 59 4.4 Tempos médios de execução . . . . . . . . . . . . . . . . . . . . . . . . . . 60 5.1 Coecientes máximos para aleatorização . . . . . . . . . . . . . . . . . . . 67 5.2 Qualidade das soluções obtidas ao longo da execução da implementação de referência . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 5.3 Tempos médios de execução da implementação de referência . . . . . . . . 75 5.4 Metaeurísticas: desvios relativos percentuais médios . . . . . . . . . . . . . 76 5.5 Metaeurísticas: número de soluções ótimas encontradas . . . . . . . . . . . 76 5.6 Metaeurísticas: tempos de execução em segundos (com diferentes máquinas) 77 5.7 Execuções longas para instâncias de incidência (256 iterações, 32 soluções de elite e religamento por perturbações com critério de parada ABW ) . . . 78 6.1 Critérios para escolha da componente-raiz por classe . . . . . . . . . . . . . 97 6.2 Critérios para escolha da componente-raiz (todas as instâncias testadas) . . 98 6.3 Resultados obtidos pelas heurísticas duais . . . . . . . . . . . . . . . . . . 105 7.1 Resultados do branch-and-ascent nas séries i080 e i160 e comparação de tempos (em segundos) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 7.2 Resultados do branch-and-ascent nas séries i320 e i640 e comparação de tempos (em segundos) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 7.3 Comparação entre o branch-and-ascent (B&A) e o branch-and-cut (B&C) . 116 7.4 Resultados do branch-and-ascent com strong-branching nas 24 instâncias da série i320 não resolvidas até janeiro de 2001, segundo a SteinLib . . . . 117 xi 1 7.5 7.6 7.7 8.1 8.2 A.1 A.2 A.3 A.4 A.5 A.6 Resultados do branch-and-cut para a classe OR-Library . . . . . . . . . . . 119 Branch-and-ascent com strong branching para a classe OR-Library . . . . . 119 Resultados obtidos para instâncias VLSI . . . . . . . . . . . . . . . . . . . 121 Resultados para a instância i320-311 (jV j = 320; jE j = 1845; jT j = 80) . . . Resultados para a instância alue7080 (jV j = 9272; jE j = 16019; jT j = 1402) OR-Library Série C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . OR-Library Séries D e E . . . . . . . . . . . . . . . . . . . . . . . . . . VLSI Séries alue e alut . . . . . . . . . . . . . . . . . . . . . . . . . . . . VLSI Séries diw e taq . . . . . . . . . . . . . . . . . . . . . . . . . . . . VLSI Séries dmxa e gap . . . . . . . . . . . . . . . . . . . . . . . . . . . VLSI Série msm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 126 129 130 131 132 133 134 Capítulo 1 Introdução Esta dissertação apresenta um estudo algorítmico do problema de Steiner em grafos, que pode ser denido da seguinte forma: Dados um grafo não-orientado G = (V; E ) com pesos positivos associados às arestas e um conjunto de terminais T V , encontrar um subgrafo conexo de G de custo mínimo que contenha todos os vértices de T . Como os pesos são positivos, a solução ótima será sempre uma árvore, a árvore de Steiner mínima. Esse problema tem aplicações práticas em diversas áreas, como: Projeto de Redes. Árvores de Steiner representam em geral a solução de menor custo para o projeto de diversos tipos de redes, como telefônica, ferroviária ou de águas pluviais. O objetivo é sempre o mesmo: ligar da maneira mais eciente possível o conjunto dos pontos que devem ser servidos (os terminais), utilizando outros pontos se necessário. Roteamento de Pacotes em Redes com Multicast . Nesse caso, os nós do grafo representam máquinas e as arestas, os links entre elas. O peso de uma aresta é uma medida de sua velocidade ou capacidade; os terminais representam as máquinas que devem receber o pacote. Projeto de Circuitos Integrados. Um problema comum no projeto de circuitos integrados é decidir como levar um mesmo sinal a diferentes terminais. Como uma árvore de Steiner ótima representa a ligação que requer a menor quantidade de material condutor, encontrar uma boa solução para esse problema pode contribuir signicativamente para a redução do custo de fabricação da peça. Trata-se de um problema muito estudado na literatura. Levantamentos sobre os mais importantes resultados (à época) disponíveis para o problema são feitos, por exemplo, em [28, 36, 62]. 1 CAPÍTULO 1. INTRODUÇÃO 2 O problema de Steiner em grafos é NP-difícil [31], o que signica que provavelmente não existe um algoritmo polinomial capaz de resolvê-lo. Dada a sua importância prática, no entanto, é plenamente justicável que se tente resolver o problema da melhor possível, seja através de algoritmos aproximados, seja através de algoritmos exatos (ainda que, no pior caso, tenham tempo de execução superpolinomial). É exatamente disso que trata esta dissertação: algoritmos para o problema de Steiner em grafos que sejam úteis na prática. Faz-se um estudo teórico dos algoritmos apresentados, mas a ênfase é na análise empírica de seu comportamento. Extensos experimentos computacionais sobre centenas de instâncias disponíveis na literatura permitem uma avaliação precisa das vantagens e desvantagens de cada método. Na dissertação são apresentados algoritmos de três diferentes categorias: heurísticas primais, heurísticas duais e algoritmos exatos. As heurísticas primais foram divididas em três subcategorias: heurísticas construtivas, métodos de busca local e metaeurísticas. Heurísticas construtivas são métodos que criam soluções viáveis para o problema de forma gulosa. São procedimentos normalmente muito rápidos, executados em frações de segundo para as instâncias testadas. O Capítulo 3 trata desses métodos, concentrando-se principalmente nos baseados no problema da árvore geradora de peso mínimo. Propõemse tanto novas heurísticas quanto implementações mais ecientes para algumas das já existentes. Quando a qualidade de uma solução construtiva deixa a desejar, pode-se utilizar um método de busca local. A partir da solução inicial, ele procura obter soluções similares, mas de melhor qualidade. Trata-se de um procedimento um pouco mais custoso, tipicamente executado em alguns segundos, às vezes minutos, para as instâncias testadas. O Capítulo 4 apresenta um estudo comparativo de três procedimentos de busca local. O Capítulo 5 trata de metaeurísticas, termo que se caracteriza algoritmos mais elaborados para a obtenção de soluções primais de boa qualidade. Em geral, resultam da combinação de elementos mais simples, como heurísticas construtivas e métodos de busca local. A dissertação introduz a metaeurística HGP+PR, um algoritmo extremamente robusto, capaz de obter excelentes resultados para as mais diversas classes de instâncias. No Capítulo 6, apresentam-se algumas heurísticas duais, algoritmos que têm como principal objetivo fornecer limites inferiores para o valor da solução ótima (ao contrário das heurísticas primais, que, ao encontrar soluções viáveis, fornecem limites superiores para o problema). Os limites inferiores permitem que se avalie a qualidade das soluções viáveis encontradas (e, em alguns casos, provam sua otimalidade). A partir de algoritmos para a obtenção de limites superiores e inferiores, é possível criar algoritmos exatos para o problema de Steiner. O Capítulo 7 introduz as rotinas de branchand-cut e branch-and-ascent, métodos de enumeração implícita baseados na construção de árvores de resolução, técnica freqüentemente utilizada na resolução de problemas NPdifíceis. Conforme se verá, os algoritmos propostos são extremamente competitivos, tendo sido capazes de resolver à otimalidade pela primeira vez diversas instâncias da literatura. CAPÍTULO 1. INTRODUÇÃO 3 Entre os algoritmos propostos, há desde os que são capazes de fornecer soluções viáveis em frações de segundo até os que podem levar dias (ou mais) para tratar a mesma instância. Evidentemente, a qualidade das soluções obtidas tende a aumentar signicativamente com o tempo de execução. O Capítulo 8 analisa essas diferenças com base em alguns casos particulares. Capítulo 2 Aspectos Gerais Antes de se iniciar o estudo de algoritmos especícos para o problema de Steiner em grafos, é conveniente introduzir algumas noções básicas e denir a metodologia a ser utilizada. A Seção 2.1 dene a notação básica utilizada ao longo da dissertação e introduz alguns conceitos essenciais associados ao problema de Steiner em grafos. A Seção 2.2 apresenta as estruturas de dados genéricas mais importantes utilizadas neste trabalho: heaps e estruturas union-nd. A Seção 2.3 trata de algoritmos para a determinação de árvores geradoras de custo mínimo e de caminhos mínimos em grafos, problemas freqüentemente mencionados ao longo desta dissertação. A Seção 2.4 trata de aspectos relevantes para os experimentos computacionais realizados: o ambiente de teste e as instâncias utilizadas. Por m, a Seção 2.5 discute as rotinas utilizadas para o pré-processamento de algumas das instâncias testadas ao longo da dissertação. 2.1 Notação e Denições Relevantes Esta seção trata de algumas notações e denições utilizadas ao longo de toda a dissertação. Conceitos mais especícos serão apresentados no contexto em que se zerem necessários. O problema básico de que trata essa tese é o problema de Steiner em grafos, denido na Introdução (Capítulo 1). Em algumas situações, será utilizada a sigla SPG (do inglês Steiner problem in graphs ) para representar o problema. Considera-se sempre que a entrada do problema é constituída por um grafo não-orientado G = (V; E ) (sendo V o conjunto de vértices também chamados de nós e E o de arestas ) e por um conjunto T de terminais, sendo T V . O grafo é positivamente ponderado, o que signica que a toda aresta e 2 E está associado um custo (ou peso ) c(e) maior que zero. Nesta dissertação, considera-se que os custos são sempre inteiros. Para indicar que as extremidades de uma aresta e são os vértices u e v, utiliza-se a notação e = (u; v). Nesse caso, o custo de e pode ser representado também por c(u; v). 4 CAPÍTULO 2. ASPECTOS GERAIS 5 2.2 Estruturas de Dados 2.2.1 Heaps Um heap é uma estrutura de dados que implementa uma la de prioridades. Ele contém diversos elementos, cada um associado a um valor, sua chave. Em todos os casos estudados nesta dissertação, considera-se maior a prioridade dos elementos de menor chave. Um heap provê as seguintes operações básicas: insert(x,k): insere no heap o elemento x com chave k; removeFirst(): retorna o elemento mais prioritário e o remove do heap ; decreaseKey(x,k): reduz para k o valor da chave associada ao elemento x, aumentando a sua prioridade. Há na literatura diversas implementações diferentes desse tipo abstrato de dados. Nesta dissertação, utilizou-se uma das mais simples, o heap binário. Ele permite realizar qualquer das operações acima em tempo O(log n), sendo n o número total de elementos. Há implementações teoricamente mais ecientes, como o heap de Fibonacci [20], que permite realizar as operações insert e decreaseKey em tempo amortizado O(1) (removeFirst continua tendo a mesma complexidade, O(log n)). Na prática, contudo, o heap binário, de implementação muito mais simples, não tem desempenho signicativamente inferior (sendo muitas vezes até mais rápido), já que as constantes associadas à sua implementação são menores (veja [23, 41], por exemplo). Discussões a respeito das duas variantes podem ser encontradas em [9], entre outros. 2.2.2 Union-Find Em muitos dos algoritmos apresentados ao longo dessa dissertação, é necessário manter informações a respeito de conjuntos disjuntos. Utiliza-se para isso uma estrutura de union-nd [9, 56]. Essa estrutura de dados provê apenas três operações básicas: makeset(x): cria um conjunto unitário contendo o elemento x; unite(x,y): une o conjunto que contém o elemento x ao conjunto que contém o elemento y; find(x): retorna o conjunto que contém o elemento x (para decidir se dois elementos x e y pertencem ao mesmo conjunto, basta testar se find(x) = find(y)). Repare que essa estrutura não permite a partição de conjuntos em conjuntos menores, o que limita as situações em que ela pode ser utilizada, mas lhe permite ser mais eciente. CAPÍTULO 2. ASPECTOS GERAIS 6 Em [55], Tarjan prova que é possível realizar qualquer seqüência de m operações, sendo n do tipo makeset (criação de conjuntos), em tempo total O(m(m; n)). Nessa expressão, (; ) denota a inversa da função de Ackermann, uma função cujo valor é menor que 4 em qualquer aplicação prática (portanto, para todos os efeitos, cada operação pode ser realizada em tempo amortizado constante). A implementação que garante essa complexidade foi a utilizada nesta dissertação. 2.3 Algoritmos 2.3.1 Árvore Geradora de Custo Mínimo O problema da árvore geradora de custo mínimo (MST, por seu nome em inglês minimum spanning tree ) pode ser denido da seguinte forma: dado um grafo não-orientado, conexo e positivamente ponderado G = (V; E ), determinar um subgrafo de G de peso mínimo que contenha todos os vértices de V . Como os pesos são positivos, o resultado será sempre uma árvore, considerada geradora porque contém todos os vértices do grafo original. Trata-se de um dos problemas mais estudados em otimização combinatória, o que explica a existência de diversos algoritmos ecientes para resolvê-lo. Nesta dissertação, apenas três algoritmos serão analisados: os algoritmos de Prim, Kruskal e Bor·vka. A descrição desses algoritmos pode ser encontrada em, entre outros, em [2, 9, 56]. Abaixo, discute-se brevemente cada um deles. Prim No algoritmo de Prim [48], a solução cresce a partir de um único vértice inicial r, a raiz. Em cada etapa do algoritmo, adiciona-se à solução o vértice que se liga a ela pela aresta de menor custo (a aresta também é adicionada à solução). Para determinar de forma eciente qual vértice deve ser inserido em cada iteração, as distâncias dos vértices à árvore são normalmente mantidas em um heap. Se for utilizado um heap binário, pode-se provar que o algoritmo é executado em tempo O(jE j log jV j). Se o heap for de Fibonacci, a complexidade de pior caso é O(jE j + jV j log jV j). O algoritmo pode também ser implementado sem um heap, em tempo O(jV j ) (essa implementação só é competitiva para grafos muito densos). 2 Kruskal O algoritmo de Kruskal [34] é executado em duas etapas. Na primeira, todas as arestas de E são ordenadas em ordem crescente de peso. Na segunda etapa, percorre-se a lista em ordem, adicionando-se uma aresta à solução (inicialmente vazia) sempre que ela não formar um ciclo. A primeira fase pode ser executada em tempo O(jE j log jE j); a segunda, em tempo O(jE j(jE j; jV j)), utilizando-se uma estrutura do tipo union-nd para controlar a formação de ciclos. CAPÍTULO 2. ASPECTOS GERAIS 7 Bor·vka O algoritmo de Bor·vka [6], também conhecido como algoritmo de Sollin,1 pode ser entendido como uma versão híbrida dos algoritmos de Kruskal e Prim. No início, o algoritmo mantém jV j componentes conexas, cada uma composta por um único vértice. Em cada iteração, o algoritmo adiciona à árvore geradora mínima a aresta de custo mínimo incidente em cada componente, criando componentes maiores. Como uma mesma aresta pode ser a mínima incidente em duas componentes, pode-se garantir apenas que o número de componentes é será reduzido à metade (no mínimo) em cada iteração. Portanto, o número de iterações é limitado a log jV j. Como cada iteração pode ser executada trivialmente em tempo O(jE j), o algoritmo como um todo tem complexidade O(jE j log jV j). Com uma implementação mais elaborada, discutida em [56], o algoritmo pode ser executado em tempo O(jE j log log jV j). 2.3.2 Caminhos Mínimos Outro problema que freqüentemente aparece associado ao problema de Steiner em grafos e o problema do caminho mínimo em grafos. Dado um grafo ponderado G = (V; E ), o problema consiste em determinar o caminho mínimo entre dois subconjuntos de V : S (as fontes ) e T (os alvos ou destinos ). No caso em que as arestas têm pesos positivos, o algoritmo normalmente utilizado para isso é o algoritmo de Dijkstra [10]. Uma discussão sobre diferentes implementações é apresentada em [2], por exemplo. O algoritmo se baseia na atribuição de potenciais aos vértices. O potencial pv de um vértice v é um limite superior para a distância de v a S . No início do algoritmo, todos os vértices em S têm potencial nulo e os demais, innito. Na implementação mais usual, utiliza-se um heap para manter os vértices ainda não processados (todos, no início); os vértices de menor potencial são prioritários. Em cada iteração do algoritmo, retira-se do heap o vértice v mais prioritário: seu potencial no momento da retirada é justamente a distância de v a S . Para todo vizinho w de v, atualiza-se o potencial pw se necessário (se pw < pv + c(v; w), faz-se pw pv + c(v; w)). O algoritmo termina quando todos os vértices de T forem retirados do heap. A complexidade dessa implementação depende do tipo de heap utilizado: com um heap de Fibonacci, é O(jE j + jV j log jV j); com heap binário, O(jE j log jV j). Nesta dissertação, foi utilizada a segunda implementação, mais simples que a primeira e com desempenho satisfatório na prática [23]. Como no caso do algoritmo de Prim, seria possível também utilizar uma implementação sem heaps, com tempo de execução O(jV j ). Isso não foi feito porque a classe de instâncias para as quais essa implementação é eciente (grafos extremamente densos) é muito restrita. 2 1 Para uma discussão a respeito do nome dado ao algoritmo, veja [2], por exemplo. CAPÍTULO 2. ASPECTOS GERAIS 8 2.4 Experimentos Computacionais 2.4.1 Ambiente de Teste Parte importante desta dissertação são os resultados computacionais obtidos. Toda a implementação dos algoritmos foi feita em C++, sendo gcc o compilador utilizado (com opção -O3, otimização máxima). Os testes foram feitos em três diferentes máquinas: um AMD K6-2 de 350 MHz e 128 MB de RAM, um Intel Pentium II de 400 MHz e 64 MB de RAM e uma Sun UltraSparc 1 de 167 MHz e 64 MB de memória (em cada capítulo, cará claro qual máquina foi utilizada para os experimentos relatados). Nas duas primeiras máquinas foi utilizado o sistema operacional Linux; na terceira, Solaris. Os dois PCs têm desempenho muito próximo (o PC é ligeiramente mais rápido, mas não mais que 20%), mas a Sun é signicativamente mais lenta (os tempos de execução nessa máquina são de 2.5 a 3 vezes maiores). No entanto, como era essa a única máquina disponível com um resolvedor de programas lineares (CPLEX 5.0 [29]), foi ela a utilizada no branch-and-cut apresentado no Capítulo 7. Os tempos de execução foram medidos com a função getrusage, que computa apenas o tempo de CPU efetivamente utilizado pelo processo. Esse método tem precisão de 1/60 de segundo. Para muitos dos algoritmos testados, especialmente as heurísticas construtivas (Capítulo 3) e os métodos de busca local (Capítulo 4), essa precisão é insuciente para permitir uma análise adequada, já que os tempos de execução são muito pequenos, às vezes inferiores a um milésimo de segundo. Assim sendo, nesses casos fez-se necessário executar cada método mais de uma vez sobre cada instância testada: os algoritmos foram executados repetidas vezes até que se completassem cinco segundos. Em cada caso, apenas o tempo total foi medido pela função getrusage; o tempo de cada execução foi calculado pela divisão do tempo total pelo número de execuções. No caso dos dos métodos de busca local (apresentados no Capítulo 4), a mesma semente para o gerador de números aleatórios foi utilizada em todas as execuções. Já para as heurísticas construtivas, a semente foi aleatoriamente determinada em cada execução. Na comparação entre os métodos, considerou-se a média das soluções obtidas. 2.4.2 Instâncias de Teste Instâncias para o problema de Steiner são muito dependentes da aplicação a que se relacionam. Para o projeto de circuitos VLSI, por exemplo, normalmente as instâncias são retilineares, com um número relativamente grande de nós (e às vezes de terminais) e baixa densidade (o grau dos nós é limitado a quatro). Em outros casos, o número de nós é menor, mas o número de arestas pode se multiplicar. Quando se desenvolve um algoritmo de propósito geral, é interessante que seja testado em um conjunto de instâncias que seja tão diversicado quanto possível. CAPÍTULO 2. ASPECTOS GERAIS 9 Sendo o problema de Steiner em grafos um dos mais extensivamente estudados, há uma multiplicidade de instâncias de teste disponíveis na literatura. São duas as principais vantagens de se utilizarem instâncias conhecidas em lugar de gerá-las aleatoriamente. Em primeiro lugar, parte delas representa grafos encontrados em situações reais. Nem sempre um algoritmo adequado para instâncias aleatórias tem desempenho satisfatório para os casos encontrados na prática (e vice-versa). Em segundo lugar, não só são conhecidas soluções exatas para um grande número dessas instâncias [32, 46, 44, 58], mas também estão disponíveis os resultados obtidos por diversas heurísticas e metaeurísticas de qualidade previamente desenvolvidas [3, 15, 16, 21, 38, 49]. Com isso, é possível comparar os novos algoritmos, como alguns dos apresentados nesta dissertação, com alguns dos mais eciente códigos já publicados. Em algumas situações, por outro lado, serão utilizadas instâncias aleatórias, construídas sob medida para testar aspectos especícos dos algoritmos implementados. No Capítulo 3, por exemplo, faz-se isso para estudar de forma precisa a inuência do número de terminais sobre o tempo total de execução de diversos algoritmos. Nas subseções se seguem, serão descritas as instâncias de teste utilizadas ao longo do algoritmo. São ao todo quatro classes: OR-Library, Incidência, VLSI e PUC. As três primeiras séries estão disponíveis na Internet no repositório SteinLib [33]. A classe PUC, criada por Rosseti et al. [51], estará disponível no mesmo repositório a partir de agosto de 2001. As quatro classes constituem um conjunto bastante heterogêneo de instâncias, o que é muito útil para avaliar a robustez dos diversos algoritmos apresentados. 2.4.2.1 OR-Library As instâncias da classe OR-Library, introduzidas por Beasley [4], baseiam-se em grafos aleatórios com pesos inteiros uniformemente distribuídos no intervalo [1; 10]. Serão testadas três séries distintas, caracterizadas pelo número de vértices (n): c (500 vértices), d (1000 vértices) e e (2500 vértices). Cada série tem 20 instâncias, resultado da combinação de quatro diferentes números de arestas (1:25n, 2n, 5n, 25n) e cinco diferentes números de terminais (5, 10, n=6, n=4, n=2) (os valores são arredondados para o inteiro mais próximo quando necessário). Soluções ótimas são conhecidas para todas as instâncias dessa série [32]. 2.4.2.2 Incidência Propostas por Duin e Volgenant [12, 14], estas instâncias são construídas sobre grafos aleatórios e têm como principal propriedade o fato de que suas arestas têm pesos de incidência. O peso de uma aresta é um inteiro escolhido em uma distribuição normal cuja média depende do número de terminais em que a aresta é incidente (0, 1 ou 2). Se ambas as extremidades da aresta forem não-terminais, a média é 100; se a aresta for incidente em exatamente um terminal, a média será 200; se for incidente em dois terminais, a CAPÍTULO 2. ASPECTOS GERAIS 10 média será 300. Essa escolha de pesos tem a nalidade de tornar pouco ecazes os testes de redução (pré-processamento) normalmente utilizados (veja Seção 2.5). Na prática, é realmente isso o que ocorre. A classe das instâncias de incidência está dividida em quatro séries, de acordo com o número de vértices (n): i080, i160, i320 e i640. Em cada série, há 20 diferentes combinações de números de terminais e números de arestas. Em uma série com n vértices, há cinco diferentes números de arestas (3n=2, bn log p nc, n(pn 1)=2, 2n, n(n 1)=10) e quatro diferentes números de terminais (blog nc, b nc, b2 nc, n=4). Para cada combinação de arestas se terminais há cinco instâncias distintas. Cada série tem, portanto, 100 instâncias.2 A nomenclatura utilizada na SteinLib é ivvv-xyz, sendo: vvv: número de três dígitos indicando a quantidade de vértices (080, 160, 320 ou 640); x: um dígitopentre 0 e 3 identicando o número de terminais (0 signica blog nc, 1 signica b nc, etc.); y: um dígito entre 0 e 4, numa referência ao número de arestas (0 signica 3n=2, 1 signica bn log nc, etc.); z: um número de 1 a 5, numa referência à semente aleatória utilizada Alguns dos algoritmos serão testados com apenas 20 instâncias de cada série (apenas uma para cada combinação de dimensões). Seguindo o que foi feito [32], serão consideradas as instâncias terminadas em 1. São conhecidas as soluções ótimas para todas as instâncias das séries i080 e i160 [32, 33]. Para a série i320, o primeiro algoritmo capaz de resolver à otimalidade todas as instâncias é o branch-and-ascent apresentado na Seção 7.3, cuja primeira versão foi apresentada em [44]. Da série i640, uma signicativa parcela dos problemas permanece em aberto. 2.4.2.3 VLSI Ao contrário das duas classes anteriores, as instâncias VLSI originam-se de um problema prático, o projeto de circuitos integrados. Introduzidas por Koch e Martin [32], correspondem a grafos em grade com buracos (regiões retangulares cujos vértices foram eliminados). O número de terminais varia entre 10 e 2344. O número de vértices e arestas varia de poucas centenas até jV j = 36711 e jE j = 68117 (instância alut2625). A nomenclatura adotada por Koch e Martin divide a classe VLSI em sete séries: alue, alut, dmxa, diw, gap, msm e taq. Para todas as instâncias, o ótimo é conhecido [32, 44, 46, 58]. As quatro maiores instâncias (alue7065, alue7080, alut2610 e alut2625) foram resolvidas pela primeira Para a série i640, só foi possível utilizar 97 instâncias, uma vez que os arquivos da SteinLib representando as instâncias i640-022, i640-023 e i640-024 apresentavam erros. 2 CAPÍTULO 2. ASPECTOS GERAIS 11 vez usando o algoritmo descrito na Seção 7.2, apresentado em versão preliminar em [44]. A Figura 7.1 (página 123) mostra a instância alue7080 e sua solução ótima. 2.4.2.4 PUC As instâncias da classe PUC foram criadas por Rosseti et al. em [51] com o objetivo de testar os limites tanto de heurísticas quanto de algoritmos exatos para o problema de Steiner em grafos. Apesar de terem dimensões semelhantes às das instâncias previamente disponíveis na SteinLib, as instâncias da classe PUC são consideravelmente mais difíceis. A classe se subdivide em três séries: 1. hc: Correspondem a hipercubos, com número de vértices variando entre 128 e 4096. Em todos os casos, o número de arestas é (jV j log jV j)=2 e o de terminais, jV j=2. No total, a série é composta por 14 instâncias. 2. cc: Instâncias derivadas do problema de cobertura de códigos, com número de vértices variando entre 64 e 4096, jE j variando entre 192 e 24574 e jT j entre 8 e 473. Esta série contém 26 instâncias. 3. bip: Instâncias sobre grafos bipartidos derivados de instâncias para o problema de cobertura de conjuntos disponíveis na OR-Library [4]. O número de vértices nessa série varia entre 550 e 3140; o de arestas, entre 5013 e 18073; e o de terminais, entre 50 e 300. A série contém 10 instâncias. Cada uma das séries acima está dividida em duas subséries de igual tamanho: nas subséries do tipo u (hc-u, cc-u, bip-u), os pesos das arestas são inteiros pequenos (1, 2, ou 3); nas subséries do tipo p, os pesos são multiplicados por 100 e a eles é adicionada uma perturbação aleatória. Instâncias do tipo p tendem a ser mais desaadoras para heurísticas primais. 2.4.2.5 Valores de Referência Em muitas situações ao longo desta dissertação, faz-se necessário medir a qualidade das soluções fornecidas pelos métodos estudados. Normalmente, basta utilizar como referência a solução ótima de cada instância. Entretanto, para boa parte da classe PUC e da série i640, as soluções ótimas não são conhecidas. Nesses casos, utilizam-se como referência as melhores soluções primais encontradas na literatura até o momento. Para a classe PUC, tais soluções são apresentadas em [51] e foram obtidos por longas execuções (chegando a dias) da metaeurística HGP+PR, descrita no Capítulo 5. Para a série i640, foram utilizados os limites superiores disponíveis na SteinLib em 1 de julho de 2001 (parte dessas soluções também foi obtida por longas execuções do método HGP+PR). o CAPÍTULO 2. ASPECTOS GERAIS 12 2.5 Pré-processamento Um importante elemento utilizado na prática para a resolução de instâncias do problema de Steiner em grafos é o pré-processamento. Executados em tempo polinomial, algoritmos de pré-processamento utilizam implicações lógicas para reduzir as dimensões do grafo (jV j, jE j e jT j). Há situações em que se pode concluir que existe pelo menos uma solução ótima que não contém determinada aresta. Essa aresta pode ser removida do grafo. O exemplo mais simples em que isso ocorre é o seguinte [14]: Todo vértice não-terminal v de grau unitário em G, assim como a aresta que nele incide, pode ser eliminado. (Motivo: a partir de qualquer solução válida que contenha esse vértice, pode-se criar uma solução de valor menor, também válida, se v e a aresta associada forem removidos.) Em outros casos, ocorre o contrário: pode-se determinar que existe pelo menos uma solução ótima que contém uma aresta. Essa aresta pode ser contraída sem prejuízo da solução. Um exemplo simples [14]: Se uma aresta e for a única incidente em um determinado terminal t e se jT j 2, então e obrigatoriamente estará em qualquer solução ótima. (Motivo: o terminal deve fazer parte de toda solução, e a única maneira de conectá-lo ao restante do grafo é através de e.) Em situações extremas, o pré-processamento pode ser suciente para encontrar a solução ótima para uma instância. Evidentemente, nem sempre é esse o caso, já que os procedimentos de pré-processamento podem ser executados em tempo polinomial e o problema de Steiner em grafos é NP-completo. No entanto, o pré-processamento pode ser útil mesmo quando não encontra a solução ótima. A execução de um algoritmo exato para o problema tende a ser mais rápida no grafo pré-processado que no grafo original, devido à redução em suas dimensões. Da mesma forma, ele pode contribuir para a aceleração das heurísticas (e especialmente metaeurísticas) utilizadas. Outro efeito positivo do pré-processamento é o fato de que ele pode melhorar a qualidade das soluções obtidas pelos métodos aproximados. Não há, no entanto, garantia de que isso ocorra; em alguns casos, as soluções encontradas podem piorar. Um estudo mais aprofundado de métodos de pré-processamento não é o objetivo desta tese. No entanto, para que fosse possível comparar as técnicas sugeridas aqui com outras existentes, foram implementados os testes de redução apresentados em [58], que, além de sugerir métodos de redução especialmente úteis para instâncias VLSI, descreve também testes anteriormente descritos por outros autores [12, 14, 63]. Mais recentemente, em [46], Polzin e Daneshmand sugerem novas implementações, signicativamente mais rápidas, para alguns dos métodos introduzidos em [12, 14]. Tendo em vista que o estudo de CAPÍTULO 2. ASPECTOS GERAIS 13 métodos de pré-processamento não é a prioridade desta dissertação, os métodos não foram reimplementados para os estudos aqui apresentados. Os métodos de pré-processamento foram aplicados sobre os grafos das classes ORLibrary e VLSI. O Apêndice A apresenta, para cada instância, o tempo necessário para a execução dos testes e as dimensões da instância resultante. Para instâncias das classes Incidência e PUC, não foi utilizada nenhuma técnica de pré-processamento, já que o efeito dos métodos conhecidos sobre essas instâncias é desprezível. Exceto no caso das heurísticas construtivas, todos os algoritmos (métodos de busca local, metaeurísticas, heurísticas duais e métodos exatos) discutidos nesta dissertação foram executados sobre as instâncias pré-processadas (no caso das classes OR-Library e VLSI). Os tempos de execução apresentados para os algoritmos não incluem o tempo de pré-processamento, a não ser que o contrário seja explicitamente indicado. Para que seja recuperado o tempo total de execução, basta somar os tempos apresentados no Apêndice A. Capítulo 3 Heurísticas Construtivas Heurísticas construtivas são métodos gulosos utilizados para a obtenção de soluções viáveis para um problema. Além de úteis por si só, são elementos importantes de métodos de busca local e metaeurísticas, conforme se verá nos Capítulos 4 e 5. Para o problema de Steiner em grafos, a literatura apresenta um grande número de métodos construtivos (veja [28, 61] para comparações entre as mais importantes). Este capítulo se restringe a heurísticas baseadas em algoritmos para o problema da árvore geradora de peso mínimo: DNH (descrita na Seção 3.1), Bor·vka (Seção 3.2), Prim (3.3), Kruskal (3.4) e Multispan (3.5). Para cada método, são discutidas as possíveis implementações e eventuais variantes. Na Seção 3.6, os métodos são comparados quanto à qualidade das soluções fornecidas e aos tempos de execução. Dois dos métodos aqui propostos não foram descritos anteriormente na literatura pesquisada: DNHz (uma variante de DNH apresentada na Seção 3.1.4) e (Bor·vka). Ambos são extensões muito simples de heurísticas previamente existentes. A principal contribuição deste capítulo, no entanto, está relacionada os métodos de Prim e Kruskal, introduzidos em [54] e muito utilizadas na prática. São propostas novas implementações que os tornam extremamente competitivos com heurísticas antes consideradas muito mais rápidas. Utilizam-se técnicas elaboradas para evitar que se realizem operações redundantes ao longo da execução dos algoritmos; na prática, isso resultou em ganhos assintóticos nos tempos de execução dos algoritmos para todas as classes de instâncias testadas. No pior caso, o tempo de execução das implementações propostas é O(jT jjE j log jV j), mas essa complexidade só se manifesta em casos muito especiais. Para as instâncias da literatura, os algoritmos comportam-se como se não dependessem de jT j, tendo complexidade aparente O(jE j log jV j). 14 CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 15 3.1 Distance Network Heuristic (DNH) A heurística DNH, como o próprio nome indica, baseia-se no grafo de distâncias , uma importante ferramenta auxiliar também para outros algoritmos apresentados ao longo desta dissertação. O grafo de distâncias, representado por D(G), tem os mesmos vértices do grafo original, mas é completo. Cada aresta (u; v) em D(G) tem custo igual à distância entre os vértices u e v em G; portanto, (u; v) é uma superaresta que representa o caminho mínimo entre u e v no grafo original. Dado um subconjunto de vértices X V , denota-se por DX (G) o subgrafo de D(G) induzido por X . A Distance Network Heuristic consiste simplesmente no cálculo da árvore geradora mínima de DT (G) (o subgrafo induzido por T do grafo de distâncias de G) e na substituição de cada superaresta pelas arestas correspondentes do grafo original (G). É importante observar que não necessariamente as arestas do grafo de distâncias representam caminhos disjuntos no grafo original; portanto, é comum que o custo da solução no grafo original seja menor que o custo dessa árvore geradora mínima (maior não pode ser). A implementação trivial desse método consiste em determinar explicitamente DT (G) e calcular sua árvore geradora mínima. A determinação de DT (G) é feita executando-se O(jT j) algoritmos de Dijkstra, o que pode ser feito em tempo O(jT j(jE j + jV j log jV j)). Sua árvore geradora mínima de DT (G) pode ser calculada em tempo O(jT j ) utilizandose o algoritmo de Prim. A extração da árvore de Steiner correspondente (sobre o grafo original) pode ser feita em tempo O(jE j). O tempo total de execução é dominado pela construção do grafo: O(jT j(jE j + jV j log jV j)). Essa implementação requer O(jV j + jT j ) posições de memória adicionais. Em [39], Melhorn sugere uma implementação assintoticamente mais eciente para esse algoritmo, com a vantagem adicional de utilizar apenas O(jV j) posições de memória adicionais. Essa implementação baseia-se na construção do diagrama de Voronoi, apresentado em detalhes a seguir. 2 2 3.1.1 Diagrama de Voronoi O conceito de diagrama de Voronoi é freqüentemente usado na área de geometria computacional [5, 47]. Considere um conjunto p ; p ; : : : ; pn de pontos no plano. O diagrama de Voronoi é a subdivisão do plano em n regiões (vor (p ); vor (p ); : : : ; vor (pn)), sendo vor (pi ) a região de Voronoi associada a pi . Ela é denida como o conjunto de todos os pontos p do plano mais próximos de pi que de qualquer outro ponto do conjunto fp ; p ; : : : ; png (ou seja, d(p; pi) d(p; pj ), para todo 1 j n). Esse conceito pode ser estendido de forma imediata para o problema de Steiner em grafos. No caso geométrico, o diagrama de Voronoi associa qualquer ponto do plano a um ponto do conjunto fp ; p ; : : : ; png. Para o problema de Steiner em grafos, o diagrama associa qualquer vértice v 2 V (terminais e não-terminais) a um dos terminais t 2 T . Um 1 2 1 1 2 1 2 2 CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 16 vértice v pertence a vor (ti) se a distância de v a ti não for superior à distância de v a qualquer outro terminal. Empates (casos em que um vértice é eqüidistante de diversos terminais) são decididos de forma arbitrária.1 O diagrama de Voronoi associado ao conjunto de terminais fornece as seguintes informações a respeito de cada vértice v: base (v): o terminal mais próximo de v; p(v; base (v)): o caminho mais curto de v à base; d(v): distância de v à sua base, ou seja, o comprimento de p(v; base (v)). Em especial, para todo terminal ti vale que base (ti) = ti , d(ti) = 0. O caminho mais curto de um terminal à sua base (ele próprio) não tem aresta alguma. O diagrama de Voronoi é construído por uma versão ligeiramente modicada do algoritmo de Dijkstra com múltiplas fontes (todos os terminais). Ao longo da execução do algoritmo, associam-se a cada vértice v três valores: base (v), d(v) e pred (v) (o predecessor de v no caminho entre ele e sua base). Como a atualização desses valores não altera a complexidade do algoritmo de Dijkstra, a construção do diagrama de Voronoi pode ser feita em tempo O(jE j + jV j log jV j). Uma vez construído o diagrama de Voronoi, é possível determinar em tempo constante os valores de base (v) e d(v) para qualquer vértice v. O caminho mínimo entre v e base (v) (que tem comprimento d(v), evidentemente) pode ser recuperado em tempo proporcional ao número de arestas no caminho; basta para isso percorrer a lista formada pelos apontadores pred que se inicia em pred (v). A utilidade do diagrama de Voronoi para o problema de Steiner em grafos está no fato de fornecer de forma simples importantes informações sobre a distância entre os terminais. Muito importante para isso são as arestas de fronteira : arestas que ligam duas diferentes regiões de Voronoi. Formalmente, uma aresta (u; v) é considerada de fronteira se base (u) 6= base (v). Associa-se a toda aresta de fronteira (u; v) um caminho entre dois terminais, base (u) e base (v). O comprimento desse caminho é justamente d(u) + c(u; v) + d(v). A seguir, apresenta-se uma propriedade muito simples, mas essencial para uma implementação eciente de vários dos algoritmos aqui propostos (Bor·vka, Prim e Kruskal). Teorema 1 Dado um terminal ta, seja tb o terminal tal que d(ta; tb) < d(ta ; tc) para todo terminal tc . Então existe uma aresta de fronteira no diagrama de Voronoi que representa o caminho mínimo entre ta e tb . No caso geométrico, nas situações de empate a convenção adotada é considerar que o ponto pertence a todas as regiões envolvidas; isso poderia também ser feito no caso do problema de Steiner em grafos, mas é algoritmicamente mais interessante efetuar o desempate. 1 CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 17 Prova Seja p(ta ; tb) o caminho mínimo entre os terminais. Seja u um vértice desse caminho tal que ele e todos os seus antecessores pertecem a vor(ta ) e seu sucessor (v) não pertence a vor(ta ). Existe um tal vértice, já que ta 2 vor(ta ) e tb 62 vor(ta ). Por denição, a aresta (u; v) é uma aresta de fronteira. Resta provar que v 2 vor(tb ). Suponha que isso não seja verdade, ou seja, que exista um terminal tc (c 6= a) cuja distância a v (dc(v)) seja menor que a distância entre v e tb (db(v)). Então existe um caminho entre ta e tc de comprimento da (u) + c(u; v) + dc(v), menor que o comprimento do caminho mínimo entre ta e tb (da (u) + c(u; v) + db(v)), o que contradiz o fato de tb ser o terminal mais próximo de ta. Portanto, conclui-se que (u; v) é uma aresta de fronteira que representa o caminho mínimo entre ta e tb . 2 Por simplicidade, o teorema acima aplica-se ao caso em que o terminal mais próximo de ta é único. Nos casos em que há empate, pode-se garantir que haverá uma aresta de fronteira ligando ta a pelo menos um dos terminais mais próximos. 3.1.2 Implementação da Heurística DNH (DNH-Prim) Com o auxílio do Diagrama de Voronoi, a árvore geradora mínima de DT (G) pode ser calculada sem que seja necessário criar explicitamente o grafo DT (G). Para isso, é necessário utilizar uma propriedade ainda mais forte que a apresentada no Teorema 1: existe uma árvore geradora mínima de DT (G) formada apenas por caminhos associados a arestas de fronteira do diagrama de Voronoi de G. A prova desse fato, apresentada em [39], é omitida aqui. Assim sendo, a solução da heurística DNH pode ser obtida calculando-se a árvore geradora mínima do grafo G0 , que tem exatamente as mesmas arestas de G, mas com pesos alterados a partir das informações fornecidas pelo diagrama de Voronoi. Dada uma aresta (u; v) de G, determina-se seu custo c0(u; v) em G0 da seguinte forma: se base (u) = base (v), então c0(u; v) = 0; se base (u) 6= base (v), então c0(u; v) = d(u) + c(u; v) + d(v). Dessa forma as arestas de fronteira concentram os pesos dos caminhos que representam. Como todas as demais arestas têm peso zero, farão parte da árvore geradora de G0 exatamente jT j 1 arestas de fronteira, necessárias para conectar as diferentes regiões de Voronoi. Para obter a árvore de Steiner correspondente, basta fazer a expansão das arestas de fronteira, extraindo delas os caminhos correspondentes. Se uma mesma aresta do grafo G estiver contida em mais de uma aresta de fronteira selecionada, cópias adicionais devem ser ignoradas. Conforme já mencionado, o custo da primeira fase da heurística a construção do algoritmo de Voronoi pode ser feita em tempo O(jE j + jV j log jV j). A segunda fase o cálculo da árvore geradora mínima também pode ser implementada com essa CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 18 complexidade se for utilizado o algoritmo de Prim. No restante desta dissertação, será utilizada a denominação DNH-Prim nas referências a essa implementação. 3.1.3 Implementação Alternativa (DNH-Bor·vka) A segunda fase da implementação DNH-Prim consiste no cálculo da árvore geradora de peso mínimo do grafo com pesos modicados, da qual se extrai posteriormente a árvore de Steiner. Considera-se aqui uma implementação alternativa, DNH-Bor·vka, em que na segunda fase a árvore de Steiner é calculada diretamente. Para isso, utiliza-se uma versão ligeiramente modicada do algoritmo de Bor·vka para o problema da árvore geradora de peso mínimo (apresentado na Seção 2.3.1). Em sua versão original, em cada iteração desse algoritmo cada vértice é ligado ao vértice mais próximo. Na versão modicada, em cada etapa liga-se cada terminal ao terminal mais próximo. Para determinar o vizinho mais próximo de cada terminal, basta percorrer a lista de arestas de fronteira no diagrama de Voronoi. Deve-se levar em conta não o custo original da aresta, mas o custo modicado: conforme já mencionado, cada aresta (u; v) representa um caminho de custo d(u) + c(u; v) + d(v) entre os terminais base (u) e base (v). Ao nal da primeira etapa, liga-se cada terminal ao terminal mais próximo. Na segunda etapa, percorre-se novamente a lista de arestas de fronteira, tomando-se o cuidado de considerar apenas as arestas (u; v) tais que base (u) e base (v) pertençam a diferentes componentes conexas. Sucedem-se as etapas até que todos os terminais estejam conectados. Como no algoritmo de Bor·vka original, cada iteração pode ser executada em tempo O(jE j) e reduz o número de componentes pelo menos à metade. Diferentemente do algoritmo original, contudo, o número inicial de componentes é jT j, não jV j. Assim sendo, são executadas no máximo O(log jT j) iterações, o que faz com que a segunda fase da heurística DNH-Bor·vka tenha complexidade O(jE j log jT j). A complexidade de toda a heurística, portanto, é O(jE j log jT j + jV j log jV j). Se o número de terminais for relativamente pequeno, essa implementação tende a ser mais rápida que DNH-Prim. 3.1.4 Variante DNHz É possível fazer uma pequena alteração na implementação DNH-Bor·vka que contribui para melhorar a qualidade das soluções obtidas sem alterar o tempo de execução do algoritmo. A alteração ocorre na segunda fase da heurística, em que o algoritmo de Bor·vka é utilizado para encontrar a árvore de Steiner com base no diagrama de Voronoi previamente construído. Após a primeira iteração do algoritmo de Bor·vka, vários pares de terminais são unidos pelos caminhos mais curtos entre eles. Cada um desses caminhos pode ter vértices intermediários, que passam a fazer parte da solução. CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 19 Na implementação DNH-Bor·vka, esse fato não é levado em conta. Depois da adição de um vértice não-terminal v a solução, iterações posteriores continuam utilizando o valor original de d(v) (a distância de v ao terminal mais próximo), quando poderiam utilizar d(v) = 0. Fazendo-se d(v) 0 após a adição de cada novo não-terminal v à solução, algumas arestas de fronteira têm seus pesos reduzidos e podem passar a ser escolhidas em virtude disso. Essa pequena alteração permite que se encontrem atalhos entre diferentes componentes em iterações posteriores. No restante desta dissertação, será utilizada a denominação DNHz para essa variante, sendo o z referente à atribuição do valor zero. A complexidade de pior caso dessa variante é idêntica à da implementação DNH-Bor·vka. De fato, conforme se verá na Seção 3.6, ambos os algoritmos têm tempos de execução praticamente idênticos, mas o método DNHz produz soluções de qualidade superior. 3.2 Bor·vka Esta seção apresenta a heurística de Bor·vka para o problema de Steiner em grafos, baseada no algoritmo homônimo para o problema da árvore geradora de peso mínimo (MST) descrito na Seção 2.3.1. O algoritmo original para o MST se inicia com jV j componentes conexas, cada uma formada por um único vértice. Em cada iteração, cada componente é ligada à componente que lhe é mais próxima, formando uma nova componente. O algoritmo prossegue até que reste uma única componente conexa. A heurística para o problema de Steiner em grafos é similar. Inicia-se o algoritmo com jT j componentes conexas, todas compostas por um único terminal. Em cada iteração, toda componente conexa é ligada à componente que lhe é mais próxima. Passam a fazer parte da nova componente conexa não só os vértices das componentes originais, mas também os do caminho entre elas. (Os novos vértices adicionados podem ser utilizados como bases para a ligação de diferentes componentes em iterações posteriores.) O algoritmo termina assim que todos os terminais estiverem conectados. Tanto no caso do algoritmo para o problema da árvore geradora mínima quanto da heurística para o problema de Steiner em grafos, em cada iteração o número de componentes reduz-se pelo menos à metade. Isso signica que serão necessárias no máximo log jV j iterações no algoritmo para o cálculo da árvore geradora de custo mínimo e log jT j na heurística para o problema de Steiner em grafos (potencialmente menor, portanto). Resta determinar o tempo necessário para executar cada iteração da heurística. Recapitulando, a operação básica a ser feita é identicar, para cada componente conexa, qual a sua componente conexa mais próxima. Conforme mostra o Teorema 1, é justamente essa a principal utilidade do diagrama de Voronoi. Uma vez construído o diagrama de Voronoi, basta percorrer as arestas de fronteira para determinar os pares de componentes próximas, de forma análoga ao que se fez nas heurísticas DNH-Bor·vka e DNHz. Naqueles casos, CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 20 contudo, basta calcular o diagrama de Voronoi uma única vez, no início do algoritmo. Na heurística de Bor·vka, o diagrama de Voronoi deve ser recalculado após cada iteração, pois os novos não-terminais adicionados à solução também passam a ser bases de regiões de Voronoi. A complexidade de cada iteração da heurística de Bor·vka é dominada justamente pelo cálculo do diagrama de Voronoi: O(jE j + jV j log jV j). Como há no máximo log(jT j) iterações, a complexidade da heurística como um todo é O((log jT j)(jE j + jV j log jV j)). Na prática, o número de iterações é freqüentemente menor que log jT j. Além disso, o algoritmo pode ser acelerado se consideramos que não é necessário recalcular o diagrama de Voronoi completo em cada iteração. O diagrama de Voronoi obtido numa iteração pode ser simplesmente atualizado na iteração seguinte. A construção completa exige a execução de um algoritmo de Dijkstra modicado tendo como origens todos os vértices que fazem parte de alguma componente já coberta pela árvore de Steiner. Para a atualização, basta preservar as informações de proximidade originais e usar como origem os vértices adicionados à árvore na iteração anterior (i.e., os vértices intermediários nos caminhos criados). Isso é possível porque a distância de cada vértice à sua base só pode diminuir (ou permanecer inalterada) ao longo da execução do algoritmo, já que bases nunca são removidas, apenas adicionadas. Na prática, a diferença entre a construção e a atualização do diagrama de Voronoi é expressiva; as operações de atualização tendem a ser extremamente locais, ou seja, normalmente exigem que apenas uma pequena parte dos vértices e arestas do grafo seja analisada. 3.3 Prim Em [54], Takahashi e Matsuyama analisam uma heurística baseada em caminhos mínimos, comumente conhecida como shortest path heuristic (SPH). Ela utiliza um princípio semelhante ao algoritmo de Prim [48] para o problema da árvore geradora mínima em grafos. Inicia-se o algoritmo SPH com uma solução parcial S = frg, sendo r, a raiz, um vértice qualquer (nas implementações realizadas, sempre um terminal). O algoritmo aumenta progressivamente esse conjunto, preservando sempre a invariante de que o subgrafo de G induzido por S seja conexo. Para isso, em cada iteração o algoritmo determina qual terminal ti 2 T n S tem distância mínima a S . Ao conjunto S são adicionados ti e os demais vértices no caminho entre ti e S . Ao nal de jT j iterações (ou jT j 1, se r for um terminal), o algoritmo terá encontrado o conjunto de vértices (terminais e não-terminais) que devem compor da árvore de Steiner. CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 21 3.3.1 Implementação em Duas Fases (Prim-T) A implementação da heurística SPH na literatura (veja [46] e [28], por exemplo) é usualmente feita em duas fases. Na primeira fase, determina-se o caminho mínimo de cada terminal a cada um dos demais vértices. Isso pode ser feito, por exemplo, executando-se jT j algorimos de Dijkstra, um a partir de cada terminal, o que requer O(jT j(jE j + jV j log jV j)) operações no pior caso. Os dados obtidos na primeira fase são armazenados em uma tabela com jT jjV j posições, da qual se podem extrair os caminhos e as distâncias correspondentes. A segunda fase utiliza os dados obtidos na primeira fase para construir a solução. A estrutura de dados básica utilizada nessa fase é um vetor (closest ) de jT j posições, uma associada a cada terminal. Em qualquer ponto da execução, closest [t] contém o vértice de S (a solução parcial) cuja distância a t é mínima. Conforme já mencionado, o conjunto S é inicializado com um vértice r qualquer, a raiz. Todas as posições do vetor closest são inicializadas com r (o único vértice na solução). Em cada iteração do algoritmo, um novo terminal é adicionado à solução, o que é feito em três passos: 1. Determinação do terminal t mais próximo de S . Para isso, basta percorrer o vetor closest. 2. Atualização de S . Adiciona-se à solução parcial S tanto o vértice t quanto os vértices do caminho ligando t à solução parcial anterior. O caminho pode ser obtido a partir dos dados obtidos na primeira fase do algoritmo. 3. Atualização do vetor closest . Para todo terminal ti ainda não presente em S , vericase (consultando-se a tabela construída na primeira fase) se a distância de ti a um dos novos vértices adicionados a S no passo 2 é menor que closest [ti ]. Se for, o valor de closest [ti ] é atualizado. São ao todo O(jT j) iterações (o número exato depende do fato de r ser terminal ou não). O passo 1 acima pode ser executado em tempo O(jT j) por iteração. Considerando-se todo o algoritmo, o passo 2 requer O(jV j) operações, visto que cada vértice pode entrar em S no máximo uma vez. O passo 3 é executado em tempo total O(jT jjV j), pois para cada vértice adicionado a S é necessário fazer O(jT j) comparações (uma para cada terminal ainda não inserido em S ). Assim sendo, a segunda fase da heurística requer O(jT jjV j) operações, o que signica que a complexidade do algoritmo como um todo é dominada pela primeira fase: O(jT j(jE j + jV j log jV j). Aceleração Em [46], Polzin e Daneshmand sugerem algumas acelerações para a implementação acima. Na primeira fase do algoritmo, é possível limitar o alcance dos jT j algoritmos de Dijkstra utilizados para construir a tabela de distâncias. A execução a partir de um terminal t não precisa alcançar todos os demais vértices. Ela pode ser interrompida assim que todos os vértices dos seguintes grupos forem alcançados: CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 22 1. vértices da região de Voronoi de t; 2. todos os terminais em regiões de Voronoi vizinhas de t. Alguns valores da tabela de distâncias permanecerão não preenchidos se esses critérios de parada forem utilizados. Em [46], prova-se que esses valores não são necessários para a segunda fase do algoritmo. Essas condições não garantem ganhos assintóticos no pior caso, mas podem levar a menores tempos de execução na prática. Em geral, os efeitos tendem a ser mais relevantes para instâncias pouco densas. Para instâncias mais densas, o número de terminais em regiões vizinhas a t é maior. Polzin e Daneshmand propõem também um método para acelerar a segunda fase do algoritmo. Como o tempo de execução é claramente dominado pela primeira fase do algoritmo, essa aceleração não foi implementada. Ao longo desta dissertação, será utilizado o nome Prim-T em referências a essa implementação da heurística de Prim (incluindo a aceleração). A letra T lembra o fato de que o algoritmo baseia-se na construção de uma tabela de distâncias. 3.3.2 Implementação Direta (Prim-D) Apresenta-se nesta seção Prim-D, uma implementação direta (em uma única fase) da heurística de Prim. O algoritmo se inicia pela criação da solução parcial inicial S = frg. Em cada iteração, executa-se um algoritmo de Dijkstra com origem em todos os vértices que pertencem a S . As execuções param assim que um terminal t que não pertence a S é alcançado. Adicionam-se então a S o terminal t e os vértices do caminho que leva a ele. Após jT j 1 ou jT j iterações (dependendo do fato de r ser ou não raiz), o algoritmo estará concluído. A complexidade dessa implementação é a de Prim-T, descrita na seção anterior: O(jT j(jE j + jV j log jV j)). Por estar dividida em duas fases, a implementação Prim-T pode ser mais vantajosa quando se deseja efetuar mais de uma execução do algoritmo. Depois de executada uma única vez a primeira fase, podem ser efetuadas tantas execuções da segunda fase (com diferentes raízes) quantas forem necessárias. Cada uma delas pode ser executada em tempo O(jT jjV j). Por outro lado, a implementação direta requer apenas O(jV j) posições de memória adicionais, enquanto a implementação em duas fases requer a construção de uma tabela com O(jT jjV j) elementos. Para muitas instâncias, a execução do algoritmo pode se tornar inviável (entre as instâncias apresentadas na Seção 2.4.2, há algumas com dezenas de milhares de vértices e milhares de terminais). Tanto a implementação baseada na construção da tabela de distâncias quanto a implementação direta requerem O(jT j) execuções do algoritmo de Dijkstra. Na versão mais simples da implementação em duas fases, as execuções são completas. Cada algoritmo de Dijkstra é executado até que todos os vértices do grafo sejam alcançados. Com a CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 23 aceleração proposta por Polzin e Vahdati [46], o alcance de cada execução é limitado à região de Voronoi a que pertence a origem e aos terminais vizinhos. No caso da implementação direta, cada execução do algoritmo de Dijkstra é ainda mais reduzida, pois pode ser interrompida assim que um terminal que não está na solução for alcançado. Renamento O principal problema dessa implementação é o fato de que, à medida que avança o algoritmo, o número de origens (do algoritmo de Dijkstra) aumenta, o que não ocorre na implementação em duas fases. No entanto, é possível adotar medidas para evitar, sempre que possível, que esse problema ocorra. Apesar de as medidas não alterarem a complexidade de pior caso do algoritmo, na prática se observam acelerações consideráveis. As diversas aplicações do algoritmo de Dijkstra são muito semelhantes ao longo da heurística. A única diferença entre uma iteração e a seguinte está no fato de que alguns nós (os do novo caminho adicionado) passam a ter distância zero à solução parcial S . Os nós que já tinham distância zero não são afetados e os pesos das arestas permanecem inalterados. Como resultado, a distância de di(v) de um vértice v à árvore existente no inicio da iteração i jamais será maior que di (v), a distância de v à árvore no início da iteração i + 1. Isso signica que os valores presentes no heap ao nal da execução do algoritmo de Dijkstra na iteração i representam limites superiores para as distâncias dos nós à árvore na iteração i + 1. Conseqüentemente, entre uma iteração e outra não é necessário reinicializar o heap ; basta inserir (com distância zero) os vértices do novo caminho adicionado. Como os novos vértices serão os primeiros a ser retirados, a execução normal do algoritmo de Dijkstra fará com que os valores das distâncias associadas demais vértices presentes no heap (que, conforme já discutido, representam limites superiores) sejam devidamente atualizadas. A distância de um vértice v diminuirá se ele estiver mais próximo de um dos vértices do caminho adicionado do que do restante da árvore (pode-se dizer que um atalho foi encontrado). Se o novo caminho em nada contribuir para que o vértice seja alcançado a partir da árvore, sua distância permanecerá inalterada (e correta). Além da atualização das distâncias dos vértices já presentes no heap, outros vértices poderão ser adicionados antes que outro terminal seja encontrado. Se forem vértices que nunca passaram pelo heap, a inserção é inevitável. No entanto, se um vértice v já tiver passado pelo heap em alguma iteração anterior, há casos em que a inserção é desnecessária. Considere que, ao ser retirado do heap na iteração i do algoritmo, a distância de v à árvore seja di(v). O processamento de v consiste em percorrer sua vizinhança e atualizar o heap se necessário. Mais especicamente, seja w um vizinho de v ainda não inserido na árvore e c(v; w) o peso da aresta que liga v a w. O heap só deve ser atualizado se di(v) + c(v; w) for menor que a menor distância calculada em iterações anteriores entre w e a árvore. Considere agora o que ocorria se v fosse inserido novamente no heap numa iteração posterior j com distância dj (v) di(v). Quando v for novamente retirado, percorrer sua vizinhança será inútil: como os pesos das arestas não mudam, a distância +1 CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 24 obtida para cada um de seus vizinhos jamais será menor que a calculada na iteração i. Conclusão: não é necessário inserir no heap um vértice cuja distância à árvore seja superior àquela que possuía quando retirado do heap em alguma iteração anterior. Esse procedimento evita que uma série de cálculos idênticos seja refeita em diferentes iterações. Apesar de não alterar a complexidade assintótica do algoritmo no pior caso, em termos práticos essas medidas normalmente levam a uma signicativa melhora de desempenho. Além disso, mesmo nas instâncias em que seu efeito é pequeno, a implementação renada nunca precisará percorrer a vizinhança de mais vértices que a implementação trivial. Depois de passar a fazer parte da solução, um vértice terá sua vizinhança percorrida exatamente uma vez na versão renada. A implementação trivial, por outro lado, requer que ela seja inspecionada em todas as iterações até o m do algoritmo. Na Seção 3.6, foi testada apenas a versão renada da implementação Prim-D, visto que ela é claramente superior à versão trivial. 3.4 Kruskal Considera-se agora uma heurística construtiva para o problema de Steiner em grafos baseada no algoritmo de Kruskal para árvores geradoras mínimas (MST). Conforme mencionado na Seção 2.3.1, o algoritmo de Kruskal para o MST consiste em (1) ordenar as arestas em ordem não-decrescente de peso e (2) inseri-las nessa ordem até que se forme uma árvore geradora. Arestas que formariam ciclos são descartadas. Em sua versão heurística adaptada para o problema de Steiner, descrita por Takahashi e Matsuyama [54], são adicionados caminhos no lugar das arestas. O algoritmo se inicia com jT j componentes distintas, cada uma correspondendo a um dos terminais. A distância entre duas componentes é denida como o comprimento do menor caminho entre dois de seus vértices. Em cada iteração do algoritmo, as duas componentes mais próximas são unidas, formando uma nova componente conexa, à qual se incorporam também os vértices no caminho mínimo que liga as duas componentes. Como existem inicialmente jT j componentes disjuntas, serão necessárias exatamente jT j 1 iterações para que todos os terminais estejam conectados e se crie uma árvore de Steiner. 3.4.1 Implementação Básica (Kruskal-B) A operação básica realizada em cada iteração do algoritmo é determinar qual o par de componentes mais próximas entre si. Na primeira iteração, executa-se um algoritmo de Dijkstra a partir de cada terminal. Feito isso, pode-se determinar o par mais próximo de componentes (terminais) em tempo O(jT j). Nas iterações seguintes, não é necessário executar novamente o algoritmo de Dijkstra jT j vezes. Suponha que duas componentes, a e b, sejam unidas na iteração i. No início dessa iteração era conhecida, para cada componente, sua vizinha mais próxima. Depois da união de a e b, só poderá haver CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 25 mudanças no conjunto de distâncias se a nova componente (formada por a e b) tornarse a mais próxima de alguma das demais componentes. Assim sendo, basta executar o algoritmo de Dijkstra a partir dos vértices da componente ab, sem interrompê-lo antes que todas as demais componentes sejam alcançadas. Dessa forma, é possível atualizar tanto a componente ab quanto as demais. A primeira iteração do algoritmo requer O(jT j) execuções do algortimo de Dijkstra, cada uma realizada em tempo O(jE j + jV j log jV j). Cada uma das O(jT j) iterações posteriores requer apenas uma execução do algoritmo de Dijkstra. A complexidade total do algoritmo, portanto, é O(jT j(jE j + jV j log jV j)). Essa implementação básica da heurística de Kruskal (que recebe a denominação de Kruskal-B) tem uma deciência básica: ela exige que se execute um algoritmo de Dijkstra completo em cada iteração, o que pode ser muito custoso. Nas subseções seguintes, serão sugeridas implementações alternativas que aproveitam melhor as informações obtidas em iterações passadas para acelerar iterações futuras. 3.4.2 Implementação com Heap de Arestas (Kruskal-E) A implementação do algoritmo de Kruskal para o problema da árvore geradora mínima (MST) é extremamente simples. Basta ordenar previamente todas as arestas e inseri-las em ordem, evitando-se a inserção das arestas que formam ciclos. É possível implementar a heurística de Kruskal para o problema de Steiner em grafos (SPG) seguindo-se o mesmo princípio, mas é necessário lidar com algumas diculdades inexistentes no algoritmo original. Em primeiro lugar, é importante lembrar que, enquanto o algoritmo para o MST lida com arestas, a heurística para o SPG lida com caminhos. Para contornar esse problema, o primeiro passo da heurística é construir o diagrama de Voronoi e determinar suas arestas de fronteira (veja Seção 3.1.1). A cada aresta (u; v) ligando diferentes regiões de Voronoi associa-se o custo c0 (u; v) = d(u) + c(u; v) + d(v), sendo d(u) e d(v) as distâncias de u e v às respectivas bases no diagrama de Voronoi. Dessa forma, reduz-se cada caminho relevante a uma única aresta que o representa. No algoritmo para o MST, bastaria então ordenar as arestas assim obtidas e percorrer a lista em ordem, adicionando-se as arestas que determinam caminhos que não formam ciclos. Na heurística para o SPG, no entanto, o conjunto de arestas (caminhos) a ser considerado é dinâmico, varia de uma iteração para outra. Após a adição de um caminho à solução, cria-se uma nova componente, constituída não só pelos vértices das duas componentes originais, mas também pelos vértices intermediários do caminho adicionado. Caminhos que partem desses vértices devem ser considerados em iterações posteriores do algoritmo, já que podem ser menores que outros anteriormente encontrados. Assim sendo, de forma análoga ao que ocorre na heurística de Bor·vka (Seção 3.2, deve-se atualizar o diagrama de Voronoi depois de cada iteração, considerando os novos vértices intermediários como possíveis bases. Com isso, arestas que estavam na fronteira entre duas regiões de Voronoi podem deixar de estar e vice-versa. Além disso, uma aresta pode passar a repre- CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 26 sentar um caminho de tamanho diferente (obrigatoriamente menor). Isso explica por que a lista ordenada de arestas muda de uma iteração para outra. É importante observar, contudo, que nem todas as arestas (caminhos) sofrem alterações. São modicados apenas os caminhos associados a arestas que tiveram pelo menos uma de suas extremidades atualizadas pelo diagrama de Voronoi: mudará o caminho associado à aresta (u; v) se e somente se as distâncias de u ou v às respectivas bases mudarem. Durante a atualização do diagrama de Voronoi é interessante criar uma lista dos vértices modicados, já que apenas as arestas incidentes nesses vértices precisam ser vericadas. No pior caso, o número de arestas analisadas pode ser comparável ao total de arestas, mas na prática as alterações normalmente são locais, envolvendo poucos vértices e arestas. Para que essa localidade se traduza em uma aceleração expressiva da heurística para o SPG, ao invés de se fazer uma lista de arestas (como se faz no algoritmo para o problema da árvore geradora mínima), é conveniente manter um heap em que cada aresta tem associada a si o custo do caminho que representa (c0(u; v) = d(u) + c(u; v) + d(v)). Com isso, é possível atualizar o conjunto de arestas sem que seja necessário percorrê-lo por completo. Com essa estrutura de dados, cada iteração do algoritmo resume-se aos seguintes passos: 1. Escolha do caminho. Retira-se do heap a aresta (u; v) que representa o menor caminho do grafo. No caso de base (u) e base (v) pertencerem à mesma componente conexa, descarta-se a aresta. Repete-se o procedimento até que a aresta retirada de fato represente um caminho entre duas componentes conexas. 2. Adição do caminho. Cria-se uma nova componente formada pela união das que continham base (u) e base (v) e pelos vértices intermediários no caminho entre elas. 3. Atualização do Diagrama de Voronoi. Realiza-se um algoritmo de Dijkstra modicado tendo como origem os vértices intermediários do novo caminho adicionado. 4. Atualização do heap de arestas. Para todo vértice u modicado durante a atualização do diagrama de Voronoi (ou seja, vértices que tiveram o valor d(u) reduzido), percorre-se a sua vizinhança. Dada uma aresta (u; v) dessa vizinhança, são dois os casos possíveis: a. base (v) pertence a uma componente conexa diferente de base (u): nesse caso, atualiza-se o custo c0 (u; v) associado a (u; v) no heap (agora menor, pois d(u) foi reduzido); se (u; v) não estiver no heap, deve ser inserido. b. base (v) pertence à mesma componente conexa de base (u): nesse caso, nada deve ser feito, mesmo que a aresta esteja no heap. O fato de que ela não mais representa uma aresta de fronteira será tratado no passo 1 de iterações futuras. CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 27 Toda aresta entra no heap como aresta de fronteira. Ao longo da execução do algoritmo, contudo, ela pode deixar de sê-lo. Basta que as bases de suas extremidades passem a pertencer à mesma componente conexa. Depois disso, é possível que a aresta volte a ser de fronteira (se uma de suas bases mudar). A estrutura do algoritmo garante que, se a aresta for de fronteira, o valor associado a ela no heap estará correto. Se não for de fronteira, nada se garante sobre ela, que pode estar no heap ou não. Se estiver, duas situações podem ocorrer: a. ela se tornará a aresta mais prioritária do heap em alguma iteração e será retirada no passo 1; ou b. ela se tornará novamente uma aresta de fronteira em alguma iteração futura do algoritmo; como isso só pode resultar da expansão de uma região de Voronoi (passo 3), ela obrigatoriamente terá o valor associado a si reduzido. A operação decreaseKey executada no passo 4 do algoritmo a torna novamente ativa. Complexidade O algoritmo é executado em O(jT j) iterações. Em cada uma delas, é necessário atualizar o diagrama de Voronoi (passo 3), o que requer O(jE j + jV j log jV j) operações. Os passos 1 e 4, que envolvem a manipulação do heap, podem ser realizadas em tempo O(jE j log jE j) por iteração no pior caso. O passo 2 é executado em tempo O(jV j) considerando-se toda a execução do algoritmo. A complexidade dessa implementação é, portanto, O(jT jjE j log jE j). Evidentemente, seria possível fazer com que a expressão da complexidade do algoritmo fosse dominada pelas atualizações do diagrama de Voronoi: O(jT j(jE j + jV j log jV j)). Bastaria substituir o heap de arestas por uma lista. Assim, o passo 1 passaria a ser executado em tempo (jE j) e o passo 4 simplesmente desapareceria. O problema dessa implementação é dar pouca margem a acelerações expressivas na prática: seu melhor caso é (jT jjE j). Quando se utiliza o heap, pode-se explorar melhor o fato de que as atualizações do diagrama de Voronoi (e por conseguinte do próprio heap ) são muito limitadas. Por isso, preferiu-se a implementação que utiliza o heap de arestas. Deu-se o nome de Kruskal-E a essa implementação (E é uma referência ao conjunto de arestas). 3.4.3 Implementação com Heap de Vértices (Kruskal-V) Esta seção propõe uma implementação alternativa para a heurística de Kruskal que tem desempenho comparável ao de Kruskal-E, mas utiliza um heap de vértices. Devido a essa característica, essa implementação é denominada Kruskal-V. Como na implementação anterior, Kruskal-V baseia-se na construção e posterior atualização do diagrama de Voronoi. A diferença principal é a maneira de se encontrar o par mais próximo de componentes em cada iteração. CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 28 Uma vez construído o diagrama de Voronoi, associa-se a cada vértice u uma única aresta e(u), que representa o menor caminho que sai de base (u), tem u como último vértice dentro da região de Voronoi de u e chega a alguma outra componente. Determinar essa aresta é simples: basta percorrer a vizinhança de u e, para cada aresta de fronteira (u; v), calcular o custo c0(u; v) = d(u) + c(u; v) + d(v) do caminho que ela representa. A aresta escolhida é aquela para a qual esse valor é mínimo. Se não houver aresta de fronteira incidente em u, o valor de e(u) é indenido. Entre as O(jV j) arestas2 escolhidas dessa forma (uma para cada vértice), está a aresta que representa o menor caminho entre diferentes componentes do grafo. Assim, é suciente usar um heap com O(jV j) elementos (vértices) para determinar o par mais próximo de componentes em cada iteração do algoritmo. Basta associar a cada vértice v o custo do caminho representado pela aresta e(v). Depois de construído o heap, são executadas as jT j 1 iterações do algoritmo, cada uma constituída pelos seguintes passos: 1. Determinação do par mais próximo de componentes. Retira-se do heap o vértice u mais prioritário. Se e(u) não mais for uma aresta de fronteira (ela pode deixar de ser ao longo da execução do algoritmo), deve-se percorrer a vizinhança de u, determinar o valor de e(u) e, se ele for denido, reinserir o vértice no heap. A operação é repetida até que o vértice retirado esteja associado a uma aresta de fronteira, que representará o caminho mínimo entre duas componentes do grafo. 2. Adição do caminho. Sendo (u; v) a aresta prioritária, adicionam-se à solução todos os vértices do caminho entre base (u) e base (v). Esses novos vértices e as componentes originais são unidos em uma só componente. 3. Atualização do diagrama de Voronoi. Para atualizar o diagrama de Voronoi, devese executar um novo algoritmo de Dijkstra tendo como origens os novos vértices adicionados à solução. 4. Atualização do heap. Veja discussão abaixo. Dado um vértice u qualquer, seja e(u) = (u; v) a aresta associada a ele no heap. O valor associado a u no heap pode car desatualizado apenas se uma das seguintes situações ocorrer: a. o valor de d(u) diminui; b. o valor de d(w) diminui, sendo w um vizinho de u (possivelmente o próprio v); c. (u; v) deixa de ser uma aresta de fronteira. Repare que nem todas as (j j) arestas são distintas. A mesma aresta ( ) pode aparecer duas vezes, uma associada ao vértice , outra ao vértice . Conforme se verá, isso não é um problema. 2 O v V u; v u CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 29 No último caso, o valor associado ao vértice u só pode aumentar ou permanecer igual. Portanto, não é necessário identicá-lo assim que ele ocorrer: pode-se manter o vértice temporariamente no heap com valor desatualizado e tratar esse caso quando o elemento for removido (passo 1 do algoritmo) ou quando ocorrer o caso (a.) ou o caso (b.) acima. Esses dois casos devem ser identicados e tratados assim que ocorrerem, pois levam a uma redução no valor associado ao vértice (i.e., um aumento na sua prioridade). É simples fazer isso. Em cada iteração, os únicos vértices que têm sua distância à base alterada podem ser identicados durante o algoritmo de Dijkstra executado no passo 3. Basta percorrer a vizinhança desses vértices para atualizar tanto os valores associados a eles próprios (caso (a.)) quanto os associados aos seus vizinhos (caso (b.)). O mesmo deve ser feito também com o vértice removido do heap no passo 1 do algoritmo. Complexidade Em cada iteração do algoritmo, é necessário atualizar o diagrama de Voronoi (passo 3), o que requer O(jE j + jV j log jV j) operações no pior caso. Os passos 1 e 4 podem ser executados em tempo O(jE j + jV j log jV j), pois: no máximo jV j atualizações são feitas, cada uma em tempo O(log jV j); como a vizinhança de cada vértice é analisada no máximo uma vez no passo 1 e outra no passo 4, o tempo total necessário para analisar todas as vizinhanças é O(jE j). Todas as execuções do passo 2 podem ser feitas em tempo O(jV j). Assim, a implementação Kruskal-V requer O(jT j(jE j + jV j log jV j)) operações no pior caso. 3.5 Multispan A última heurística apresentada, Multispan, tende a fornecer soluções de qualidade bem inferior às anteriores. No entanto, pelo fato de se basear em um princípio diferente do das demais heurísticas, ela pode ser útil em situações em que é necessário gerar soluções com diversidade (é o caso da metaeurística apresentada no Capítulo 5). Inicialmente, determina-se a árvore geradora mínima do grafo e faz-se a poda (pruning ) de todas as folhas que não são terminais (o processo é recursivo: se novas folhas nãoterminais surgirem, também serão podadas). Se pelo menos um vértice for eliminado, calcula-se a árvore geradora mínima do subgrafo induzido pelos vértices restantes. O procedimento se repete até que não seja possível fazer a poda da solução obtida. Cada árvore geradora mínima pode ser determinada em tempo O(jE j + jV j log jV j) utilizando o algoritmo de Prim. No pior caso, o algoritmo terá O(jV j jT j) iterações, o que signica que sua complexidade assintótica é O(jV j(jE j + jV j log jV j)) (pois O(jV j jT j) = O(jV j)). Na prática, contudo, o número de iterações é tipicamente muito pequeno. Normalmente, o número de folhas podadas é grande em algumas poucas iterações iniciais. CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 30 O número de folhas podadas logo se reduz, o que quase sempre leva ao m do algoritmo, pois a árvore encontrada em uma iteração tende a ser muito semelhante à obtida na iteração anterior (após a poda). Uma implementação assintoticamente mais eciente do algoritmo (no pior caso) utiliza o algoritmo de Kruskal para o cálculo da árvore geradora mínima (veja Seção 2.3.1). Nesse caso, basta ordenar as arestas uma única vez, em tempo O(jE j log jE j). Feito isso, o cálculo de cada árvore geradora mínima pode ser feito em tempo O(jE j(jE j; jV j)). Como cada solução intermediária pode ser podada em tempo O(jV j), são necessárias O(jE j log jE j + jV jjE j(jE j; jV j)) = O(jV jjE j(jE j; jV j)) operações no pior caso para executar todo o algoritmo. Observações empíricas mostraram que a implementação baseada no algoritmo de Prim tende a garantir soluções de melhor qualidade, desde que o algoritmo seja executado a partir de uma raiz diferente em cada iteração. Isso aumenta a probabilidade de que se criem novas folhas que são não-terminais e, portanto, podem ser podadas. A implementação baseada no algoritmo de Prim foi a única testada na Seção 3.6. 3.6 Análise Comparativa 3.6.1 Qualidade das Soluções Nesta seção, comparam-se as diversas heurísticas propostas quanto à qualidade das soluções fornecidas. Para algumas das heurísticas, foram propostas diferentes implementações, que, em princípio, podem levar a soluções distintas, já que casos de empate pode ser resolvidos de forma distinta. Observou-se experimentalmente que isso de fato ocorre, mas as diferenças são muito pequenas e não indicam a superioridade de uma implementação em relação às demais. Assim sendo, os resultados apresentados nesta seção referem-se a uma única implementação de cada heurística: DNH: implementação que utiliza o algoritmo de Bor·vka (DNH-Bor·vka); Kruskal: implementação baseada no heap de arestas (Kruskal-E); Prim: implementação direta (Prim-D); DNHz, Bor·vka e Multispan: utilizou-se a única implementação proposta em cada caso. Para avaliar a qualidade das soluções fornecidas pelas heurísticas propostas, foram testadas todas as instâncias das quatro classes apresentadas na Seção 2.4.2: Incidência, OR-Library, PUC e VLSI. A Tabela 3.1 apresenta, para cada uma dessas classes, três medidas para a qualidade relativa das heurísticas (essas medidas são descritas, por exemplo, em [24]): CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 31 Desvio relativo percentual médio (dr%): Dada uma instância, dene-se desvio relativo percentual de um método M como a diferença percentual entre o valor da solução obtida por M e o valor da melhor solução encontrada para a instância, considerando-se todos métodos. O desvio relativo percentual médio é a média dos desvios percentuais obtidos pelo método para todas as instâncias do grupo. Rank médio (rank): Para cada instância, os métodos são ordenados de acordo com a qualidade das soluções fornecidas. O melhor método tem rank 1, o segundo melhor tem rank 2 e assim sucessivamente. Em caso de empate, atribui-se a todos os métodos envolvidos o mesmo rank, calculado como a média dos ranks individuais.3 O rank médio de um método é a média dos ranks obtidos para todas as instâncias do grupo. melhor: Número de instâncias para as quais o método obteve o melhor valor entre todos os métodos, incluindo os casos de empate no primeiro lugar. A comparação entre os métodos também pode ser feita de forma absoluta. Para cada série de instâncias, a Tabela 3.2 mostra o desvio percentual médio das soluções fornecidas pelas heurísticas em relação às melhores soluções primais conhecidas. Em geral, os valores utilizados para comparação são comprovadamente ótimos. Em alguns casos, porém, utilizam-se os valores obtidos heuristicamente, conforme mencionado na Seção 2.4.2. Nas séries em que é esse o caso (i640, bip, cc e hc, todas marcadas com um asterisco na tabela), os valores apresentados são limites superiores para o desvio percentual em relação ao ótimo. Tanto na Tabela 3.1 quanto na Tabela 3.2, os valores em negrito destacam o melhor resultado obtido para cada grupo de instâncias. Analisando-se as tabelas, merece destaque a (baixa) qualidade das soluções fornecidas pelo algoritmo Multispan. Apenas para a classe PUC o método tem desempenho relativo competitivo com alguns dos demais métodos. Não por acaso, as instâncias PUC são aquelas em que o número de terminais é maior (em relação ao número de vértices), principalmente na série hc. Em instâncias com poucos terminais, o desempenho da heurística Multispan tende a ser pior, pois, por ter uma característica global, ela normalmente encontra soluções com um número de vértices maior que o utilizado por outros métodos. Quando se comparam as demais heurísticas entre si, observa-se que a qualidade das soluções fornecidas depende claramente da capacidade de cada método encontrar atalhos nas soluções. Utilizar vértices não-terminais já inseridos na solução para efetuar ligações em fases posteriores do algoritmo é essencial para garantir melhores resultados. Nas heurísticas de Kruskal e Prim, as informações de proximidade entre as componentes do grafo são atualizadas logo após a adição de cada caminho à solução. Com Exemplo: se os valores forem (1200, 1203, 1203, 1205, 1206, 1207), os respectivos ranks serão (1, 2.5, 2.5, 4, 5, 6). 3 CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS classe algoritmo dr% rank melhor DNH 8.201 3.660 127 DNHz 4.686 2.888 142 Incidência Bor·vka 3.776 2.441 179 (397 instâncias) Kruskal 1.369 2.317 202 Prim 0.658 1.883 272 Multispan 69.194 5.987 0 DNH 4.697 4.400 4 DNHz 2.356 2.658 11 OR-Library Bor·vka 1.989 2.092 27 (60 instâncias) Kruskal 0.730 2.333 26 Prim 0.889 2.642 12 Multispan 51.679 5.833 1 DNH 28.125 5.120 0 DNHz 20.781 4.050 0 PUC Bor·vka 19.445 3.480 2 (50 instâncias) Kruskal 2.376 1.740 17 Prim 0.438 1.310 33 Multispan 29.494 4.070 0 DNH 3.556 3.599 6 DNHz 3.173 3.224 7 VLSI Bor·vka 1.647 2.250 21 (116 instâncias) Kruskal 0.553 1.573 58 Prim 0.726 1.586 50 Multispan 65.133 6.000 0 DNH 8.597 3.837 137 DNHz 5.472 3.022 160 Total Bor·vka 4.465 2.455 229 (623 instâncias) Kruskal 1.236 2.134 303 Prim 0.675 1.855 367 Multispan 63.565 5.821 1 Tabela 3.1: Qualidade relativa das diversas heurísticas estudadas 32 CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 33 série desvio percentual médio DNH DNHz Bor·vka Kruskal Prim Multispan i080 24.45 20.76 20.26 18.14 17.23 73.77 i160 26.93 23.29 22.35 20.42 19.32 91.27 i320 29.45 25.57 24.38 21.34 20.74 110.38 i640 30.73 26.41 25.10 22.14 21.41 125.18 c 6.51 4.18 4.06 3.27 3.10 43.77 d 7.04 5.16 4.91 3.71 3.69 58.16 e 9.80 6.69 5.93 4.09 4.70 67.34 bip 56.66 51.41 51.41 14.89 15.01 32.43 cc 38.53 26.02 23.30 11.02 8.67 55.40 hc 30.34 28.36 28.36 10.37 7.18 20.91 alue 5.13 4.57 3.25 2.77 2.67 78.94 alut 6.14 5.85 3.64 3.20 3.91 92.94 diw 5.65 5.43 3.27 1.89 2.54 79.32 dmxa 6.81 6.80 5.14 3.14 3.64 56.79 gap 6.29 5.12 4.03 2.82 3.53 56.23 msm 5.41 5.12 4.00 3.03 2.65 61.99 taq 6.77 6.37 4.52 3.33 3.10 66.14 Tabela 3.2: Desvios percentuais médios em relação às melhores soluções primais conhecidas isso, podem-se levar em conta os vértices adicionados em um passo para escolher da melhor maneira possível o caminho a ser adicionado no passo seguinte. Já no algoritmo de Bor·vka, vários caminhos são adicionados na mesma iteração, mas as informações de proximidade entre as componentes são atualizadas apenas entre uma iteração e outra. Na heurística DNHz, a atualização também se faz entre iterações, mas de forma muito mais limitada. O método DNH não faz qualquer atualização ao longo da execução: as informações fornecidas pelo diagrama de Voronoi construído no início do algoritmo são as únicas utilizadas até o m da execução. A qualidade média das soluções fornecidas pelos diversos métodos reete esses fatos. As heurísticas de Prim e Kruskal são as que apresentam os melhores resultados na média, seguidos pelas heurísticas de Bor·vka, DNHz e, nalmente, DNH. Em termos absolutos, a qualidade das soluções fornecidas pelas heurísticas implementadas depende fortemente do tipo de instância testada, conforme deixa claro a Tabela 3.2. Para as instâncias de Incidência, as soluções fornecidas pelas melhores heurísticas estão em média a 20% do ótimo. Para a classe VLSI, os resultados são expressivamente melhores: a diferença média entre os valores obtidos pelas heurísticas e os valores ótimos é de cerca de 3% apenas. Essas qualidades podem ou não ser sucientes; tudo depende da aplicação às quais se destinam as soluções obtidas. Os Capítulos 4 e 5 apresentam CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 34 métodos que podem ser utilizados para encontrar soluções de melhor qualidade se isso for necessário. 3.6.2 Tempos de Execução A qualidade das soluções fornecidas pelas heurísticas construtivas é muito importante, mas é apenas um aspecto a ser levado em conta na sua análise. Outra característica a ser considerada é o tempo de execução, discutido nesta seção. Uma primeira estimativa para o tempo de execução de um algoritmo é sua complexidade de pior caso. A Tabela 3.3 resume as expressões obtidas para as complexidades das implementações descritas ao longo deste capítulo. A primeira coluna apresenta a complexidade da implementação ideal, enquanto a segunda mostra a complexidade de pior caso da implementação de fato realizada. Tipicamente, a diferença se deve ao uso de heaps binários no lugar de heaps de Fibonacci (veja as seções anteriores neste capítulo para uma discussão detalhada de cada caso). algoritmo pior caso teórico implementação DNH-Prim O(jE j + jV j log jV j) O(jE j log jV j) DNH-Bor·vka O(jE j log jT j + jV j log jV j) O(jE j log jV j) DNHz O(jE j log jT j + jV j log jV j) O(jE j log jV j) Bor·vka O((log jT j)(jE j + jV j log jV j)) O(jE j log jT j log jV j) Kruskal-B O(jT j(jE j + jV j log jV j)) O(jT jjE j log jV j) Kruskal-E O(jT j(jE j + jV j log jV j)) O(jT jjE j log jE j) O(jT jjE j log jV j) Kruskal-V O(jT j(jE j + jV j log jV j)) Prim-T O(jT j(jE j + jV j log jV j)) O(jT jjE j log jV j) O(jT j(jE j + jV j log jV j)) O(jT jjE j log jV j) Prim-D Multispan O(jV jjE j(jE j; jV j)) O(jV jjE j log jV j) Tabela 3.3: Complexidade dos algoritmos construtivos estudados Considerando-se as implementações de fato, a tabela revela quatro categorias de algoritmos, determinadas pelos comportamentos assintóticos. Os três primeiros (DNHPrim, DNH-Bor·vka e DNHz) têm complexidade O(jE j log jV j). Na segunda categoria está apenas o algoritmo de Bor·vka, com pior caso O(jE j log jT j log jV j). A terceira categoria contém os métodos Kruskal e Prim, que têm complexidade O(jT jjE j log jV j) (o fator log jE j na complexidade do algorimto Kruskal-E é assintoticamente equivalente a log jV j para grafos simples). Na quarta categoria está o algoritmo Multispan. A diferença básica entre os algoritmos, portanto, é sua dependência em relação ao número de terminais do grafo: nula (primeira categoria), logarítmica (segunda) ou linear (terceira). (O algoritmo Multispan pode ser considerado um caso à parte. A expressão de pior caso desse algoritmo é na verdade O((jV j jT j)jE j(jE j; jV j)).) CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 35 Essa dependência, no entanto, refere-se aos desempenhos de pior caso dos algoritmos. Conforme já se mencionou ao longo da descrição de cada um dos algoritmos, as implementações sugeridas têm justamente o objetivo de serem ecientes na prática. Para testar essa eciência, cada implementação foi executada sobre todas as instâncias mencionadas na Seção 2.4.2. A única exceção foi o método Prim-T: como ele exige a alocação O(jT jjV j) posições de memória, não foi possível executá-lo sobre três das instâncias: alue7065, alue7080 e alut2625. Os valores apresentados para esse algoritmo não incluem essas instâncias. Uma primeira medida da eciência relativa dos diversos métodos é fornecida na Tabela 3.4. Para cada série, apresentam-se os tempos médios necessários para a execução de cada algoritmo em um AMD K6-2 com clock de 128 MHz. O símbolo identica as situações em que nem todas as execuções puderam ser realizadas. série i080 i160 i320 i640 c d e bip cc hc alue alut diw dmxa gap msm taq tempo (milésimos de segundo) Bvk Kr-B Kr-E Kr-V Pr-T Pr-D Mltspn 0.8 0.9 15.0 1.2 1.2 4.2 1.2 1.3 3.4 3.4 3.6 102.3 5.2 4.0 25.8 4.7 5.2 15.7 15.6 16.2 719.5 25.3 15.9 179.7 21.7 24.5 80.5 80.7 82.2 6234.5 116.1 64.1 1447.6 101.4 124.2 6.2 6.2 6.6 142.4 11.2 9.0 144.4 4.8 16.1 14.7 14.2 15.3 459.0 28.8 22.8 632.7 11.7 42.5 49.6 48.8 55.4 3208.3 94.2 75.0 4485.5 40.2 156.3 14.9 16.0 16.7 1403.2 19.0 21.8 106.9 16.0 44.6 17.2 17.0 21.1 1456.1 36.0 28.2 521.2 25.7 41.5 14.2 14.3 15.2 8628.2 22.1 26.7 512.1 15.9 103.6 58.7 56.4 76.7 4058.1 73.0 95.2 104.1 45.0 218.0 92.2 92.4 126.1 2825.5 115.2 143.2 320.6 64.7 529.7 16.9 17.1 24.7 164.5 24.4 29.8 78.4 15.9 90.7 3.9 3.9 5.7 44.8 5.6 7.2 15.9 4.0 15.5 6.5 6.1 8.8 65.7 8.5 10.6 36.6 5.8 24.4 5.7 5.7 8.3 63.7 8.1 10.0 24.0 5.8 20.7 11.0 11.0 14.7 144.1 15.0 18.5 56.0 9.4 49.7 Tempos médios de execução (absolutos) dos métodos construtivos D-P D-B 0.9 0.8 3.4 15.2 74.4 6.9 15.7 49.6 20.0 18.1 15.7 61.0 90.1 20.2 5.0 7.6 7.4 12.5 Tabela 3.4: Dz Essa forma de comparação tem uma deciência intrínseca. Como cada série é bastante heterogênea (com instâncias de tamanhos e características diferentes), os tempos obtidos por uma mesma implementação podem ser muito distintos dentro da mesma série. Assim sendo, quando se considera o tempo médio de execução, dá-se um peso maior às instâncias mais lentas, o que pode introduzir distorções. Para corrigir essa deciência, é conveniente utilizar também o conceito de tempo relativo . Para cada instância, considera-se não o valor absoluto do tempo de execução, mas a razão entre esse tempo e o de um algoritmo CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 36 tomado como referência. Escolheu-se como referência a heurística DNH-Prim, a única cujo desempenho é completamente independente do número de terminais (os métodos DNH-Bor·vka e DNHz dependem de jT j, ainda que não assintoticamente). Essa escolha permite avaliar o que mostra a Tabela 3.3, que a diferença básica entre as complexidades dos algoritmos implementados está justamente na dependência em relação ao de número de terminais. A Tabela 3.5 apresenta, para cada classe, a média dos tempos relativos obtidos pelos diversos métodos (considerando todas as instâncias testadas de cada classe). algoritmo tempo relativo médio Incidência OR-Library PUC VLSI Total DNH-Prim 1.000 1.000 1.000 1.000 1.000 DNH-Bor·vka 0.847 0.837 0.849 0.779 0.829 DNHz 0.834 0.829 0.861 0.779 0.825 Bor·vka 0.978 0.967 0.972 1.068 0.993 Kruskal-B 25.736 42.668 101.642 8.898 30.324 Kruskal-E 1.518 1.561 1.482 1.071 1.437 Kruskal-V 1.583 1.640 1.469 1.373 1.541 Prim-T 9.664 34.368 15.096 3.367 11.345 Prim-D 1.400 0.851 1.162 0.761 1.210 Multispan 1.458 2.248 2.706 2.965 1.913 Tabela 3.5: Tempos médios de execução das heurísticas construtivas em relação a DNHPrim As tabelas sugerem que, no caso das instâncias testadas (que formam um conjunto bastante amplo e heterogêneo), o desempenho das heurísticas é na média muito mais equilibrado do que sugerem as complexidades apresentadas na Tabela 3.3, exceção feita às implementações Prim-T e Kruskal-B. Em relação aos tempos médios, observa-se que nenhuma das heurísticas (exceto as duas já mencionadas) é duas vezes mais rápida ou mais lenta que o método de referência. Para uma melhor análise desse fenômeno, é interessante observar também a Tabela 3.6. Ela mostra, para cada classe de instâncias, os piores tempos relativos obtidos pelas heurísticas. Ou seja, considerando-se todas as execuções individuais, seleciona-se aquela cuja razão entre o tempo de execução da heurística em estudo e o da heurística de referência (DNH-Prim) é maior. Com isso, espera-se tornar mais evidente o pior caso de cada algoritmo. A tabela mostra que a implementação Kruskal-B pode ser até três ordens de grandeza mais lenta que a implementação de referência. Tanto no pior caso quanto na média, esse método é muito mais lento que todos os demais. Também o método Prim-T pode ter tempos de execução ordens de grandeza maiores que os de algoritmos cuja complexidade independe de jT j. No pior caso, esse método foi CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 37 algoritmo pior tempo relativo Incidência OR-Library PUC VLSI Total DNH-Prim 1.000 1.000 1.000 1.000 1.000 DNH-Bor·vka 1.509 1.526 1.192 1.151 1.526 DNHz 1.509 1.478 1.233 1.164 1.509 Bor·vka 1.509 1.672 1.518 1.579 1.672 Kruskal-B 195.536 274.003 734.123 146.132 734.123 Kruskal-E 3.197 2.375 3.111 1.388 3.197 Kruskal-V 2.704 2.435 2.012 1.772 2.704 Prim-T 53.828 257.691 44.588 6.890 257.691 Prim-D 2.107 1.119 1.781 1.063 2.107 Multispan 2.857 7.421 8.567 7.490 8.567 Tabela 3.6: Piores tempos de execução dos métodos construtivos em relação a DNH-Prim mais de 250 vezes mais lento que o algoritmo DNH-Prim. Esse valor se refere à instância e20, que, não por acaso, possui um grande número de terminais: 1000. Nessa instância (um grafo aleatório com distribuição uniforme dos pesos), o termo jT j na expressão da complexidade da implementação Prim-T se manifesta claramente. Ainda de acordo com a tabela, também as classes de Incidência e PUC possuem instâncias em que o método Prim-T é signicativamente pior que os demais. Para as instâncias VLSI, o desempenho do algoritmo é melhor, graças à aceleração proposta por Polzin e Daneshmand descrita na Seção 3.3.1. A estrutura rígida das instâncias VLSI faz com que o tempo de cálculo dos caminhos mínimos na primeira fase do algoritmo seja de fato reduzido. Leve-se em conta, no entanto, que os dados apresentados não incluem justamente as três maiores instâncias da série, que não puderam ser avaliadas devido a restrições de memória (o que, aliás, é um dos grandes problemas do método Prim-T). Entre os métodos que utilizam uma quantidade linear de memória adicional, o método mais lento é Multispan, justamente o que fornece soluções de pior qualidade. Deve-se considerar, no entanto, que a implementação testada não foi a mais eciente. Conforme mencionado na Seção 3.5, utilizou-se o algoritmo de Prim como sub-rotina, sendo o algoritmo de Kruskal uma alternativa melhor. Independentemente disso, as qualidades das soluções fornecidas pela heurística são comparativamente tão ruins que não se justicam maiores investimentos em sua implementação. Para as instâncias testadas, os métodos com menores tempos de execução na média são DNH-Bor·vka e DNHz. A comparação dos métodos entre si revela que praticamente não há distinção entre eles. Isso não é uma surpresa, uma vez que DNHz é apenas uma ligeira variação de DNH-Bor·vka; as etapas básicas dos algoritmos são as mesmas. Ambos são normalmente mais rápidas que o método DNH-Prim. Conforme já mencionado, o uso do algoritmo de Bor·vka para calcular a árvore de Steiner na segunda fase desses algoritmos CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 38 é vantajoso porque explora o fato de que o número de terminais freqüentemente é muito inferior ao de vértices. A Tabela 3.6 mostra que, mesmo nos casos em que o número de terminais é grande, o desempenho desses métodos não é signicativamente inferior ao do método DNH-Prim (é no máximo cerca de 50% pior). A heurística de Bor·vka para o problema de Steiner em grafos surge como uma boa alternativa à heurística DNH. Além de fornecer soluções de qualidade superior, conforme mostrou a Seção 3.6.1, ela o faz sem requerer signicativos aumentos no tempo de execução. De fato, para as instâncias consideradas essa heurística é em média até mais rápida que a implementação da heurística DNH usando o algoritmo de Prim. Evidentemente, é mais lenta que os algoritmos DNH-Bor·vka e DNHz, até porque heurística de Bor·vka é uma extensão (com iterações adicionais) desses métodos. Conforme esperado, as duas heurísticas de melhor qualidade, Kruskal e Prim-D apresentam tempos de execução piores que os dos métodos DNH e Bor·vka. Observe, no entanto, que os tempos são perfeitamente aceitáveis. Considerando todas as 623 execuções, a heurística de Kruskal foi no máximo 3.2 vezes mais lenta que a heurística de referência; a heurística Prim-D mostrou-se ainda mais estável, sendo não mais que 2.1 vezes mais lenta que DNH-Prim. Isso mostra que as novas implementações sugeridas são muito robustas. Apesar de haver um fator jT j nas complexidades de pior caso dessas heurísticas (veja Tabela 3.3), ele na prática não se manifestou: os tempos de execução mantiveram-se sempre na mesma ordem de grandeza da heurística DNH-Prim, cujo desempenho independe do número de terminais. Lembre-se de que muitas das instâncias testadas têm centenas, algumas até milhares, de terminais. É interessante observar que o algoritmo de Prim é especialmente eciente para as instâncias VLSI. A estrutura rígida dessas instâncias (planares e com uma métrica bem denida) faz com que o algoritmo se comporte essencialmente como o algoritmo de Dijkstra. O número de vezes em que a vizinhança de um vértice deve ser percorrida é relativamente pequeno. Com isso, o algoritmo se torna, na média, até mais rápido as diversas implementações da heurística DNH. As duas novas implementações propostas para a heurística de Kruskal, apesar de muito diferentes, têm desempenhos muito próximos na prática. A implementação baseada em arestas tem a vantagem de ser mais simples e, nesse teste, mais rápida, ainda que não muito (a diferença de desempenho só é mais expressiva no caso das instâncias VLSI, justamente as mais esparsas, em que jV j = O(jE j)). A implementação baseada em nós tem a vantagem de requerer menor quantidade de memória adicional: apenas O(jV j), contra O(jE j) da heurística baseada em arestas. Como regra geral, pode-se dizer que a implementação baseada em heap de nós é mais interessante para instâncias densas; para as demais, a baseada em arestas parece ser mais interessante. Comportamento Assintótico As Tabelas 3.5 e 3.6 indicam que, para as instâncias testadas, as novas implementações propostas para as tradicionais heurísticas de Kruskal e Prim têm desempenho praticamente independente de jT j para as instâncias testadas. CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 39 Apresentam-se a seguir experimentos especícos para avaliar esse fato. Utilizou-se um gerador de grafos aleatórios para construir uma nova série de instâncias que mostrasse de forma clara a dependência dos algoritmos em relação a jT j. Todos os grafos da série têm os mesmos números de vértices e arestas; varia apenas o número de terminais. Escolheu-se arbitrariamente jVqj = 1024 e jE j = 32768, o que signica a série tem densidade intermediária (jE j = jV j jV j). O número p de terminais varia entre jT j = 2 e jT j = 1024, numa progressão geométrica de razão 2 (valores não inteiros são arredondados para cima). Para cada valor de jT j foram criados 20 grafos, com diferentes sementes para o gerador de números aleatórios. Os pesos das arestas foram aleatoriamente escolhidos com distribuição uniforme no intervalo [1; 10], como nas instâncias da ORLibrary. Repare que os limites inferior e superior para jT j representam dois importantes casos especiais polinomiais do problema de Steiner: o problema do caminho mínimo entre dois vértices (jT j = 2) e o problema da árvore geradora de peso mínimo (T = V ). A inclusão desses valores na série tem o único objetivo de permitir uma melhor compreensão do comportamento dos algoritmos; evidentemente, não se pretende que qualquer dos métodos aqui propostos seja utilizado para resolver esses casos especiais. A Figura 3.1 mostra o tempo de execução de cada um dos algoritmos implementados em função do número de terminais. Cada ponto do gráco representa a média dos tempos de execução obtidos para todas as 20 instâncias que possuem a mesma quantidade de terminais. Na escala em que foi apresentado, o gráco só permite uma análise detalhada dos tempos de execução das implementações Kruskal-B e Prim-T, que, para essa série de instâncias, dependem de jT j de forma muito mais signicativa que os demais métodos. Para a comparação dos outros métodos entre si, deve-se utilizar a Figura 3.2, que apresenta os mesmos dados em uma escala mais adequada. Para analisar os algoritmos, é importante, antes de mais nada, compreender o signicado de cada curva observada. Uma reta horizontal indica que o tempo de execução do algoritmo independe do número de terminais. Uma reta inclinada indica que o tempo de execução do método é proporcional a log jT j, já que os grácos têm escala logarítmica. Uma curva com inclinação crescente indica que uma dependência superlogarítmica em relação a jT j. Em particular, se a curva for exponencial, a complexidade do algoritmo será proporcional a jT j. Apenas três das implementações têm partes relevantes de suas curvas com inclinação crescente: Kruskal-B, Prim-T e Multispan. A curva que representa Kruskal-B é denitivamente uma exponencial: pode-se perceber claramente que o algoritmo tem tempo de execução proporcional a jT j. Já a curva que representa Prim-T é uma exponencial apenas para valores de jT j até aproximadamente 256. Para valores mais altos, a taxa de crescimento do tempo de execução diminui e passa nalmente a ser negativa. Isso é conseqüência dos métodos de aceleração propostos por Polzin e Daneshmand descritos CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 40 20.00 Kruskal-B Prim-T Kruskal-V Kruskal-E Multispan Prim-D Boruvka DNH-Prim DNH-Boruvka DNHz 18.00 16.00 14.00 tempo (s) 12.00 10.00 8.00 6.00 4.00 2.00 0.00 2 4 8 16 32 64 terminais (escala log) 128 256 512 1024 Figura 3.1: Tempos de execução em função do número de terminais 0.20 tempo (s) Kruskal-B Prim-T Kruskal-V Kruskal-A Multispan Prim-D Boruvka DNH-Prim 0.15 DNH-Boruvka DNHz 0.10 0.05 0.00 2 4 8 16 32 64 terminais (escala log) 128 256 512 1024 Figura 3.2: Tempos de execução em função do número de terminais (métodos mais rápidos) CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 41 na Seção 3.3.1. Mantida a densidade do grafo xa, a ecácia desses métodos aumenta sensivelmente quando o número de terminais é muito grande. De acordo com a discussão apresentada na Seção 3.5, o número de iterações do algoritmo Multispan é proporcional a jV j jT j no pior caso. No entanto, a curva que representa esse algoritmo na Figura 3.2 mostra um crescimento superlinear (ainda que mais suave que os de Prim-T e Kruskal) do tempo de execução em relação a jT j para praticamente todos os valores de jT j. Isso mostra que, para essa classe de instâncias, o pior caso pouco revela sobre o comportamento do algoritmo. Apenas no caso limite, em que jV j = jT j, a expressão de pior caso se revela claramente: apenas uma iteração é executada, já que o algoritmo reduz-se ao algoritmo de Prim para o problema da árvore geradora mínima. Nesse caso extremo, Multispan é tão rápido quanto os demais métodos. Conforme esperado, apenas um método, DNH-Prim, tem desempenho completamente independente do número de terminais no grafo. A curva que o representa é perfeitamente horizontal. Consultando a Tabela 3.3, observa-se que também os métodos DNH-Bor·vka e DNHz têm expressões de pior caso independentes de jT j: O(jE j log jV j). Na verdade, no entanto, essa expressão esconde um termo aditivo jE j log jT j. É a primeira fase dos algoritmos (a construção do diagrama de Voronoi) que é executada em tempo O(jE j log jV j); a segunda fase (construção da árvore de Steiner) tem complexidade O(jE j log jT j). Isso explica por que as curvas na Figura 3.2 não são horizontais. Observe que, para essa classe de instâncias, a implementação da heurística DNH usando o algoritmo de Prim é mais vantajosa que a que utiliza o de Bor·vka para valores de jT j até aproximadamente 64. A partir daí, a superioridade é do algoritmo DNH-Prim, ainda que por uma pequena margem (cerca de 20% no pior caso). Para essa classe de instâncias, a heurística de Bor·vka para o problema de Steiner em grafos apresentou desempenho muito semelhante ao das implementações DNH-Prim e DNHz. Lembre-se de que a diferença principal entre o método de Bor·vka e esses dois algoritmos é o fato de que, na segunda fase, é necessário recalcular o diagrama de Vonoroi após cada uma das O(log jT j) iterações. A Figura 3.2 mostra que esse custo adicional é relevante apenas quando o número de terminais é pequeno. Quando o número é maior, as alterações no diagrama de Voronoi tendem a ser mais locais e, conseqüentemente, mais rápidas. As duas implementações mais rápidas do algoritmo de Kruskal (Kruskal-V e KruskalE) mostraram comportamento distinto para as instâncias testadas. A implementação baseada em um heap de vértices (Kruskal-V) é mais rápida, especialmente para valores pequenos de jT j. Para instâncias com poucos terminais, os caminhos escolhidos tendem a ter um grande número de arestas, o que se traduz em modicações relevantes no diagrama de Voronoi após cada iteração. Na implementação Kruskal-V, a alteração das propriedades de um vértice v no diagrama de Voronoi se traduz em uma única operação de atualização do heap. Na implementação Kruskal-E, diversas atualizações são necessárias, uma para cada vértice de v. Isso explica por que o tempo de execução de Kruskal-E é maior. Repare que, a partir de um certo ponto (jT j = 11, aproximadamente), um aumento no número de CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 42 terminais se traduz em uma queda no tempo de execução desse algoritmo; ela é motivada pelo fato de que as alterações do diagrama de Voronoi entre iterações tornam-se cada vez mais limitadas. Repare que as diferenças entre as novas implementações (Kruskal-V e Kruskal-E) são pequenas quando se considera a implementação básica (Kruskal-B), que é ordens de grandeza mais lenta. Nesse caso, está claro que a troca de uma implementação simples por outra mais elaborada é extremamente vantajosa, mesmo não havendo ganhos assintóticos no pior caso. O método que consistentemente obteve os menores tempo de execução foi a implementação direta do algoritmo de Prim, signicativamente mais rápida que as demais. Para essa classe de instâncias, o tempo de execução do método Prim-D é praticamente independente do número de terminais. Seus tempos de execução são em média de 30% a 40% menores que os do algoritmo DNH-Prim, cuja complexidade de pior caso de fato independe do valor de jT j. Há duas razões que permitem que Prim-D seja tão mais rápido que as demais implementações para algumas instâncias (como as dessa série): Entre os algoritmos estudados, ele é o único capaz de encontrar uma solução sem sequer percorrer todas as arestas do grafo. Isso explica por que o algoritmo é o mais rápido para o caso jT j = 2: ele simplesmente se reduz ao algoritmo de Dijkstra. O algoritmo Prim-D faz uma única passagem pelo grafo. Todas as demais implementações (com exceção das lentas Multispan, Kruskal-B e Prim-T) se iniciam com a construção do diagrama de Voronoi. É necessária pelo menos mais uma passagem pelo grafo para determinar a árvore propriamente dita. É claro que, na sua única passagem, Prim-D pode precisar percorrer a vizinhança de um mesmo vértice diversas vezes: O(jT j) no pior caso. Claramente não é isso que ocorre para as instâncias aqui testadas. Como conclusão da análise das Figuras 3.1 e 3.2, é importante reiterar que ela tem m apenas ilustrativo. Trata-se de uma classe de instâncias muito especíca: grafos aleatórios com densidade xa, distribuição aleatória dos pesos das arestas, terminais aleatoriamente escolhidos. Variações em quaisquer desses fatores podem se traduzir em curvas signicativamente diferentes. Algoritmos que se mostraram especialmente interessantes para essa classe podem não sê-lo para outras. A análise de um caso especíco é útil para melhor compreender alguns aspectos do funcionamento de cada algoritmo. Não se deve tirar dela qualquer conclusão denitiva sobre a qualidade relativa dos métodos. 3.7 Conclusão Este capítulo apresentou apenas algumas dentre as diversas heurísticas construtivas existentes para o problema de Steiner. O ponto em comum entre todas as heurísticas é o fato CAPÍTULO 3. HEURÍSTICAS CONSTRUTIVAS 43 de se basearem em algoritmos para o problema da árvore geradora de custo mínimo. É muito simples, portanto, explicar em linhas gerais o funcionamento de cada uma delas, embora nem sempre seja esse o caso quando se trata da implementação. Entre as contribuições do capítulo estão duas novas heurísticas muito simples, DNHz e Bor·vka, ambas extensões da implementação de Melhorn para a heurística DNH. Sem aumentar de forma relevante os tempos de execução, ambas apresentam resultados em geral melhores que os do método original, pois buscam ativamente encontrar atalhos que levem a caminhos mais curtos. Ou seja: enquanto a heurística original busca ligar sempre dois terminais, as variantes tentam, em menor ou maior grau, utilizar vértices nãoterminais previamente inseridos como âncoras para a ligação de diferentes componentes. De fato, buscar atalhos parece ser a chave para garantir a qualidade das soluções fornecidas. Os algoritmos que fazem isso de maneira mais cuidadosa, as heurísticas de Prim e Kruskal, são os que levam aos melhores resultados. Depois da inserção de cada novo caminho à solução, esses métodos reavaliam as informações de proximidade para identicar novos atalhos criados. A principal contribuição deste capítulo talvez seja ter mostrado experimentalmente que é possível fazer com que esses métodos sejam tão rápidos quanto os métodos anteriores, apesar das expressões de suas complexidades sugerirem o contrário. Na prática, o que se observou é que, para as instâncias da literatura, é possível executar os algoritmos de Prim e Kruskal em tempos da mesma ordem de grandeza que a heurística DNH, que é assintoticamente muito mais eciente no pior caso. Mais que isso, em muitos casos os algoritmos mais elaborados foram até mais rápidos que o método DNH. Isso foi conseguido graças às novas implementações das heurísticas de Prim e Kruskal aqui apresentadas. Todas seguem o mesmo princípio: evitar o retrabalho. Ao longo de sua execução, os algoritmos mantêm estruturas auxiliares que lhes permitem identicar exatamente quais medidas anteriormente calculadas podem estar desatualizadas e quais não podem. Atuando só nos casos estritamente necessários, os ganhos em desempenho são signicativos. Note que, no pior caso, as novas implementações não têm complexidade melhor que as das normalmente utilizadas. Na prática, no entanto, o que se observou para as instâncias testadas foi um ganho assintótico: o fator jT j nas expressões de complexidade dos algoritmos simplesmente não se revelou. Portanto, das experiências relatadas nesse capítulo deriva-se um princípio com aplicações muito mais amplas que o problema de Steiner. Uma implementação mais elaborada de um algoritmo, mesmo quando não representa um ganho assintótico no pior caso, pode levar a signicativos ganhos de desempenho. Dentro dos limites do razoável, deve-se fazer o máximo para permitir ao o algoritmo ser melhor que seu pior caso, especialmente quando o pior caso é muito raro. Capítulo 4 Busca Local Apesar de as soluções encontradas por heurísticas construtivas terem qualidade média razoável, há situações em que é desejável obter soluções ainda melhores, mesmo que para isso seja necessário executar rotinas computacionalmente mais custosas. Nesta seção, serão discutidos diversos métodos de busca local, que têm exatamente esse m: melhorar a qualidade de soluções já existentes. Essencial para algoritmos de busca local é a noção de vizinhança . Uma vizinhança dene, para qualquer solução S , um conjunto de outras soluções com características muito próximas. Tipicamente, as soluções vizinhas de S podem ser obtidas por meio de pequenas modicações em S , sendo a natureza dessas modicações justamente o que caracteriza a vizinhança. Um método de busca local consiste simplesmente em percorrer a vizinhança de uma solução (a solução corrente ) em busca de outra que tenha menor valor. Se for encontrada uma tal solução, ela se torna a nova solução corrente e o algoritmo continua. Se uma determinada solução não tiver nenhuma solução vizinha de melhor qualidade, diz-se que ela é um ótimo local . A Seção 4.1 apresenta três formas de representação de uma solução para o problema de Steiner em grafos: por vértices de Steiner, por vértices-chaves e por caminhos-chaves. A Seção 4.2 mostra como cada uma dessas representações dene uma vizinhança. A implementação dos métodos de busca local em si é discutida na Seção 4.3. A Seção 4.4 apresenta uma análise comparativa experimental dos diferentes métodos de busca local, incluindo estratégias híbridas baseadas na composição de diferentes algoritmos. Por m, a Seção 4.5 comenta os resultados obtidos. Na literatura, encontram-se implementações anteriores de métodos de busca local baseados em nós de Steiner [40, 60] e em caminhos-chaves [59]. Métodos híbridos utilizando ambas as vizinhanças foram apresentados em [38] e [50], sendo essa última referência a base do Capítulo 5. A noção de vértices-chaves também não é nova [15, 16]. No entanto, não é de conhecimento do autor uma descrição prévia de um método formal de busca local baseado em vértices-chaves. A primeira versão da rotina descrita nesta dissertação foi apresentada em [43], artigo escrito em co-autoria com Marcus Poggi de Aragão, Celso 44 CAPÍTULO 4. BUSCA LOCAL 45 C. Ribeiro e Eduardo Uchoa. 4.1 Representação da Solução Uma solução S para o problema de Steiner em grafos é representada de forma completa pelos conjuntos de vértices (VS ) e arestas (ES ) que a compõem. Há, no entanto, outras formas de se representar uma solução. A partir de qualquer uma delas, os conjuntos VS e ES podem ser facilmente derivados. Entre essas representações, destacam-se: Arestas. Uma solução pode ser representada simplesmente por ES , seu conjunto de arestas. O conjunto de vértices (VS ), é formado pelas extremidades das arestas de ES . Vértices de Steiner. Os vértices não-terminais que pertencem a uma solução recebem a denominação de vértices de Steiner. Se forem conhecidos apenas os vértices de Steiner de uma solução S , basta adicionar a eles o conjunto de terminais (T ) para restaurar VS . A partir de VS , determina-se ES calculando-se uma árvore geradora mínima no subgrafo de G induzido por VS . Vértices-chaves. O conjunto KS de vértices-chaves (ou nós-chaves ) de uma solução S é formado por todos os não-terminais que têm grau maior que dois na solução. A partir desse conjunto pode-se recuperar a solução S . Basta para isso (1) calcular a árvore geradora mínima do subgrafo de D(G) (o grafo de distâncias, denido na Seção 3.1) induzido por KS [ T (o conjunto de nós cruciais ou vértices cruciais , seguindo a terminologia adotada em [16]) e (2) extrair de cada aresta dessa árvore geradora, que representa um caminho, as arestas correspondentes do grafo original. Caminhos-chaves. Caminhos-chaves são caminhos entre nós cruciais de um grafo. Qualquer solução S pode completamente caracterizada pelo conjunto de caminhoschaves a compõem. A partir desse conjunto, podem-se determinar os vértices (VS ) e arestas (ES ) de S de forma imediata: são os vértices e arestas que compõem os caminhos. Observe que não é necessário armazenar todos os vértices ou arestas de cada caminho-chave; basta que sejam conhecidas suas extremidades. O caminho completo é reconstruído como o caminho mínimo entre elas. A única das representações compactas propostas que permite a reconstrução exata da solução armazenada é a representação por arestas. Em todos os demais casos, o processo de compactação não é completamente reversível. A partir de um único conjunto de vértices de Steiner VS , podem-se obter diversos conjuntos de arestas distintos; basta, para isso, que a árvore geradora do subgrafo induzido por VS não seja única. Situações semelhantes ocorrem para o conjunto de vértices-chaves e para o de caminhos-chaves (quando representados apenas por suas extremidades). CAPÍTULO 4. BUSCA LOCAL 46 Na verdade, existe a possibilidade de que, após a compactação e a descompactação de uma solução S , seja recuperada uma solução de valor ainda menor que o original. Considere, por exemplo, a representação baseada em vértices de Steiner. Uma solução S = (VS ; ES ) é comprimida simplesmente ignorando-se ES . A partir de VS , constrói-se um conjunto ES0 calculando-se a árvore geradora mínima do subgrafo induzido por VS . O conjunto de arestas encontrado terá o menor custo possível, dada a restrição de que todos os vértices de VS devem ser utilizados. Contudo, pode ocorrer que o conjunto original de arestas ES não fosse minimal, ou seja, que ES não representasse as arestas de uma árvore geradora mínima de VS . Nesse caso, a solução S 0 recuperada seria ainda melhor que a armazenada. Para a maioria das aplicações, essa irreversibilidade do processo de compactação não representa um problema. O fato de a solução recuperada ser melhor que a original pode até ser uma vantagem. É esse o caso para todos os usos discutidos neste e nos próximos capítulos. 4.2 Vizinhanças A partir das representações introduzidas na seção anterior, podem-se denir vizinhanças para o problema de Steiner em grafos. A vizinhança de uma solução S é constituída por todas as soluções a que se pode chegar a partir de uma modicação em S , sendo a natureza da modicação justamente o que caracteriza a vizinhança. Serão apresentadas três vizinhanças, baseadas em vértices de Steiner (Seção 4.2.1), caminhos-chaves (4.2.2) e vértices-chaves (4.2.3). 4.2.1 Vértices de Steiner Considere a representação de uma solução como um vetor de incidência de vértices de Steiner: se uma determinada posição tem valor 1, o vértice correspondente pertence à solução; se tem valor 0, não pertence. Considera-se uma solução S 0 vizinha de uma solução S se os vetores que as representam forem diferentes em no máximo uma posição. Em outras palavras, se S 0 é obtida a partir da inserção ou da remoção de um único nó de Steiner de S . Isso signica que cada solução tem no máximo jV j jT j vizinhos. Possivelmente haverá menos, uma vez que, para que S 0 represente uma solução viável, o subgrafo induzido pelos terminais e pelos vértices de Steiner deve ser conexo. Utilizando-se essa vizinhança, todo o espaço de busca pode ser alcançado a partir de qualquer solução inicial. O diâmetro do espaço é jV j jT j. A solução mais distante de uma solução qualquer é aquela em que todos os jV j jT j bits que a representam estão invertidos. Para chegar de uma a outra, basta inserir inicialmente os vértices de Steiner da solução objetivo e em seguida remover os vértices de Steiner supéruos da solução inicial (como ambas as etapas são feitas passo a passo, deve-se ter o cuidado de escolher CAPÍTULO 4. BUSCA LOCAL 47 em cada momento um vértice que não desconecte o subgrafo induzido). 4.2.2 Caminhos-chaves Seja S = fc ; c ; c ; : : : ; ck g uma solução para o problema de Steiner no grafo G, representada em termos dos caminhos-chaves que a compõem. Se for removido de S um caminho-chave ci qualquer, restarão duas componentes conexas. Sejam Ca e Cb essas componentes. A Si vizinha de S associada a ci é formada pelas componentes Ca e Cb e por um caminho de comprimento mínimo que liga algum vértice de Ca a outro de Cb. De acordo com essa denição, a solução S pode ser seu próprio vizinho. O número de vizinhos de uma solução é igual ao número de caminhos-chaves que a compõem. Em [59], prova-se que esse número é no máximo 2jT j 3. Ao contrário da vizinhança baseada em nós de Steiner, essa vizinhança não permite percorrer todo o espaço de soluções a partir de qualquer solução factível. O contra-exemplo é simples: considere o grafo planar formado por um triângulo eqüilátero em que os três pontos extremos são terminais e o baricentro é um não-terminal ligado aos três terminais; se a solução inicial for formada por dois lados do triângulo, essa estratégia de busca local jamais encontrará uma solução que utilize o baricentro. Para que fosse possível percorrer todo o espaço de busca a partir de qualquer ponto, não se poderia exigir a minimalidade do caminho que religa as componentes formadas pela remoção de um caminho-chave. 1 2 3 4.2.3 Vértices-chaves A vizinhança baseada em vértices-chaves (ou nós-chaves) assemelha-se à baseada em nós de Steiner. Dada uma solução S , seus vizinhos são determinados a partir da inserção ou da remoção de um único nó-chave. Entretanto, ao contrário do que ocorre com a busca por nós de Steiner, o que se faz é uma tentativa de se mudar a solução de uma determinada forma. Considere, por exemplo, o caso da inserção. Seja KS o conjunto de nós-chaves da solução corrente S . A inserção de um novo nó-chave v é feita calculando-se a árvore geradora mínima do subgrafo do grafo de distâncias induzido por T [ KS [ fvg. Com certeza v pertencerá à nova árvore obtida, mas nada garante que será de fato um nó-chave, ou seja, que terá grau maior ou igual a três. Mais que isso, nada garante que o conjunto de nós-chaves da solução obtida será de fato KS [fvg. Nós que antes pertenciam a KS podem passar a ter grau menor que três e outros nós podem passar a ter grau maior. O caso da remoção é similar: não se sabe ao certo quais serão os nós-chaves da solução vizinha. Assim, a denição da vizinhança baseada em nós-chaves é menos rígida que a baseada em vértices de Steiner. Basicamente, o que se faz é sugerir uma modicação (tornar um vértice crucial ou não-crucial), mas a sugestão pode levar a modicações ainda mais profundas. Conforme se verá na Seção 4.4, neste caso a falta de rigidez não torna a busca local menos eciente. CAPÍTULO 4. BUSCA LOCAL 48 4.3 Implementação da Busca Local Nesta seção são discutidas implementações de métodos de busca local baseados nas três vizinhanças apresentadas na seção anterior. Conforme se verá, em alguns casos é interessante modicar ligeiramente a denição da vizinhança para tornar mais ecazes os algoritmos. Ao longo das subseções seguintes, será adotada a seguinte convenção: S = (VS ; ES ) denota a solução corrente e Sv representa uma solução vizinha, sendo v o vértice que caracteriza a transição S para Sv (o signicado preciso de v depende da vizinhança utilizada e cará claro em cada caso). 4.3.1 Estratégia Geral Os três métodos de busca local implementados compartilham a mesma estrutura geral; varia apenas a vizinhança considerada. O método básico consiste em percorrer circularmente a lista de vértices que fazem parte do grafo. Para cada vértice, verica-se se ele determina um ou mais movimentos. Em caso positivo, os vizinhos correspondentes são explorados, um a um. Se o custo de um vizinho for pelo menos tão bom quanto o da solução corrente, o vizinho passará a ser a solução corrente. Mesmo em caso de substituição da solução corrente, a busca continua a partir do nó seguinte na lista. A busca termina assim que for feita uma passagem por todos os nós sem que haja melhora estrita no valor da solução corrente. Diz-se então que a busca chegou a um ótimo local . A noção de movimento depende da vizinhança utilizada. Para a busca por nós de Steiner, um não-terminal v pode induzir dois tipos de movimento: inserção, se não zer parte da solução, e remoção, se zer. Para a busca por vértices-chaves, um não-terminal v será candidato a remoção se zer parte da solução corrente e tiver grau maior ou igual a três; todos os demais não-terminais são candidatos a inserção. Para a busca baseada em caminhos-chaves, induzem movimentos tanto os terminais quanto os vértices-chaves: todos os caminhos incidentes em v são caminhos-chaves e, portanto, candidatos a substituição (nesse caso, um mesmo vértice pode induzir mais de um movimento). Pruning Nas buscas baseadas em nós de Steiner e nós-chaves, vizinhos são determinados pelo cálculo de árvores geradoras mínimas. Se houver um nó não-terminal de grau unitário na árvore, tanto ele quando a aresta associada a ele são retirados. Isso mantém a viabilidade da solução e reduz o seu valor, o que contribui para melhorar a qualidade das soluções fornecidas pelas buscas locais e acelera sua convergência. A poda (pruning ) de todos os vértices descartáveis de cada solução pode ser feita em tempo O(jV j), desprezível em relação ao tempo total de execução das rotinas de busca local. Assim sendo, esse procedimento foi adotado na computação de vizinhos tanto na busca por vértices de Steiner quanto na busca por vértices-chaves. CAPÍTULO 4. BUSCA LOCAL 49 Aleatorização Na descrição inicial da estratégia de busca local, mencionou-se que a lista de vértices do grafo deve ser percorrida circularmente. A ordem em que os vértices são considerados pode interferir na qualidade das soluções obtidas. Nas implementações realizadas, utilizou-se sempre uma permutação aleatória dos vértices. 4.3.2 Busca por Vértices de Steiner Dado um vértice v, a vizinhança baseada em vértices de Steiner prevê dois tipos de movimentos: inserção e remoção. Na análise de um potencial vizinho Sv , é necessário determinar tanto sua viabilidade como seu custo. Viabilidade No caso do movimento de inserção, o teste de viabilidade é trivial. Basta vericar se existe uma aresta entre ele e algum outro vértice da solução, o que pode ser feito em tempo proporcional ao grau de v. Para acelerar a busca local, é conveniente exigir que haja pelo menos duas arestas ligando v a VS . Se houver apenas uma aresta, a solução vizinha será viável, mas seu custo será maior que o de S (antes do pruning ), pois v será obrigatoriamente uma folha. Apenas com duas ou mais arestas ligando v ao conjunto original de vértices é possível que a solução vizinha seja melhor. Para o movimento de remoção, o teste de viabilidade é mais complexo. Seria necessário fazer uma busca (em profundidade, por exemplo) no subgrafo induzido pelos vértices restantes na solução (nós de Steiner e terminais) e vericar se todos são alcançados. Isso requer O(jE j) operações no pior caso.1 Na implementação realizada, optou-se por embutir o teste de viabilidade no próprio cálculo do custo da solução (árvore) vizinha. Custo O custo da solução vizinha é calculado com o algoritmo de Kruskal para o proble- ma da árvore geradora mínima. Conforme já mencionado na Seção 2.3.1, esse algoritmo é executado em duas fases. Na primeira, as arestas candidatas a pertencer à árvore geradora mínima são ordenadas em ordem não-decrescente de custos. Na segunda etapa, percorre-se a lista de arestas e, sempre que uma aresta não formar ciclos, ela é inserida na solução. Durante a execução da busca local, o algoritmo de Kruskal é aplicado diversas vezes sobre diferentes subconjuntos de E . Contudo, não é necessário que em cada execução seja executado um algoritmo completo de ordenação. Em lugar disso, ordenam-se uma única vez todas as arestas em E , o que requer O(jE j log jE j) operações. Seja L a lista ordenada de arestas resultante. A partir de L , criam-se jV j sublistas (também ordenadas), cada uma representando as arestas adjacentes a um dos vértices: L ; L ; : : : ; LjV j. Repare que 0 0 1 2 Na verdade, em tempo (j j) é possível determinar todos os vértices cuja remoção inviabiliza a solução. Basta calcular as componentes biconexas do subgrafo de induzido pelos vértices da solução; os vértices de articulação são os que não podem ser removidos. Um algoritmo para encontrar componentes biconexas em tempo (j j) é descrito, por exemplo, em [37]. Esse algoritmo não foi implementado. 1 O E G O E CAPÍTULO 4. BUSCA LOCAL 50 as sublistas não são disjuntas, já que cada aresta aparece em exatamente duas delas. Como a lista original de arestas L é ordenada, a criação das sublistas pode ser feita em tempo total O(jE j). Além das jV j + 1 listas já mencionadas, é conveniente manter, a cada momento, duas outras listas ordenadas: LS , contendo as arestas da solução corrente, e LS , contendo todas as arestas do subgrafo de G induzido pelos vértices da solução corrente. Ambas as estruturas podem ser criadas em tempo O(jE j) a partir de L . Repare que, como a solução corrente (S ) pode mudar ao longo do algoritmo, ambas as listas podem ser recomputadas diversas vezes (ao contrário das listas L ; L ; : : : ; LjV j). Uma vez denidas as estruturas auxiliares, pode-se discutir a implementação dos dois tipos de movimento na busca por nós. No caso do movimento de inserção, pode-se garantir que existe uma árvore geradora mínima do grafo induzido por VS [fvg contendo apenas arestas originalmente contidas na solução corrente (ES ) e arestas que ligam v a VS . Assim, conjunto de arestas candidatas é resultado da intercalação de LS e o subconjunto relevante de Lv , a lista de arestas incidentes em v. Como há no máximo O(jV j) elementos em cada lista, a intercalação pode ser feita em tempo O(jV j). A complexidade do movimento de inserção como um todo é, portanto, dominada pela da segunda fase do algoritmo de Kruskal: O(jV j(2jV j; jV j)), praticamente linear.2 Para o movimento de remoção, utiliza-se como lista de candidatos LS , que contém todas as arestas do subgrafo induzido pela solução corrente. Algumas dessas arestas (as incidentes em v) são simplesmente ignoradas durante a segunda fase do algoritmo de Kruskal, que é executada em tempo O(jE j(jE j; jV j)) (no pior caso, LS tem O(jE j) elementos). Conforme já mencionado, o algoritmo de Kruskal incorpora um teste de viabilidade: ao nal do algoritmo ele testa, em tempo constante, se a solução criada tem de fato apenas uma componente conexa. A atualização da lista LS é feita após todo movimento bem-sucedido, seja ele de remoção ou inserção. A atualização pode ser feita em tempo O(jE j). Se efetuada após um movimento de remoção, basta retirar da lista as arestas incidentes no vértice removido. Se efetuada após uma inserção, basta intercalar a lista anterior com a lista de arestas incidentes no vértice inserido (restrita às arestas incidentes em vértices do subgrafo). 0 0 0 1 4.3.3 Busca por Caminhos-chaves A implementação da busca local baseada em caminhos-chaves deriva diretamente da denição da vizinhança. Quando um caminho-chave é retirado da solução, ela é dividida É possível implementar o movimento de inserção em tempo (j j) usando algoritmos especícos para atualização de árvores geradoras mínimas [40, 53]. A implementação desses algoritmos, no entanto, é relativamente complexa, o que sugere que sua aplicação na prática pode não ser tão vantajosa. Esse métodos não foram implementados. 2 O V CAPÍTULO 4. BUSCA LOCAL 51 em duas componentes conexas, Ca e Cb . A determinação da solução vizinha consiste em calcular o menor caminho mínimo entre um vértice de Ca e um vértice de Cb. Isso pode ser feito em tempo O(jE j + jV j log jV j) executando-se o algoritmo de Dijkstra. Utillizam-se como pontos de partida todos os vértices de uma das componentes (inseridos inicialmente no heap com peso 0) e como alvo qualquer vértice da outra componente. Um detalhe de implementação importante para o tempo de execução do algoritmo é a escolha da componente que servirá de origem em cada caso. Tipicamente, ao se remover um caminho-chave, uma das componentes geradas tem muito menos vértices que a outra. Em tempo proporcional ao tamanho da solução, O(jV j), é possível determinar qual delas é menor. Escolhê-la como origem acelera consideravelmente a execução do algoritmo. Conforme já mencionado, a busca é implementada de forma circular e exaustiva, sendo os vértices analisados seguindo uma permutação aleatória dos rótulos originais. Para cada vértice-chave ou terminal v, vericam-se os vizinhos determinados por todos os caminhoschaves da solução S que têm v como extremidade. Para evitar a repetição de operações, só são considerados os caminhos-chaves cuja outra extremidade tem rótulo maior que v. Se a solução vizinha Sv for melhor que a solução original, Sv torna-se a nova solução corrente. Dene-se agora o que torna uma solução vizinha melhor que a solução corrente. De acordo com a própria denição da vizinhança (Seção 4.2.2), um vizinho Sv de uma solução S jamais terá valor maior que o de S . Evidentente, se Sv tiver valor estritamente menor que o de S , Sv deve se tornar a solução corrente. Caso contrário (i.e., se as duas soluções tiverem o mesmo valor), os seguintes critérios de desempate são considerados: 1. Número de terminais Seja ci o caminho-chave retirado de S e c0i o caminhochave que o substitui em Sv . Será considerada melhor a solução cujo caminho-chave correspondente tiver mais terminais como extremidade (o número de terminais pode ser 0, 1 ou 2). Se, por exemplo, ambas as extremidades de c0i forem terminais mas apenas uma das extremidades de ci o for, a solução corrente será substituída pela vizinha. A motivação para esse critério é simples. A substituição de um caminhochave por outro que utiliza mais terminais diminui em uma unidade o grau de pelo menos um não-terminal u que originalmente era vértice-chave. Em especial, se o novo grau de u for 2, ele deixará de ser um vértice-chave. Com isso, os dois caminhos-chaves que a ele se ligavam passam a formar um único caminho, que, por ser mais longo, tem maior probabilidade de ser substituído em iterações futuras do algoritmo. 2. Número de vértices Em caso de empate no critério anterior, escolhe-se a solução com maior número de vértices. Um aumento no número de vértices na solução tende a criar mais pontos de ligação entre os pares de componentes conexas formados pela remoção de caminhos-chaves em passos posteriores da busca local. Com isso, aumenta a chance de que caminhos menores sejam encontrados. Os critérios de desempate serão menos ecazes se utilizados apenas para escolher CAPÍTULO 4. BUSCA LOCAL 52 entre o caminho mínimo encontrado e o caminho originalmente existente entre as duas componentes. Na implementação realizada, o próprio algoritmo para determinação do caminho mínimo incorpora as regras de seleção. Durante a construção dos caminhos, dáse prioridade àqueles partem de terminais e/ou chegam a terminais, e em caso de empate, àqueles que possuem o maior número de vértices. Observe que, estando os critérios de desempate embutidos no próprio algoritmo de Dijkstra, a solução vizinha calculada será garantidamente melhor que a solução corrente (ou pelo menos igual a ela). Assim, sempre a solução vizinha substitui a corrente. A inclusão dos critérios de desempate no algoritmo pode ser feita de forma muito simples, pela adição de perturbações aos custos originais das arestas. Para privilegiar caminhos mais longos, basta subtrair um valor constante " do peso de todas as arestas. A prioridade para caminhos incidentes em terminais pode ser obtida subtraindo-se um fator extra " das arestas incidentes em um terminal e 2" das arestas incidentes em dois terminais. Se os pesos das arestas forem inteiros (como no caso de todas as instâncias testadas), os valores " = 1=(3jV j) e " = 1=3 (por exemplo) garantem que os critérios acima serão corretamente observados. Conforme já mencionado na Seção 4.3.1, a busca termina assim que todos os vértices forem testados sem que o valor da função objetivo se altere. Nesse caso, o valor é calculado sobre os custos originais das arestas, sem levar em conta as perturbações. Na prática, o que se verica é que os critérios de desempate são relevantes quando há uma multiplicidade de caminhos mínimos ligando duas componentes. Isso é comum principalmente para instâncias em que muitas arestas têm exatamente o mesmo peso, como nos classes OR-Library, VLSI e em algumas instâncias da série PUC (as terminadas em u). Para instâncias de Incidência e as da classe PUC terminadas em p, os critérios raramente são decisivos. Experimentos preliminares mostraram, no entanto, que o uso rigoroso dos critérios de desempate tem um lado negativo. Quando é necessário aplicar a busca local por caminhoschaves diversas vezes sobre a mesma instância (a partir de diferentes soluções iniciais), como no caso das metaeurísticas descritas no Capítulo 5, a variabilidade das soluções pode diminuir. É razoável que isso aconteça, já que os critérios de desempate contêm certos preconceitos quanto ao que constitui uma boa solução. Optou-se por uma solução intermediária: utilizam-se os critérios de desempate, mas não de forma estrita. Adiciona-se a ele um componente aleatório. Mais especicamente, subtrai-se do custo da aresta e valor " (jV j t(e) + rand (0; 20)) do custo de cada aresta, sendo " uma constante pequena, t(e) o número terminais em que e é incidente (0, 1 ou 2) e rand (0; 20) um número escolhido aleatoriamente com distribuição uniforme no intervalo [0; 20]. Essa expressão, empiricamente determinada, quase sempre obedece à prioridade dada a caminhos com mais terminais, mas usa apenas de forma aproximada o critério que privilegia caminhos mais longos. Na implementação realizada, sempre a solução vizinha substitui a corrente, ainda que seja igual ou mesmo pior (segundo os critérios sugeridos). 1 2 2 1 2 CAPÍTULO 4. BUSCA LOCAL 53 4.3.4 Busca por Vértices-chaves A busca por nós-chaves pode ser implementada como a busca por nós de Steiner, mas aplicada sobre o grafo de distâncias D(G). O principal problema dessa implementação é o fato de que o grafo de distâncias é completo, com O(jV j ) arestas. A quantidade de memória necessária se torna rapidamente proibitiva com o aumento de jV j. Felizmente, é possível calcular a árvore geradora mínima do grafo de distâncias sem que seja necessário determinar explicitamente esse grafo. De acordo com o discutido na Seção 3.1, pode-se utilizar uma adaptação da heurística construtiva DNH (com a implementação de Melhorn [39]). Em sua forma original, a heurística calcula a árvore geradora mínima do subgrafo de D(G) induzido por T , o conjunto de terminais. Para estender o algoritmo para o subgrafo induzido por KS [T (sendo KS o conjunto de nós-chaves), basta considerar como pseudoterminais justamente o conjunto T [ KS (os nós cruciais, na denominação introduzida na Seção 4.1). Conforme já mencionando na Seção 3.1, os caminhos escolhidos pela heurística DNH entre os pares de nós cruciais não são necessariamente disjuntos. Como uma aresta que aparece em mais de um caminho deve ser considerada apenas uma vez, a solução fornecida pela heurística DNH pode ter valor menor que o da árvore geradora mínima do subgrafo induzido pelo conjunto de nós cruciais. Do ponto de vista do método de busca local, essa particularidade é muito interessante: soluções vizinhas de qualidade melhor que a esperada são sempre bem-vindas. A análise experimental das heurísticas construtivas feita no capítulo anterior revela, no entanto, que a heurística DNH em geral provê soluções de menor qualidade que as fornecidas por outras heurísticas. Nesse aspecto, um dos métodos de maior destaque é a heurística de Prim, que fornece melhores soluções sem signicativo aumento no tempo de execução. Seria interessante, portanto, se a heurística de Prim pudesse substituir DNH na implementação da busca por nós-chaves. Isso pode de fato ser feito, uma vez que o seguinte teorema é válido: 2 Teorema 2 Dado um grafo G = (V; E ) e um conjunto de vértices X V , o custo de qualquer solução fornecida pela heurística de Prim sobre G usando X como um conjunto de terminais será menor ou igual ao custo de uma árvore geradora mínima do subgrafo de D(G) induzido por X , sendo D(G) o grafo de distâncias associado a G. Prova Seja M a árvore geradora mínima do grafo de distâncias e S uma solução obtida pela heurística de Prim. Seja e = (v; w) uma aresta (no grafo de distâncias) em M e suponha, sem perda de generalidade, que v é adicionado à solução S pelo algoritmo de Prim antes de w (observe que ambos são terminais). Para provar que c(S ) c(M ), basta provar que o custo da adição de w à árvore pela heurística de Prim é menor que ou igual a c(e), para toda aresta e de M . Por hipótese, a heurística adiciona v a S antes de w. Conseqüentemente, na iteração em que w foi adicionado, v era uma das raízes do algoritmo de Dijkstra executado e, portanto, a distância de w à árvore era no máximo CAPÍTULO 4. BUSCA LOCAL 54 igual a c(e) nesse momento. Como a adição de w a S se faz pela inclusão do caminho mais curto entre eles, o comprimento desse caminho não pode ser maior que c(e), o que conclui a prova. 2 A Figura 4.1 mostra um exemplo em que a heurística de Prim encontra uma solução de valor inferior ao da árvore geradora mínima do subgrafo induzido pelo conjunto de terminais. No exemplo, a árvore geradora tem valor 23, enquanto a solução fornecida pela heurística vale 22. Lembre-se de que, no caso da busca por nós-chaves, a heurística é aplicada não sobre o conjunto de terminais, mas sobre o de nós cruciais (que inclui também os nós-chaves). uu 13 v u L L 12 uu L L L L 13 L L 5 w 5 grafo original r L L L x L Lu v u L L uu L L L L L 13 L L L L L Lu x 10 árvore geradora mínima do grafo de distâncias para u, v e x v u L L 12 L L L L L L L L L L Lu x r 5 w 5 solução fornecida pela SPH (usando v ou x como raiz) Figura 4.1: Comparação entre a heurística de Prim e árvore geradora do grafo de distâncias Nos experimentos computacionais apresentados neste e nos próximos capítulos, o método de fato implementado como sub-rotina da busca local é o de Prim. Além disso, para cada vizinho, calcula-se também a árvore geradora mínima do subgrafo de G induzido pelos vértices da solução construtiva (obtida pela heurística de Prim). Esse procedimento pode melhorar o valor da solução inicialmente calculada. Por m, faz-se um pruning da árvore obtida, que consiste na remoção de todos os não-terminais que são folhas. Nesse ponto, permite-se até a remoção de vértices candidatos a nós-chaves, interpretados como terminais na execução da heurística de Prim. A combinação da heurística de Prim com o cálculo da árvore geradora do subgrafo e sua posterior poda contribui para que vizinhos de melhor qualidade sejam encontrados, principalmente quando a solução inicial está distante do valor ótimo. Conforme já mencionado, uma característica dessa busca local é o fato de que não há controle sobre a estrutura da solução vizinha obtida. A heurística de Prim recebe como entrada o grafo original e um conjunto de pseudoterminais (composto pelos terminais e pelo conjunto K de nós-chaves candidatos). Na solução obtida pelo algoritmo de Prim sobre essa entrada, haverá um conjunto K de nós não-terminais com grau maior que três, os nós-chaves. Não necessariamente é verdade que K 0 = K . Pode-se assegurar que os vértices de K farão parte da solução obtida, mas nada garante que todos terão grau maior CAPÍTULO 4. BUSCA LOCAL 55 que dois. Além disso, pode haver em K 0 vértices que não pertencem a K . Isso não deve ser entendido como uma deciência, mas como uma característica da busca local. Na implementação, adotou-se a seguinte convenção. Se uma solução vizinha Sv tiver valor menor ou igual ao da solução corrente S , Sv passa a ser a solução corrente. Para se chegar a Sv , calculou-se uma árvore associada a um determinado conjunto de nóschaves candidatos, que, como se viu, podem ser diferentes dos nós-chaves que de fato aparecem em Sv . Quando Sv se torna a solução corrente, passa-se também a considerar como corrente o conjunto real de nós-chaves. 4.3.5 Estratégias Híbridas A combinação das vizinhanças propostas permite a criação de estratégias híbridas de busca local, mais poderosas que os métodos individuais. A idéia é submeter as soluções encontradas por um método a outro. Mais precisamente, a busca híbrida consiste em executar um método de busca local até que ele não tenha mais capacidade de melhorar a solução; em seguida aplica-se sobre a solução resultante outro método, também até o seu limite; e assim sucessivamente. É interessante aplicar esse método de forma circular: depois de aplicados todos os métodos, retorna-se ao primeiro. Com isso, pode-se melhorar ainda mais o valor da função objetivo. Nesse caso, interrompe-se o algoritmo quando todos os métodos falharem consecutivamente. Repare que o resultado fornecido pela estratégia híbrida jamais será pior que o fornecido pelo primeiro dos métodos de busca local aplicados (por outro lado, ela também jamais será mais rápida que ele). No entanto, nada garante que a solução fornecida será melhor que a obtida pela aplicação exclusiva de algum dos demais métodos diretamente sobre a solução original. Em geral, contudo, é razoável esperar que os resultados fornecidos pelo método híbrido tenham qualidade superior aos fornecidos por cada um dos outros métodos isoladamente. Anal, na verdade está-se utilizando uma vizinhança mais ampla. A combinação de diversas buscas locais é relativamente comum em metaeurísticas. Para o caso especíco do problema de Steiner, há pelo menos um caso em que isso foi feito: em [38], Martins et al. apresentam um GRASP paralelo que usa uma combinação das buscas por nós de Steiner e por caminhos-chaves. 4.4 Resultados Experimentais Esta seção apresenta uma análise comparativa experimental de diversas estratégias de busca local. São testados de forma individual os três métodos propostos ao longo do capítulo: as buscas por nós de Steiner (N ), por caminhos-chaves (P, do inglês key-paths ) e nós-chaves (K, do inglês key-nodes ). Estudaram-se ainda algumas estratégias híbridas formados a partir da composição desses métodos: NP, PN, KNP, KPN, NPK e PNK. O rótulo de cada método híbrido é formado pelos símbolos que representam os métodos CAPÍTULO 4. BUSCA LOCAL 56 individuais utilizados, na ordem em que são aplicados. Como no caso das heurísticas construtivas, há dois aspectos básicos a se analisar: a qualidade das soluções fornecidas e o tempo de execução. Assim sendo, o estudo do desempenho relativo dos métodos é feito nos mesmos moldes do estudo apresentado no capítulo anterior. As estratégias de busca local foram testadas para todas as instâncias apresentadas na Seção 2.4.2. Para as classes OR-Library e VLSI, foram utilizadas as instâncias préprocessadas. Os tempos mencionados nesta seção, obtidos num AMD K6-2 de 350 MHz, não incluem os tempos de pré-processamento, apresentados separadamente no Apêndice A. Fixada uma instância, garantiu-se que todos os métodos de busca local fossem testados sobre exatamente a mesma solução inicial, construída pelo algoritmo de Prim. Para cada classe de instâncias, a Tabela 4.1 compara a qualidade de das soluções fornecidas por quatro estratégias de busca local (N, P, NP e PN ) utilizando as medidas introduzidas na Seção 3.6.1. Além disso, a mesma tabela compara os tempo de execução dessas estratégias; a última coluna mostra o tempo médio obtido por cada método em relação aos tempos obtidos pelo método-base (N, nesse caso). Instâncias para as quais todos os métodos encontraram o mesmo valor foram consideradas apenas para o cálculo do tempo relativo médio. As três primeiras medidas (dr%, rank e melhor) foram feitas com base apenas nas demais instâncias (o número de instâncias efetivamente consideradas em cada caso é mostrado na primeira coluna da tabela, junto ao nome de cada classe). A Tabela 4.2 tem a mesma estrutura, mas compara as estratégias NP, K, KNP, KPN, NPK e PNK. A estratégia NP é utilizado como base para o cálculo dos tempos relativos nesse caso. Fez-se a divisão dos métodos em dois grupos para a comparação porque tanto a busca K quanto as estratégias híbridas que a utilizam são de uma a duas ordens de grandeza mais lentas que as demais buscas. Apesar de os maiores tempos de execução em geral levarem a soluções de melhor qualidade, há situações em que utilizar a busca K pode não ser interessante, o que justica uma comparação mais pormenorizada das estratégias alternativas. A existência de um elemento comum aos dois grupos, a busca NP, permite que estratégias de grupos distintos sejam convenientemente comparadas. O fato de que estratégias que utilizam a busca K são signicativamente mais lentas pode ser comprovado utilizandose justamente o método NP : a Tabela 4.1 mostra que ele pode ser executado em tempos próximos aos dos métodos N, P e PN ; por outro lado, na Tabela 4.2 observa-se que os métodos que utilizam a busca por nós-chaves são pelo menos 20 vezes mais lentos. Também podem ser utilizadas para comparar os diversos métodos a Tabela 4.3 e a Tabela 4.4. A primeira mostra, para cada uma das séries de instâncias estudadas, o desvio percentual médio das soluções obtidas pelas diferentes estratégias de busca local em relação às melhores soluções primais conhecidas. A segunda tabela mostra os CAPÍTULO 4. BUSCA LOCAL classe estratégia dr% rank melhor tempo rel. N 1.171 2.480 117 1.000 Incidência P 14.797 3.436 56 1.014 (320 instâncias) NP 0.449 1.884 196 1.644 PN 0.573 1.961 187 2.139 N 0.798 2.707 14 1.000 OR-Library P 0.334 3.328 6 0.502 (29 instâncias) NP 0.068 1.931 22 1.566 PN 0.113 2.034 22 1.502 N 0.568 2.141 22 1.000 PUC P 4.284 3.946 0 0.370 (46 instâncias) NP 0.433 1.913 26 1.326 PN 0.477 2.000 25 1.600 N 1.162 3.622 6 1.000 VLSI P 0.361 2.592 18 0.795 (49 instâncias) NP 0.128 1.888 35 2.140 PN 0.105 1.898 32 1.820 N 1.083 2.586 159 1.000 Total P 11.170 3.389 80 0.879 (444 instâncias) NP 0.387 1.891 279 1.675 PN 0.481 1.963 266 1.988 Tabela 4.1: Estratégias formadas pelas buscas por nós de Steiner e caminhos-chaves 57 CAPÍTULO 4. BUSCA LOCAL classe estratégia dr% rank melhor tempo rel. NP 1.556 4.148 73 1.000 K 1.587 4.365 76 22.643 Incidência KNP 0.795 3.089 152 27.610 (337 instâncias) KPN 0.763 3.049 156 27.719 NPK 0.921 3.166 154 23.154 PNK 0.862 3.147 148 23.017 NP 1.239 5.354 3 1.000 K 0.037 3.229 20 77.472 OR-Library KNP 0.028 2.979 22 78.047 (24 instâncias) KPN 0.028 3.000 21 76.323 NPK 0.061 3.542 16 77.538 PNK 0.011 2.896 22 76.348 NP 1.731 4.564 5 1.000 K 2.268 4.894 5 19.506 PUC KNP 0.613 2.926 13 29.953 (47 instâncias) KPN 0.615 2.851 18 28.024 NPK 0.526 2.766 20 23.024 PNK 0.645 3.000 16 24.045 NP 0.708 5.407 4 1.000 K 0.160 3.477 22 20.930 VLSI KNP 0.112 3.163 24 22.243 (43 instâncias) KPN 0.084 3.163 25 21.776 NPK 0.134 2.988 29 22.838 PNK 0.113 2.802 31 21.427 NP 1.476 4.376 85 1.000 K 1.439 4.275 123 27.524 Total KNP 0.670 3.073 211 32.049 (451 instâncias) KPN 0.643 3.037 220 31.726 NPK 0.759 3.127 219 28.436 PNK 0.722 3.085 217 28.127 Tabela 4.2: Estratégias que utilizam a busca por nós-chaves 58 CAPÍTULO 4. BUSCA LOCAL 59 tempos de execução médios (em segundos) de cada estratégia. (Conforme já observado na Seção 3.6.2, deve-se ter cuidado ao analisar tabelas como essa, já que os valores mostrados são denidos basicamente pelo comportamento de cada método para as maiores instâncias.) Em ambas as tabelas, fez-se também a divisão dos métodos em dois grupos, determinados pelo fato de se utilizar ou não a busca por nós-chaves. Os melhores valores obtidos em cada grupo estão destacados em negrito. As medidas relativas à heurística construtiva (Prim) também são apresentados nas tabelas, como referência. série i080 i160 i320 i640 c d e bip cc hc alue alut diw dmxa gap msm taq Prim 16.898 19.592 20.558 21.937 1.359 1.749 2.826 15.068 9.674 8.472 2.355 2.547 1.207 2.157 2.585 1.615 2.199 N 3.466 3.608 3.879 4.241 0.182 0.748 1.778 7.382 3.687 2.549 1.376 1.753 0.823 1.460 1.160 0.741 1.063 P 11.637 14.723 16.397 17.216 0.272 0.831 0.897 10.930 6.684 7.059 0.769 0.761 0.446 0.481 0.554 0.593 0.644 desvio percentual médio NP 2.934 3.178 3.264 3.441 0.146 0.620 0.825 7.122 3.534 2.549 0.540 0.802 0.424 0.433 0.340 0.372 0.395 PN 3.289 3.231 3.361 0.401 K 3.235 3.030 3.288 3.385 0.035 0.008 0.030 6.643 3.797 4.272 0.237 0.175 0.050 0.399 0.170 3.345 0.227 0.672 0.761 6.461 3.977 2.329 0.674 0.630 0.181 0.318 0.485 0.182 0.015 0.394 KNP 2.521 2.392 2.624 2.655 0.035 KPN 2.467 2.386 2.613 0 2.613 0.028 0.008 0.026 0.026 4.678 2.792 1.821 0.192 0.050 4.771 2.769 1.806 0.191 0.174 0.050 0.170 0.170 0 0 0.164 0.015 0.370 0.015 0.241 NPK 2.366 2.655 2.831 2.769 0.017 0.041 0.045 4.566 2.569 2.023 0.175 0.182 0.104 0.292 0.231 0.008 0.181 PNK 2.340 2.592 2.966 2.501 0.005 0 0.035 4.749 2.951 1.585 0.185 0.195 0.024 0.227 0 0.099 0.182 Tabela 4.3: Desvios percentuais médios em relação às melhores soluções conhecidas Com base no que a apresentam as Tabelas 4.1 a 4.4, é possível efetuar uma análise pormenorizada do comportamento dos diversos algoritmos. Estratégias mais rápidas (N e P ) Inicialmente, considere apenas as estratégias mais rápidas, que não utilizam a busca K. Comparando-se as duas formas puras de busca local nesse grupo (P e N ), verica-se que a busca baseada em nós de Steiner tende a obter soluções de melhor qualidade para grafos densos ou com muitos terminais. Para instâncias de incidência, os resultados da busca por nós foram sensivelmente melhores que os obtidos pela busca por caminhos-chaves. Para as instâncias VLSI e a série e da OR-Library (instâncias esparsas após o pré-processamento), a busca P é a melhor. Os caminhos-chaves nesses casos são longos (possuem mais arestas), o que favorece a busca P. Além disso, a busca N pouco tem a fazer, já que raramente um único nó de Steiner pode ser adicionado ou retirado da solução sem torná-la inviável. Quanto ao tempo de execução, o que se observa é que a busca P depende menos fortemente do número de vértices. Para instâncias muito esparsas, ela é a mais rápida; CAPÍTULO 4. BUSCA LOCAL série i080 i160 i320 i640 c d e bip cc hc alue alut diw dmxa gap msm taq Prim 0.0011 0.0049 0.0234 0.1158 0.0012 0.0033 0.0110 0.0355 0.0460 0.0350 0.0120 0.0185 0.0024 0.0003 0.0014 0.0007 0.0032 N 0.008 0.033 0.147 0.714 0.009 0.038 0.286 2.797 6.070 9.534 6.720 8.265 0.034 0.002 0.024 0.008 0.192 60 tempos médios de execução (em segundos) P K 0.007 NP PN 0.012 0.016 0.040 0.050 0.087 0.287 0.234 0.509 2.383 1.185 3.493 0.003 0.012 0.010 0.010 0.058 0.044 0.053 0.416 0.249 0.456 3.722 4.222 0.264 7.726 6.891 1.415 9.790 8.747 1.204 23.669 10.079 1.614 12.013 17.952 0.021 0.075 0.047 0.002 0.006 0.004 0.014 0.054 0.056 0.005 0.016 0.015 0.060 0.375 0.294 0.06 27.79 KNP 0.08 0.60 4.53 47.24 0.45 0.45 0.41 3.25 2.82 2.72 21.23 21.47 98.66 119.22 69.69 162.72 110.83 136.69 237.66 317.15 403.54 796.84 5.87 5.71 0.11 0.11 5.75 5.73 0.65 0.64 8.53 10.02 KPN 0.08 0.61 4.54 45.73 0.46 2.79 20.99 123.01 145.97 130.55 325.38 526.23 5.67 0.11 5.79 0.64 8.56 NPK 0.06 0.41 3.36 25.81 0.45 2.44 22.33 92.38 144.31 116.73 370.45 515.34 PNK 0.07 0.46 3.51 28.26 0.46 2.56 20.93 132.68 104.96 165.88 345.81 344.95 5.32 6.40 0.08 0.08 6.07 0.59 8.90 6.07 0.61 8.45 Tabela 4.4: Tempos médios de execução para mais densas (como as de incidência) a busca N é a mais rápida. De qualquer forma, observa-se que a ordem de grandeza dos tempos de execução é, em média, a mesma para as instâncias testadas. Conforme esperado, as buscas híbridas (NP e PN ) levam a soluções de melhor qualidade quando comparadas aos métodos puros. Conforme mostram as Tabelas 4.1 e 4.3, na média ambas as buscas híbridas obtêm resultados melhores que qualquer dos métodos individuais, mesmo que o pior método seja executado primeiro. Ainda assim, os tempos de execução das buscas híbridas não são consideravelmente muito maiores que os das buscas individuais. Isso indica que, na prática, cada um dos métodos é executado um número pequeno de vezes: o ótimo local híbrido é alcançado logo nas primeiras iterações. Quando as buscas NP e PN são comparadas entre si, não se observam diferenças signicativas na qualidade das soluções obtidas. Ora uma das estratégias é melhor, ora a outra. Já a diferença no tempo de execução pode ser expressiva. Observe por exemplo as séries i640, e e alue na Tabela 4.4: nesses casos, uma estratégia pode requerer até o dobro do tempo da outra, em média. A melhor política para garantir um bom desempenho parece ser executar primeiro o método mais rápido, até porque os demais jamais serão executados mais vezes que ele. Estratégias que utilizam a busca por nós-chaves Conforme já mencionado, a busca K é consideravelmente mais lenta que as buscas N e P. E a diferença é grande, freqüentemente de uma ou duas ordens de grandeza. Em contrapartida, as soluções encontradas pela busca K foram, em todas as séries de instâncias, melhores (na média) que soluções CAPÍTULO 4. BUSCA LOCAL 61 encontradas pelas buscas N ou P. A superioridade da busca K é mais evidente nas classes VLSI e OR-Library, conforme mostram tanto a Tabela 4.1 quanto a Tabela 4.3. Apesar de menos poderosas, as buscas N e P podem contribuir para melhorar o desempenho da busca K. Os ganhos obtidos quando se utiliza uma estratégia híbrida (NPK, PNK, KNP, KPN ) são mais evidentes justamente nos casos em que a busca K não é tão superior às demais: nas classes Incidência e PUC (a Tabela 4.2 deixa clara essa diferença). Os quatro métodos híbridos estudados fornecem soluções de qualidade semelhante: não é evidente a superioridade de nenhum dos métodos sobre os demais. Já quando se considera o tempo de execução, nota-se uma diferença entre os métodos, principalmente nas classes Incidência e PUC. Iniciar a busca híbrida com os métodos mais rápidos (N ou P ) parece ser mais vantajoso que iniciá-la com a busca K. Para algumas séries, as estratégias NPK e PNK são até mais rápidos que o método K puro, conforme mostram os valores em negrito na Tabela 4.2. 4.5 Conclusão A Tabela 4.3 conrma a armação feita no início deste capítulo: métodos de busca local podem melhorar signicativamente soluções produzidas por heurísticas construtivas. Para várias das séries testadas, é possível reduzir o desvio relativo médio em uma ordem de grandeza. Tome o caso da série i320: um desvio de 20.5% é reduzido a 3.2% pela busca NP e a 2.6% pela busca KPN. Na série d, o valor reduz-se de 1.7% para zero pela ação das estratégias KNP e PNK, o que signica que todos os ótimos da série são encontrados. Há casos em que a redução é relativamente mais modesta, como no caso das instâncias bip. Mesmo assim, os ganhos são signicativos. Naturalmente, as soluções de melhor qualidade são obtidas às custas de um aumento no tempo de execução do algortimo. Mesmo as buscas mais rápidas são pelo menos uma ordem de grandeza mais lentas que a heurística construtiva. As buscas mais caras, baseadas na busca K, podem chegar a ser quatro ordens de grandeza mais lentas. Ainda assim, em situações em que a qualidade das soluções é o mais importante, aumentos dessa ordem (ou ainda maiores) no tempo de execução são toleráveis. De qualquer forma, as buscas podem ser aceleradas se forem feitos alguns ajustes na implementação. Na implementação da busca N, por exemplo, seria interessante manter um controle das componentes biconexas do subgrafo induzido pela solução corrente. Isso permite descartar vértices candidatos a remoção sem que seja necessário calcular explicitamente o vizinho. A busca K, no entanto, parece ser a que tem maior espaço para acelerações. Em particular, seria interessante buscar uma implementação em que as operações realizadas para a construção de um vizinho pudessem ser aproveitadas para a computação do vizinho seguinte. Mais concretamente, pode-se conceber uma implementação baseada na heurística DNH em que o diagrama de Voronoi (cuja construção constitui primeiro passo da heurística) fosse apenas atualizado de uma iteração para ou- CAPÍTULO 4. BUSCA LOCAL 62 tra. As possibilidades de implementação são muitas e merecem estudo mais aprofundado. Anal, conforme se pôde observar, é excelente a qualidade das soluções fornecidas pela busca K ; qualquer aceleração seria muito bem-vinda. Capítulo 5 Metaeurísticas As estratégias de busca local apresentadas no capítulo anterior fornecem soluções de qualidade média muito superior às obtidas por heurísticas construtivas. Há situações, no entanto, em que mesmo esses resultados são insucientes. Apela-se então para metaeurísticas, heurísticas mais elaboradas normalmente formadas a partir da composição de elementos de heurísticas mais simples. Uma característica importante de metaeurísticas é uma exploração mais completa e cuidadosa do espaço de soluções. Um estudo sobre metaeurísticas para problemas de otimização combinatória em geral pode ser encontrado em [1]. Entre as metaeurísticas mais tradicionais, há na literatura exemplos de implementações para o problema de Steiner em grafos: algoritmos genéticos [18, 30], simulated annealing [11], busca tabu [3, 21, 49], GRASP [38], entre outras. Este capítulo descreve HGP+PR, uma nova metaeurística para o problema de Steiner em grafos. Trata-se de um GRASP híbrido baseado em perturbações com religamento (path-relinking ) adaptativo. Esse método foi introduzido em [50], artigo escrito em coautoria com Celso C. Ribeiro e Eduardo Uchoa. A estrutura básica do algoritmo é descrita na Seção 5.1. Seus elementos constituintes são discutidos mais detalhadamente nas Seções 5.2 a 5.5. Resultados experimentais, incluindo comparações do algoritmo com outras metaeurísticas, são apresentados na Seção 5.6. Por m, na Seção 5.7 é feita uma análise das características mais importantes do algoritmo. 5.1 HGP+PR: Estrutura Básica A metaeurística aqui apresentada para a resolução do problema de Steiner em grafos é semelhante a um GRASP [19] (Greedy Randomized Adaptive Search Procedure, ou Procedimento Guloso Aleatorizado Adaptativo de Busca). Trata-se de um método muito 63 CAPÍTULO 5. METAEURÍSTICAS 64 simples, que tem como principal parâmetro de entrada o número de iterações. Em cada iteração de um GRASP, cria-se uma solução construtiva e, em seguida, aplica-se sobre ela um método de busca local. Considera-se como solução da metaeurística a melhor solução encontrada ao longo de todas as iterações. Repare que, se a mesma solução construtiva fosse encontrada em todas as iterações do algoritmo, também os ótimos locais seriam semelhantes. Assim, um importante aspecto do GRASP (em sua forma original) é o uso de um heurística construtiva aleatorizada. Um algoritmo guloso puro toma sempre a decisão que parece ser mais vantajosa em um determinado instante de sua execução; em versões aleatorizadas, a ação tomada em cada ponto é sorteada entre uma lista de alternativas mais promissoras. Quanto maior o tamanho da lista, maior será o grau de aleatorização do algoritmo. A metaeurística aqui proposta tem a mesma estrutura básica de um GRASP: em cada iteração, gera-se uma solução construtiva com aleatorização e executa-se um procedimento de busca local. A diferença básica está justamente na forma de garantir a variabilidade das soluções construtivas. No GRASP original, os elementos de aleatorização são inseridos no código do algoritmo construtivo. O que se propõe aqui é, em lugar disso, alterar a própria instância em cada iteração, adicionando-se perturbações aos pesos das arestas. A heurística gulosa é aplicada sobre o grafo com pesos perturbados; para a busca local, os pesos originais são restaurados. A principal vantagem da aleatorização por perturbações é a facilidade de implementação. Além disso, ela permite que de forma simples sejam incorporados ao algoritmo conceitos normalmente utilizados com sucesso em outras metaeurísticas. Combinam-se diferentes funções de perturbação de forma a que se alternem elementos de intensicação (exploração mais pormenorizadas de regiões promissoras do espaço de busca) e de diversicação (exploração de regiões menos conhecidas do espaço de busca). Isso caracteriza o uso de oscilação estratégica , originalmente utilizada em métodos de busca tabu. O algoritmo também utiliza perturbações em uma etapa de pós-otimização. As melhores soluções criadas pelo GRASP são utilizadas para compor um pool de soluções de elite. Após a execução de todas as iterações do GRASP, essas soluções são combinadas entre si, numa técnica conhecida como path-relinking (religamento), implementado tanto da forma usual (com um algoritmo especicamente criado para tal m) quando de forma aleatorizada, baseada no uso de perturbações. Em resumo, a principal característica da metaeurística proposta é permitir a integração, a partir de um único conceito (perturbações), de diversos elementos utilizados em outras metaeurísticas (tais como a geração de soluções gulosas aleatorizadas, intensicação, diversicação, oscilação estratégica e religamento). Conforme já mencionado, será usada a sigla HGP+PR (Hybrid GRASP with Perturbations and Path-Reliking ) em referências a essa metaeurística. HGP representa as versões do algoritmo sem a etapa de pós-otimização (religamento). A Figura 5.1 mostra um pseudocódigo simplicado do algoritmo HGP+PR. CAPÍTULO 5. METAEURÍSTICAS 65 01 function HGP+PR { 02 for k = 1 to max_it do { 03 aplique uma estratégia de perturbação sobre os pesos originais; 04 construa uma solução gulosa usando os pesos perturbados; 05 aplique busca local sobre a solução obtida (usando pesos originais); 06 atualize o pool de soluções de elite; 07 } 08 aplique path-relinking sobre o pool de soluções de elite; 09 return (melhor solução encontrada); 10 } Figura 5.1: Pseudocódigo do GRASP híbrido com perturbações e religamento Nas próximas seções, cada um dos componentes do algoritmo será analisado separadamente. Na Seção 5.2, discute-se a escolha das heurísticas construtivas utilizadas. A Seção 5.3 trata das estratégias de busca local. O uso de perturbações para aleatorização das heurísticas construtivas é analisado na Seção 5.4. A Seção 5.5 discute as estratégias de religamento utilizadas. 5.2 Heurísticas Construtivas Uma das principais vantagens do uso de perturbações é o fato de que elas permitem a aplicação direta de qualquer heurística construtiva. Em um GRASP puro, toda heurística construtiva utilizada deve ter um componente de aleatorização inserido no próprio algoritmo, o que requer um esforço adicional de implementação. A criação de uma lista de candidatos muitas vezes altera signicativamente a estrutura dos algoritmos utilizados. No lugar de um heap, por exemplo, pode ser necessário usar uma árvore binária de busca para armazenar os elementos mais promissores. Outra diculdade encontrada na aleatorização de algoritmos construtivos está no fato de que muitos deles não possuem uma aleatorização natural. A criação da lista de candidatos pode simplesmente adiar a inserção de elementos originalmente prioritários, sem alterar signicativamente a estrutura das soluções obtidas. Isso compromete seriamente a eciência do GRASP, que depende da aleatorização para explorar diferentes regiões do espaço de busca. Uma alternativa para superar esse problema é criar uma lista de candidatos excepcionalmente grande. Apesar de essa medida de fato aumentar a variabilidade, ela diminui a qualidade das soluções obtidas. Numa tentativa de aleatorização da heurística de Prim para o problema de Steiner em grafos, por exemplo, as diculdades descritas acima podem ser claramente observadas. A implementação descrita na Seção 3.3 não pode ser utilizada, já que ela supõe que, uma vez retirado do heap um nó terminal, o caminho que o liga à árvore será incorporado à solução. CAPÍTULO 5. METAEURÍSTICAS 66 Na versão aleatorizada, isso não necessariamente é verdade. Caminhos distantes seriam adicionados previamente à árvore, possivelmente diminuindo a distância de alguns vértices já retirados do heap à árvore. Um heap provavelmente deixaria de ser a estrutura mais adequada para manter as alternativas mais promissoras. Quando se utiliza a estratégia baseada em perturbações, esses problemas desaparecem. Qualquer heurística construtiva pode ser utilizada até mesmo como uma caixa-preta. Nenhuma alteração na sua estrutura é necessária. A variabilidade é garantida pela perturbação do GRASP. Basta ajustar a intensidade das perturbações para aumentar ou diminuir a variabilidade das soluções obtidas. Para deixar clara a exibilidade de uso de diferentes métodos construtivos na metaeurística, foram utilizadas cinco das heurísticas descritas no Capítulo 3: Prim (implementação direta), Kruskal (implementação com heap de nós), Bor·vka, DNHz e Multispan. Os métodos são utilizados nessa ordem nas primeiras cinco iterações: Prim na primeira, Kruskal na segunda, etc. Em cada uma das iterações posteriores, um dos métodos é escolhido aleatoriamente. 5.3 Busca Local Assim como ocorre no caso das heurísticas construtivas, há total liberdade de escolha em relação à estratégia de busca local utilizada em cada iteração. Os três métodos básicos (P, N e K ) apresentados no Capítulo 4 podem ser utilizados, bem como qualquer outro método que se venha a propor. Evidentemente, também métodos híbridos podem ser aplicados. Essa facilidade de substituição dos diversos componentes é uma das mais importantes características da metaeurística proposta. Para a maior parte dos experimentos apresentados neste capítulo, optou-se por utilizar as buscas NP e PN : uma delas é escolhida aleatoriamente em cada iteração. Conforme mostra a Seção 4.4, as soluções fornecidas por esses métodos (muito semelhantes entre si) têm desempenho superior aos métodos N (busca por nós de Steiner) e P (busca por vértices-chaves), sem que isso signique tempos de execução expressivamente maiores. Estratégias de busca local utilizando a busca por nós-chaves (método K ) produzem soluções de melhor qualidade, mas com tempo de execução consideravelmente maior. Conforme mencionado na Seção 4.3.1, todos os procedimentos de busca local testados percorrem circularmente uma lista de vértices que é uma permutação aleatória de seus rótulos originais, recalculada no início da execução de cada método. Isso signica que, no HGP+PR, também os procedimentos de busca local têm um componente de aleatorização, que contribui para melhorar a qualidade das soluções fornecidas pela metaeurística. CAPÍTULO 5. METAEURÍSTICAS 67 5.4 Estratégias de Perturbação Foram utilizadas três funções de aleatorização distintas. Em todos os casos, o peso da aresta e na i-ésima iteração do algoritmo (ci(e)) é escolhido aleatoriamente com distribuição uniforme entre c(e) (o peso original) e ri(e) c(e), sendo o coeciente ri(e) justamente o que diferencia os três métodos. No cálculo de ri(e), dois dos métodos utilizam o número de soluções (ótimos locais) em que a aresta e aparece antes da iteração i; esse número é representado por ti (e). Claramente, 0 ti (e) i. A Tabela 5.1 mostra como o coeciente ri(e) é calculado para os três métodos de aleatorização. 1 1 método efeito coeficiente (ri (e)) D diversicação 1:25 + 0:75 ti (e)=(i 1) I intensicação 2 0:75 ti (e)=(i 1) U uniforme 2 Tabela 5.1: Coecientes máximos para aleatorização 1 1 No método D, o valor de ri(e) tende a ser maior para arestas que apareceram mais freqüentemente nas soluções previamente encontradas. Com isso, esse método tende a estimular a diversicação das soluções. O método I , ao contrário, penaliza as arestas pouco utilizadas em iterações anteriores, favorecendo as mais utilizadas. Trata-se, portanto, de uma estratégia de intensicação , concentrando a busca em regiões mais promissoras do espaço de soluções. O terceiro método, U , penaliza as arestas de forma uniforme, independentemente de sua utilização em iterações anteriores. Evidentemente, como se trata de um GRASP, tanto a diversicação quanto a intensicação devem ser feitas mantendo-se a garantia de aleatorização das soluções construtivas. Por isso, o valor mínimo de ri(e) jamais é inferior a 1.25 nos métodos I e D: garante-se a existência de um intervalo dentro do qual o peso de cada aresta é escolhido. As penalizações não determinam diretamente o custo penalizado da aresta, elas atuam sobre o conjunto de valores que o custo pode assumir. Escolha dos métodos Em cada iteração da metaeurística, pode-se utilizar qualquer dos métodos de perturbação descritos acima (I , D ou U ), bem como qualquer outro que se venha a conceber. Conforme já mencionado, os métodos de aleatorização I e D exploram dois conceitos bastante utilizados em metaeurísticas elaboradas: intensicação e diversicação. Quando as técnicas são empregadas alternadamente, o algoritmo como um todo utiliza uma variante de oscilação estratégica, comum em implementações de busca tabu. A oscilação estratégica é denida justamente como a alternância entre fases de intensicação e de diversicação na exploração do espaço de busca durante a execução de um algoritmo. Repare que, no caso do HGP, isso é feito de maneira muito simples, por meio da manipulação dos pesos das arestas. CAPÍTULO 5. METAEURÍSTICAS 68 Ao menos na primeira iteração do HGP, é conveniente não se utilizar nenhuma perturbação, até porque o valor de t não está denido. Na implementação realizada, as h primeiras iterações são executadas sem perturbação, sendo h o número de heurísticas construtivas utilizadas. O próprio fato de se utilizarem diferentes heurísticas induz a variabilidade das soluções nas iterações iniciais. Em cada uma das demais iterações, os métodos da estratégia utilizada são aplicados alternadamente, sendo as heurísticas construtivas escolhidas aleatoriamente. Essa estratégia torna-se mais clara com um exemplo. Suponha que se executem 128 iterações de um HGP com duas heurísticas construtivas (H e H ) e três métodos de perturbação (I , D e U ). Inicialmente, cada método construtivo é aplicado sem qualquer perturbação. Na primeira iteração, o método H é aplicado; na segunda, H . A partir da terceira iteração, os métodos de perturbação (I , D e U ) são utilizados alternadamente: I na terceira iteração, D na quarta, U na quinta, I na sexta, D na sétima, etc. A heurística construtiva aplicada em cada iteração (a partir da terceira) é escolhida aleatoriamente, com igual probabilidade para cada uma delas. Com isso, espera-se que todas as combinações entre heurísticas construtivas e métodos de perturbação sejam tentadas. Nos testes apresentados na Seção 5.6.1, são utilizadas cinco heurísticas construtivas (Prim, Kruskal, Bor·vka, DNHz e Multispan) e três métodos de perturbação (I , D e U ). 0 1 1 2 2 5.5 Religamento (Path-relinking ) O objetivo primário de um GRASP é encontrar uma solução de boa qualidade. Entretanto, no processo de busca pela melhor solução, são geradas diversas boas soluções, ótimos locais em relação aos métodos de busca local utilizados. Tipicamente, mesmo soluções de valores muito próximos podem ter estruturas muito diferentes. O método de pós-otimização proposto tenta combinar algumas das soluções obtidas pelo HGP na tentativa de aproveitar as melhores características de cada uma delas. As soluções utilizadas no pós-processamento são chamadas de soluções de elite , um conceito muito utilizado em métodos de busca tabu (como [3], por exemplo). Duas a duas, as soluções de elite são utilizadas para obter outras soluções, em um processo conhecido como path-relinking (religamento). Foram estudadas duas técnicas de religamento: uma baseada em movimentos de inserção e remoção de vértices de Steiner (movimentos complementares) e outra baseada em perturbações. Na verdade, o método de pós-otimização implementado não se limita a fazer a combinação entre as melhores soluções obtidas pelo HGP. Esse é apenas o seu primeiro passo. As soluções resultantes da combinação constituem uma segunda geração de soluções de elite (a primeira é constituída por soluções geradas nas iterações do HGP). Elementos da segunda geração podem ser combinados novamente entre si, gerando uma terceira geração, e assim sucessivamente. Pode-se dizer que o procedimento tem elementos de um algoritmo genético: cada conjunto de soluções de elite pode ser entendido como uma população. CAPÍTULO 5. METAEURÍSTICAS 69 A Seção 5.5.1 explica em detalhes a construção de cada geração de soluções de elite. Na Seção 5.5.2, discutem-se os métodos implementados para efetuar a combinação de um par de soluções. A Seção 5.5.3 discute a questão dos critérios de parada para a parte genética do algoritmo. 5.5.1 Soluções de Elite A lista de soluções de elite (L) contém as soluções de melhor qualidade (menor valor) encontradas pelo HGP até a iteração corrente, representadas pelo conjunto dos vértices de Steiner que as compõem. No início do algoritmo, a lista não contém soluções, que são adicionadas à medida que o HGP progride. Como apenas os ótimos locais são candidatos a fazer parte da lista soluções de elite, a cada iteração tenta-se inserir no máximo uma solução em L. Como todas as soluções na lista devem ser combinadas entre si, o tempo de execução da fase de pós-processamento é proporcional a jLj . Limitar o valor de jLj a uma constante pequena é conveniente para evitar que o tempo de execução do algoritmo seja excessivo (adotou-se jLj = 10 na maioria dos testes realizados). Dada a limitação do tamanho, é preciso haver um critério para a seleção das soluções que serão inseridas na lista de elite. Para ser inserida, uma solução S deve ter duas características. Em primeiro lugar, deve ser diferente de qualquer outra solução presente na lista. Em segundo lugar, deve ter custo estritamente menor que o da pior solução armazenada até então (a menos, é claro, que ainda haja espaços vazios). A solução de maior valor é retirada da lista para dar lugar à nova se necessário. Esses critérios são observados não só na construção da primeira geração de soluções de elite, mas de todas as demais. Observe que nada impede que haja numa geração soluções que estiveram presentes em gerações anteriores, desde que resultem de cruzamentos (elementos de uma geração não são diretamente inseridos na geração seguinte). Portanto, combinações feitas em uma geração podem ser refeitas em outras. Como alguns dos procedimentos de combinação não são determinísticos, isso não leva necessariamente a uma perda de diversidade. 2 5.5.2 Estratégias de Religamento Foram implementadas três estratégias de religamento: por movimentos complementares (descrita na Seção 5.5.2.1), por perturbações (Seção 5.5.2.2) e um terceiro, adaptativo, resultado da combinação dos anteriores (5.5.2.3). CAPÍTULO 5. METAEURÍSTICAS 70 5.5.2.1 Religamento por Movimentos Complementares Este método, utilizado por Bastos e Ribeiro [3], baseia-se na representação de soluções por vértices de Steiner descrita na Seção 4.1. Soluções são representadas por vetores binários: o valor 1 em uma posição indica que o vértice correspondente faz parte da solução; o valor 0 signica que o vértice não pertence à solução. Diferentes soluções terão diferentes vetores característicos (ou vetores de incidência ). Dadas duas soluções, o religamento por movimentos complementares consiste em transformar uma delas (a solução inicial) na outra (a solução guia) a partir de movimentos sucessivos de inserção e/ou remoção de vértices. Em cada etapa, cada bit diferente entre a solução atual e a solução guia representa um movimento candidato. Se o vértice v está presente na solução atual mas não na guia, ele é um candidato a remoção; se ocorre o contrário (presente na solução guia e ausente na atual), o vértice é candidato a inserção. Os movimentos de inserção e remoção são similares aos utilizados na busca local por nós de Steiner (Seção 4.2.1). Em cada etapa, são testados todos os movimentos possíveis e o melhor deles é escolhido (o que reduz mais o custo da solução ou o que provoca o menor aumento). Assim, a solução inicial gradualmente transforma-se na solução guia. O processo pode ser entendido como uma trajetória por um caminho que liga as duas soluções originais no espaço de busca (isso torna clara a origem do termo path-relinking ). O resultado do religamento é a melhor solução encontrada ao longo do caminho. Fixado um par de soluções, vericou-se experimentalmente que é mais vantajoso utilizar como solução inicial a de menor valor e como guia a de maior valor. Anal, como a vizinhança da solução inicial é explorada mais cuidadosamente pelo método, é interessante que essa seja uma solução de boa qualidade. Um aspecto importante a se considerar nesse método é o número de soluções analisadas. Se as soluções combinadas tiverem d bits de diferença, o caminho entre elas terá exatamente d 1 etapas (soluções) intermediárias. Como em cada uma devem ser testados todos os movimentos candidatos, serão analisados O(d ) vizinhos. Em instâncias cujas soluções têm muitos vértices, o tempo de execução do método pode ser intoleravelmente elevado. Para instâncias menores, no entanto, o método é muito eciente. 2 5.5.2.2 Religamento por Perturbações No religamento por pertubações, a combinação (ou cruzamento) de duas soluções é feita pela alteração aleatorizada dos custos das arestas do grafo original, seguida da aplicação de uma heurística construtiva. Depois de construída a solução, os pesos originais das arestas são restaurados e aplica-se um método de busca local.1 Na implementação realizada, a Quando usado para caracterizar esse método, o termo religamento não deve ser interpretado literalmente. A rigor, trata-se apenas de uma combinação de duas soluções. Alternativamente, pode-se pensar no método como um crossing-over otimizado, utilizando-se a terminologia dos algoritmos genéticos. 1 CAPÍTULO 5. METAEURÍSTICAS 71 estratégia de busca local é a mesma utilizada nas iterações do HGP. Nada impede, no entanto, que se utilize um método distinto. A função de penalização é projetada com o objetivo de gerar soluções que de fato combinem características das duas soluções originais. Cada aresta do grafo original é multiplicada por um número inteiro,2 escolhido de acordo com o número de soluções (0, 1, ou 2) em que ela aparece. Quando a aresta está presente em ambas as soluções, o multiplicador é 1 (ou seja, o peso permanece inalterado); quando está presente em apenas uma, o multiplicador é aleatoriamente escolhido no intervalo [50; 100] com distribuição uniforme de probabilidades; quando não aparece em nenhuma das duas soluções, o multiplicador é 2000. Assim, as arestas que não estão presentes em qualquer das árvores são severamente penalizadas e as que aparecem em ambas as árvores praticamente não recebem penalização. O objetivo é fazer com que o algoritmo construtivo contenha todas (ou quase todas) as arestas comuns às soluções originais e nenhuma aresta que não pertença a alguma das soluções. Arestas presentes em pelo menos uma delas recebem penalizações médias e em um intervalo relativamente grande; dessa forma, espera-se que elas se misturem na composição da nova árvore. 5.5.2.3 Religamento Adaptativo A eciência relativa dos dois métodos de religamento propostos depende das características da instância à qual são aplicados. Tanto o tempo de execução quanto a qualidade das soluções obtidas variam caso a caso. Assim sendo, propõe-se um método adaptativo, que combina ambos os métodos. Seja L a lista de soluções de elite. Inicialmente, utiliza-se o método por movimentos complementares para combinar a melhor solução de L com cada um das demais. Em seguida, repetem-se as mesmas ligações, mas usando o método aleatorizado. Os tempos de execução dessas duas passagens são medidos. O método mais rápido é escolhido para efetuar as demais combinações, sendo utilizado também nas gerações seguintes, se houver. (Em caso de empate no tempo de execução, a preferência é dada ao método aleatorizado.) Esse método privilegia claramente o tempo de execução, mas observações empíricas mostraram que, em geral, essa escolha leva ao método com maior probabilidade de sucesso, pelo menos quando NP é a estratégia de busca local utilizada. Um estudo mais detalhado a esse respeito é apresentado em [50]. O uso de um multiplicador inteiro evita que o programa lide com valores em ponto utuante, o que tenderia a torná-lo mais lento. Evidentemente, essa estratégia tem a desvantagem de não poder ser aplicada a grafos cujos pesos originais são muito altos, já que a possibilidade de overow seria muito grande. 2 CAPÍTULO 5. METAEURÍSTICAS 72 5.5.3 Critérios de Parada Conforme já mencionado, o religamento é feito em diversas iterações. As soluções do pool são combinadas entre si, formando um novo conjunto de soluções (um novo pool ). Diz-se que as novas soluções constituem uma nova geração. Seus elementos são também combinados entre si, formando uma outra geração, e assim sucessivamente. Falta a essa descrição um critério de parada. Se implementado da forma como foi descrito, as gerações poderiam se suceder indenidamente. Entre os critérios possíveis, é natural pensar nos que se baseiam na comparação de uma geração com as anteriores: (B)est : a combinação dos elementos da geração i só será feita se a melhor solução dessa geração tiver valor menor que a melhor solução encontrada em gerações anteriores; (A)verage : a combinação dos elementos da geração i só será feita se a média dos valores das soluções dessa geração for menor que a média dos valores da geração anterior (i 1); (W)orst : a combinação dos elementos da geração i só será feita se a pior solução dessa geração for melhor que a pior solução encontrada na geração anterior. O princípio básico nos três casos é o mesmo: só prosseguir se houver alguma evidência de que o algoritmo está progredindo. Na prática, observou-se que o método B , por ser mais rigoroso, tende a levar a um número menor de iterações que o método A, por exemplo. O método W tem comportamento intermediário. Na maioria dos testes realizados, utilizouse o método B : o algoritmo prossegue até que uma nova geração não possua uma solução melhor que todas as encontradas anteriormente. É possível ainda fazer combinações desses critérios. Na Seção 5.6.2.1, relata-se um experimento em que o algoritmo só é interrompido se nenhum dos critérios for satisfeito, ou seja, se os valores da melhor solução, da pior solução e da média das soluções da geração corrente forem maiores ou iguais aos respectivos valores da geração anterior. Para essa estratégia (que recebe a denominação de ABW ), a qualidade das soluções fornecidas tende a aumentar, pois o número de iterações é quase sempre maior que o obtido quando um critério individual é utilizado. É preciso, no entanto, ter um cuidado especial: se os critérios forem usados de forma estrita, o número de iterações pode ser innito. Sejam dois critérios C e C quaisquer. É possível que, numa determinada geração, o critério C melhore e C piore; isso é suciente para que o algoritmo prossiga. Em uma iteração futura, C pode piorar (assumindo o valor anterior) e C , melhorar. O algoritmo também prossegue nesse caso. É fácil ver que há o risco de formação de ciclo potencialmente innito. Mas é simples evitá-lo: basta armazenar o valor utilizado na última vez em que cada critério foi utilizado. Em iterações futuras, proíbe-se que valores menores sejam usados. 1 1 2 1 2 2 CAPÍTULO 5. METAEURÍSTICAS 73 5.6 Resultados Experimentais A qualidade das soluções fornecidas pelo algoritmo HGP+PR depende, conforme já mencionado, dos parâmetros utilizados. Em especial, a escolha da busca local, o número de iterações, o número de soluções de elite e o critério de parada para o path-relinking têm inuência marcante sobre a qualidade do algoritmo. Um aumento no tempo de execução resultante da alteração de qualquer desses parâmetros tende a melhorar a qualidade das soluções obtidas. Para os experimentos apresentados na Seção 5.6.1, utiliza-se um conjunto de parâmetros de referência arbitrariamente escolhido. A implementação resultante é analisada tanto em termos absolutos quanto em relação a outros algoritmos apresentados na literatura. Em situações práticas, no entanto, a escolha dos parâmetros deve ser feita de acordo com o tempo disponível em cada caso. Na Seção 5.6.2, discute-se a questão da escalabilidade do algoritmo, ou seja, como um aumento no tempo de execução se traduz na prática em soluções de melhor qualidade. Como no caso das buscas locais, foram utilizadas instâncias pré-processadas nos testes envolvendo as classes OR-Library e VLSI. Todas as execuções foram realizadas em um Pentium II de 400 MHz. Os tempos de execução apresentados não incluem os tempos de pré-processamento, que podem ser obtidos no Apêndice A. 5.6.1 Implementação com Parâmetros de Referência São os seguintes os parâmetros utilizados na implementação de referência da metaeurística HGP+PR: heurísticas construtivas : Prim (implementação direta), Kruskal (Kruskal-V), Bor·v ka, DNHz e Multispan. busca local : métodos NP e PN (um deles é escolhido aleatoriamente em cada iteração); estratégia de perturbação : IDU (oscilação estratégica); número de iterações : 128; soluções de elite : 10; path-relinking : adaptativo (seleciona o mais rápido entre os religamentos por movimentos complementares e por perturbações); critério de parada : B (melhor solução). CAPÍTULO 5. METAEURÍSTICAS 74 A Tabela 5.2 mostra a qualidade das soluções obtidas pelo algoritmo HGP+PR com esses parâmetros. Para cada série de instâncias testada, apresentam-se os desvios percentuais médios em relação à melhor solução primal conhecida. O resultado nal do algoritmo (o valor obtido após o path-relinking ) é apresentado na última coluna. As demais colunas apresentam alguns resultados parciais obtidos pelo algoritmo. A coluna 32, por exemplo, apresenta o desvio percentual médio considerando-se, para cada instância, a melhor solução obtida nas 32 primeiras iterações do GRASP. série desvios percentuais 1 2 4 8 16 32 64 128 PR i080 3.003 2.031 1.608 0.772 0.300 0.118 0.048 0.008 0.004 i160 3.049 2.447 1.752 1.019 0.624 0.367 0.308 0.170 0.098 i320 3.391 2.802 2.028 1.425 0.974 0.698 0.522 0.370 0.129 i640 2.918 2.595 1.959 1.561 1.253 0.920 0.792 0.619 0.290 c 0.637 0.154 0.114 0.067 0.047 0.047 0 0 0 d 0.645 0.574 0.557 0.106 0.096 0.081 0.081 0.041 0.017 e 0.476 0.253 0.241 0.194 0.103 0.097 0.083 0.072 0.053 bip 6.928 6.300 5.653 5.227 4.628 4.476 4.157 3.560 2.293 cc 4.070 2.995 2.646 2.374 2.172 2.021 1.848 1.764 0.912 hc 2.520 1.892 1.708 1.545 1.523 1.444 1.420 1.297 0.792 alue 0.705 0.508 0.375 0.276 0.161 0.132 0.091 0.085 0.022 alut 0.642 0.536 0.438 0.279 0.201 0.136 0.096 0.086 0.029 diw 0.375 0.159 0.093 0.027 0.027 0.027 0.027 0 0 dmxa 0.511 0.395 0.211 0.025 0 0 0 0 0 gap 0.577 0.449 0.012 0.012 0.012 0.012 0 0 0 msm 0.427 0.156 0.156 0.059 0.005 0 0 0 0 taq 0.398 0.179 0.179 0.139 0.131 0.062 0.039 0.033 0.021 Tabela 5.2: Qualidade das soluções obtidas ao longo da execução da implementação de referência A coluna 1 da tabela representa a primeira iteração do GRASP, que nada mais é que o algoritmo de Prim seguido de uma busca local (lembre-se de que não há perturbações na primeira iteração). Observe que o desvio médio diminui signicativamente ao longo do algoritmo, o que demonstra a eciência da metaeurística. Evidentemente, isso se reete em um aumento signicativo nos tempos de execução do algoritmo, apresentados na Tabela 5.3 (compare com a Tabela 4.4). Anal, é necessário executar um procedimento completo de busca local em cada iteração. Comparação com outros métodos A melhor maneira de avaliar a qualidade dos resultados obtidos é compará-los com os obtidos por outras heurísticas apresentadas na literatura. Serão considerados os seguintes algoritmos: CAPÍTULO 5. METAEURÍSTICAS 75 série tempo (s) HGP PR Total i080 2.00 0.14 2.14 i160 9.54 0.57 10.12 i320 54.04 2.89 56.94 i640 345.31 17.05 362.38 c 1.88 0.25 2.13 d 6.93 0.98 7.91 e 47.73 11.47 59.20 bip 371.06 134.22 505.29 cc 571.75 703.49 1275.25 hc 839.30 1335.36 2174.67 alue 1653.52 1909.41 3562.93 alut 1775.44 1886.44 3661.89 diw 9.36 2.00 11.36 dmxa 0.79 0.14 0.93 gap 7.29 1.45 8.74 msm 2.12 0.49 2.61 taq 36.69 11.81 48.50 Tabela 5.3: Tempos médios de execução da implementação de referência RTS: Busca tabu reativa apresentada por Bastos e Ribeiro em [3]. O mesmo artigo apresenta a variante RTS+PR, que utiliza o religamento por movimentos complementares descrito na Seção 5.5.2.1. TS: Busca tabu de Ribeiro e Souza [49]. FT: Busca tabu de Gendreau et al. [21]. SV: Procedimento SVertex+ReBuild de Duin e Voss [15]. PH2: Método Piloto de Duin e Voss, na versão apresentada em [16] que leva a soluções de melhor qualidade.3 A comparação se baseia na análise de apenas seis séries de instâncias: c, d, e, i080, i160 e i320. As demais séries não foram testadas pelos autores dos algoritmos de referência. Quanto à qualidade das soluções fornecidas, utilizam-se duas medidas para comparar os métodos: desvios em relação ao ótimo (Tabela 5.4) e número de soluções ótimas encontradas (Tabela 5.5). Nessas tabelas, é bom lembrar que as séries c, d e e têm 20 instâncias cada, enquanto as demais têm 100 instâncias cada. 3 Os valores aqui apresentados para esse algoritmo referem-se não ao artigo original, mas a execuções do algoritmo feitas por C. Duin em abril de 2001 [13]. CAPÍTULO 5. METAEURÍSTICAS 76 Para alguns dos algoritmos testados, os valores para a série i320 são estimados (limites superiores para os desvios e inferiores para o número de ótimos), já que nem todos os ótimos dessa série eram conhecidos na época em que os respectivos artigos foram publicados ou submetidos para publicação. A série só foi completamente resolvida em 2001 por Poggi de Aragão, Uchoa e Werneck [44]; o algoritmo utilizado para isso (branch-and-ascent ) será apresentado detalhadamente no Capítulo 7. série HGP HGP+PR RTS RTS+PR TS FT SV c 0 0 0.13 0.01 0.26 0.02 0.54 d 0.041 0.017 0.36 0.10 0.71 0.11 0.75 e 0.072 0.032 0.59 0.17 0.83 0.31 OR-Library 0.038 0.016 0.36 0.09 0.60 0.15 i080 0.008 0.004 0.02 0.01 0.08 1.03 i160 0.170 0.098 0.19 0.12 0.35 0.98 i320 0.370 0.129 0.48 0.34 0.89 1.20 Incidência 0.183 0.077 0.23 0.16 0.44 1.07 Tabela 5.4: Metaeurísticas: desvios relativos percentuais médios série HGP HGP+PR c 20 20 d 18 19 e 16 17 OR-Lib 54 56 i080 96 97 i160 75 81 i320 61 67 Incidence 232 245 Tabela 5.5: Metaeurísticas: PH2 0.02 0.04 0.04 0.03 RTS RTS-PR TS FT SV PH2 17.7 19.8 17 18 14.2 16.7 9 14 11.6 13.6 7 10 43.5 50.1 33 42 96 99 89 96 73 81 58 90 56 64 39 86 225 244 186 272 número de soluções ótimas encontradas Os tempos médios de execução dos algoritmos são apresentados na Tabela 5.6. Os valores são apresentados apenas como referência, já que as máquinas utilizadas são muito diferentes. Os algoritmos HGP e HGP+PR foram executados em um Pentium II de 400 MHz. O mesmo ocorreu com os algoritmos RTS e RTS+PR para instâncias de Incidência; para a OR-Library, uma Sun Ultra 1 de 167 MHz. Máquinas do mesmo tipo foram utilizadas para testar os algoritmos FT e TS. O método SV foi executado em um Pentium de 90 MHz. Finalmente, os tempos para o algoritmo PH2 foram obtidos em um Intel Celeron de 500 MHz. Os tempos não incluem o pré-processamento, usado por todas os métodos para as instâncias da OR-Library. CAPÍTULO 5. METAEURÍSTICAS 77 série HGP HGP+PR RTS RTS+PR TS FT SV PH2 c 1.88 2.13 2.0 3.0 1.0 18.0 d 6.93 7.91 5.3 12.5 3.9 115.0 e 47.73 59.20 21.3 157.2 17.6 1474.0 OR-Library 18.85 23.08 9.6 57.6 7.5 535.7 i080 2.00 2.14 5.8 6.2 3.3 0.4 1.1 i160 9.54 10.12 28.1 29.9 12.2 2.7 11.1 i320 54.04 56.94 162.6 168.1 50.5 14.8 140.4 Incidência 21.86 23.07 65.5 68.1 22.0 6.0 50.9 Tabela 5.6: Metaeurísticas: tempos de execução em segundos (com diferentes máquinas) As tabelas mostram que os algoritmos HGP e HGP+PR apresentam resultados de excelente qualidade quando comparado a outras metaeurísticas. Para as instâncias da OR-Library, a qualidade é superior às fornecidas por todos os demais métodos. Já para as instâncias de Incidência, a busca tabu reativa (RTS e RTS+PR) é capaz de fornecer soluções de qualidade semelhante; o Método Piloto (PH2) apresenta resultados ligeiramente melhores. Note, porém, que tanto a busca tabu reativa quanto o Método Piloto apresentam tempos de execução maiores (para execuções em máquinas equivalentes ou superiores às usadas pelo método HGP+PR). A diferença não é muito signicativa, mas existe. 5.6.2 Escalabilidade Os resultados experimentais apresentados até aqui referem-se a execuções do algoritmo HGP+PR com o conjunto de parâmetros de referência: 128 iterações, 10 soluções de elite, busca local NP /PN, etc. Apesar de o algoritmo ter um bom desempenho quando executado dessa forma, há situações em que se está disposto a gastar um tempo ainda maior para encontrar soluções de boa qualidade. Esta seção mostra que, em caso de necessidade, isso pode ser feito com sucesso. 5.6.2.1 Parâmetros Vários dos parâmetros do algoritmo têm clara inuência sobre a qualidade das soluções obtidas (e sobre o tempo de execução). Os mais evidentes são o número de iterações e o de soluções de elite. O primeiro se reete linearmente no tempo de execução (quando o número de iterações dobra, também aproximadamente dobra o tempo dedicado ao GRASP). O segundo, o número de soluções de elite, tem inuência quadrática sobre o tempo de execução (quando dobra o número de soluções de elite, o número de cruzamentos feitos em cada geração é multiplicado por quatro, aproximadamente). Em ambos os casos, há uma inuência indireta sobre o número de gerações criadas no religamento, que pode CAPÍTULO 5. METAEURÍSTICAS 78 diminuir ou, mais provavelmente, aumentar. A Tabela 5.2 mostra que o efeito da adição de novas iterações de GRASP tende a ser menos perceptível que o de um path-relinking mais cuidadoso (basta comparar o que ocorre quando o número de iterações aumenta de 64 para 128 com o que ocorre quando se aplica o path-relinking às soluções obtidas após a iteração 128). Se houver mais tempo disponível, portanto, parece ser mais vantajoso usá-lo para aumentar a eciência do pathrelinking. Para vericar que execuções mais longas de fato levam a melhores resultados, observe os resultados mostrados na Tabela 5.7. Ele mostra o resultado da execução do algoritmo HGP+PR com as mesmas heurísticas construtivas e buscas locais da implementação de referência, mas com os demais parâmetros alterados. Foram executadas 256 iterações de GRASP em cada caso, o dobro do valor original. O path-relinking, além de contar com 32 soluções de elite (o valor de referência é 10), tem duas outras características diferentes: o critério de parada é ABW (veja a discussão na Seção 5.5.3) e o religamento é feito sempre utilizando o método por perturbações (na implementação de referência, o método de parada é B e o religamento, adaptativo). série desvio (%) ótimos tempo (s) HGP HGP+PR HGP HGP+PR HGP HGP+PR i080 0.003 0 98 100 4.08 12.78 i160 0.129 0.033 80 92 19.48 84.62 i320 0.258 0.018 66 85 111.80 503.26 Total 0.130 0.017 244 277 45.12 200.22 Tabela 5.7: Execuções longas para instâncias de incidência (256 iterações, 32 soluções de elite e religamento por perturbações com critério de parada ABW ) Quando se comparam esses resultados com os obtidos pela implementação de referência (colunas HGP e HGP+PR das Tabelas 5.4, 5.5 e 5.6), é possível vericar claramente alguns aspectos do funcionamento do algoritmo. Os dados para os algoritmos sem pathrelinking (HGP) mostram que dobrar o número de execuções de fato dobra o tempo médio de execução (que passa de 21.86s para 45.12s), mas leva a melhores soluções: o erro relativo médio para as três séries passou de 0.183% para 0.130%. Observe, no entanto, que um desempenho ainda melhor foi alcançado simplesmente utilizando-se o path-relinking adaptativo após apenas 128 iterações de GRASP (na média, erro relativo de 0.077% em 23.07s, conforme mostra a Tabela 5.4). Isso demonstra mais uma vez a ecácia do religamento. Soluções signicativamente melhores são obtidas quando após as 256 iterações de GRASP se executa o path-relinking reforçado (por perturbações, com 32 soluções de elite e critério de parada ABW ). A Tabela 5.7 mostra que o erro relativo é de apenas 0.017%. É claro que isso tem um custo: o tempo médio de execução é de mais de 200 segundos, muito maior que nos outros casos. CAPÍTULO 5. METAEURÍSTICAS 79 De certa forma, pode-se considerar que a parametrização utilizada para construir a Tabela 5.7 caracteriza um algoritmo fundamentalmente diferente do mostrado pela implementação de referência. Na parametrização original, apenas cerca de 5% do tempo de execução é dedicado ao religamento (veja Tabela 5.6). Na nova parametrização a ênfase do algoritmo é outra: mais de 75% do tempo é dedicado ao path-relinking. As gerações são maiores (32 soluções), os cruzamentos são mais lentos (para essa classe de instâncias, o religamento por movimentos complementares é mais rápido) e o número de gerações é maior (entre outros fatores, porque o critério de parada ABW é menos rígido). Apenas como ilustração, considere o que ocorre com a série i320: com a implementação de referência, o número de gerações varia de 1 a 5 (1.58 na média); com a nova implementação, o intervalo se amplia para valores de 1 a 16 (5.24 na média). Um aumento expressivo. 5.6.2.2 Outras Sub-rotinas Uma das principais características da metaeurística proposta é permitir a adição de novos componentes de forma muito simples. Um dos elementos que podem contribuir mais signicativamente para aumentar a qualidade das soluções obtidas é a estratégia de busca local. Nas versões do algoritmo HGP estudadas até aqui, utilizaram-se apenas as buscas por nós de Steiner (N ) e por caminhos-chaves (P ). No entanto, conforme já demonstrado no Capítulo 4, a qualidade das soluções obtidas tende a ser maior se for utilizada também a busca por nós-chaves (K ). O custo, no entanto, é alto: um aumento de uma a duas ordens de grandeza no tempo de execução do algoritmo. Considere o caso das instâncias PUC. A Tabela 5.2 mostra que o método HGP+PR com as buscas NP e PN obtém desvios médios de 2.293% (série bip), 0.912% (série cc) e 0.792% (hc). Como os valores ótimos das soluções para a maioria das instâncias nessas séries não são conhecidos, os desvios são calculados em relação às melhores soluções primais já encontradas, apresentadas em [51]. Essas soluções primais de referência foram obtidas justamente com o algortimo HGP+PR utilizando a busca NPK. O desvios médios acima mostram uma nítida diferença na qualidade. (Note porém que ela não se deve apenas à busca K : em muitos casos foram utilizadas mais iterações de GRASP ou soluções de elite. Mas a busca K sem dúvida tem papel importantíssimo.) Trata-se de um excelente exemplo de situação para a qual o algoritmo HGP+PR pode ser utilizado. O objetivo do artigo [51] era justamente introduzir instâncias que fossem consideradas difíceis tanto para heurísticas primais quanto para algoritmos exatos. Um dos aspectos que torna uma instância difícil é a diferença percentual entre um bom limite inferior para o valor de sua solução (obtido por programação linear, conforme se verá no Capítulo 6) e a solução ótima. Para a maioria das instâncias, no entanto, foi impossível calcular uma solução ótima (anal, trata-se de instâncias difíceis). Fez-se necessário utilizar uma heurística para calcular soluções primais tão boas quanto possível. Utilizou-se o algoritmo HGP+PR com a busca NPK. Os demais parâmetros variaram de acordo com a instância. Tipicamente, o tempo de execução foi de vários dias, o que, para essa aplicação CAPÍTULO 5. METAEURÍSTICAS 80 é perfeitamente tolerável. 5.7 Conclusão Nos algoritmos aqui apresentados, HGP e HGP+PR, componentes básicos muito simples (heurísticas construtivas e métodos de busca local) são integrados por uma superestrutura comum. O princípio básico utilizado para a integração é um só: utilizar perturbações. Elas surgem basicamente para introduzir um elemento de aleatorização nas heurísticas construtivas do GRASP e para permitir a combinação de soluções na fase de religamento. Esses dois objetivos podem ser alcançados sem o uso de perturbações. Em um GRASP puro, a aleatorização é conseguida alterando-se o código das heurísticas construtivas utilizadas. O path-relinking pode ser feito, por exemplo, utilizando-se movimentos complementares. No entanto, o uso de perturbações tem algumas vantagens inerentes: Simplicidade de implementação. Perturbar os pesos das arestas de um grafo é mais simples do que criar novos métodos construtivos ou algoritmos para efetuar o religamento de forma explícita. Eciência. Aleatorizar diretamente um algoritmo construtivo em muitos casos signica torná-lo mais lento, pois pode requerer o uso de estruturas mais complexas (é esse o caso do algoritmo de Prim, por exemplo); o uso de perturbações permite utilizar as melhores implementações de cada método. Qualidade. Em um GRASP puro, a aleatorização de uma heurística gulosa se faz pela criação de uma lista de candidatos em cada etapa do algoritmo. Em vez de se escolher apenas o elemento de maior prioridade, escolhe-se um dentre os mais prioritários. Em muitos casos, obter uma aleatorização razoável requer o uso de listas extremamente grandes, o que tende a piorar muito a qualidade das soluções construtivas. Métodos de busca local aplicados sobre essas soluções tendem não só a ser mais lentos, mas também a obter soluções de pior qualidade. Uso de estratégias de busca elaboradas. O uso de perturbações permite que se implementem de forma de forma muito simples estratégias elaboradas para percorrer o espaço de soluções, como intensicação, diversicação e oscilação estratégica. Flexibilidade. A adição ou substituição de elementos no HGP+PR é extremamente simples, pois não requer qualquer alteração na estrutura dos algoritmos entre si. Novos métodos construtivos e novas formas de busca local podem ser integrados tanto à primeira fase do algoritmo (HGP) quanto à fase de religamento. Para isso não é necessário sequer conhecer o princípio de funcionamento dos componentes adicionados, que podem ser utilizados como caixas-pretas. CAPÍTULO 5. METAEURÍSTICAS 81 Evidentemente, de nada adiantariam essas vantagens se os resultados fornecidos pela heurística HGP+PR não fossem competitivos. O algoritmo não só fornece soluções de excelente qualidade, mas o faz para uma grande variedade de instâncias. Trata-se de um método muito robusto. Para várias das instâncias em aberto da literatura (da série i640, por exemplo), os melhores valores conhecidos foram obtidos por esse algoritmo [33]. Capítulo 6 Algoritmos Duais Os algoritmos apresentados nos capítulos anteriores são capazes de obter soluções de excelente qualidade para instâncias do problema de Steiner em grafos. Contudo, nenhum deles é capaz de fornecer garantias relevantes para a qualidade da solução obtida. Na prática, isso signica que, após a execução de um desses algoritmos, não se sabe ao certo se a solução fornecida pode ou não ser melhorada, e em quanto pode ser melhorada. Uma solução primal viável (uma árvore de Steiner) fornece apenas um limite superior para o valor da solução ótima do problema. Para que se possa fornecer uma garantia de qualidade para essa solução, é necessário que se conheça também algum limite inferior. A diferença percentual entre os limites inferior e superior é uma estimativa (de pior caso) para a qualidade da solução fornecida. O problema de Steiner em grafos pode ser formulado como um problema de programação linear inteira. Removendo-se as restrições de integralidade, obtém-se sua relaxação linear , que pode ser resolvida em tempo polinomial, conforme se verá na Seção 6.1. Em um problema de minimização (como é o de Steiner), a remoção de qualquer restrição só pode diminuir o valor da solução obtida (ou mantê-lo inalterado, se a restrição for redundante). Portanto, a solução da relaxação linear do problema é sempre um limite inferior para o valor de sua solução ótima. Para o problema de Steiner em grafos, há formulações lineares muito fortes: a diferença percentual entre o valor da solução ótima (inteira) e a solução da relaxação (o gap de dualidade ) é normalmente muito pequena, muitas vezes nula. No entanto, mesmo sendo um problema polinomial, o cálculo exato da relaxação linear pode ser extremamente custoso na prática. Em certas situações, torna-se interessante calcular apenas soluções aproximadas para a relaxação linear. É justamente disso que trata este capítulo: heurísticas combinatórias baseadas no dual da relaxação linear da formulação do problema de Steiner como um problema de programação linear inteira. A formulação utilizada pelos algoritmos é discutida em detalhes na Seção 6.1. A Seção 6.2 descreve uma heurística construtiva para a obtenção de soluções duais (dual 82 CAPÍTULO 6. ALGORITMOS DUAIS 83 ascent ) proposta por Wong em [65]. A Seção 6.3 introduz alguns métodos derivados dessa heurística que têm o objetivo de melhorar a qualidade das soluções obtidas. Por m, a Seção 6.4 contém alguns comentários nais sobre o uso de heurísticas duais. Versões preliminares dos algoritmos apresentados neste capítulo (e no próximo) foram apresentadas em [44], artigo escrito em co-autoria com Marcus Poggi de Aragão e Eduardo Uchoa. A discussão apresentada neste capítulo requer o conhecimento dos princípios básicos de programação linear inteira e de seu uso para problemas de otimização combinatória. Referências relevantes para o assunto são [42] e [64]. 6.1 Formulação e Dualidade 6.1.1 Formulação Primal O problema de Steiner de grafos pode ser formulado como um problema de programação inteira de diversas maneiras diferentes (veja [22, 36, 45] para análises comparativas). Na prática, uma das mais freqüentemente utilizadas é a baseada em cortes direcionados, discutida em [65]. Muitos dos mais ecazes algoritmos exatos para o problema de Steiner utilizam essa formulação [32, 46, 58]. Será essa a formulação utilizada pelos algoritmos estudados neste e no próximo capítulo. A formulação é válida para a versão direcionada do problema de Steiner em grafos (em que objetivo é encontrar uma arborescência de peso mínimo a partir de uma raiz r), mas pode ser normalmente aplicada para o caso não-direcionado. Para isso, basta transformar cada aresta (v; w) do grafo original em dois arcos antiparalelos de mesmo custo no grafo direcionado (v; w) e (w; v) e xar um terminal qualquer como raiz. Portanto, sem perda de generalidade, será utilizada a notação G = (V; A) para representar um grafo direcionado de entrada, sendo V o conjunto de vértices e A o de arcos. O conjunto T , como sempre, representa o conjunto de vértices terminais. Dado um subconjunto próprio W dos vértices de um grafo direcionado G(V; A) (ou seja, W V ), dene-se o corte direcionado (W ) como o conjunto dos arcos (u; v) 2 A tais que u 62 W e v 2 W . Em outras palavras, pertencem a (W ) todos os arcos com cauda (origem) em V n W e cabeça em W ( (W ) é denido de forma análoga, mas considerando os arcos que saem de W ). Dada uma instância do problema de Steiner em grafos, seja W o conjunto composto por todos os conjuntos de vértices que determinam cortes que separam a raiz r de algum outro terminal. Em outras palavras, W 2 W se W contém pelo menos um terminal e não contém a raiz r. A formulação baseada em cortes direcionados para o problema de Steiner em grafos exige apenas que, para todo W 2 W , pelo menos um arco de (W ) faça parte da solução. Isso é suciente para garantir que a solução ótima (inteira) será + CAPÍTULO 6. ALGORITMOS DUAIS 84 uma arborescência. Associando-se uma variável xa a cada arco e sendo c(a) seu custo, essa formulação pode ser expressa formalmente como: X 8 >> min c(a) xa >> a2A X >> < s.a. xa 1 (Pcd) > a2 W >> xa 0 >> >: xa inteiro ( ) (1) 8 W 2 W (2) 8a2A 8a2A (3) (4) A expressão (1) representa a função objetivo: a soma dos custos dos arcos selecionados (nesta dissertação, considera-se que os custos são sempre inteiros, mas a mesma formulação pode ser utilizada se não for esse o caso). As restrições do tipo (2) representam os cortes direcionados. As restrições (3) e (4) são equivalentes a requerer que as variáveis x sejam binárias (como se trata de um problema de minimização e os custos são positivos, nenhuma variável terá valor maior que 1). Repare, no entanto, que o número de restrições do tipo (2) dessa formulação é igual ao número de elementos em W , exponencial no pior caso. Assim sendo, pode não ser possível resolver a relaxação linear de forma direta. O que normalmente se faz é resolver inicialmente um programa linear utilizando um subconjunto de W , muitas vezes vazio. Executa-se então uma rotina de separação , cuja função é vericar se a solução obtida obedece a todas as restrições determinadas por W e, caso isso não ocorra, apresentar restrições violadas. As restrições encontradas são introduzidas no programa linear, que é reotimizado. O processo prossegue até que a rotina de separação ateste que a solução encontrada não viola nenhuma restrição. Uma propriedade importante da programação linear garante que, se um problema tiver uma rotina de separação polinomial, então ele pode ser resolvido em tempo polinomial, mesmo que tenha um número exponencial de restrições [25, 26]. É esse o caso da formulação Pcd do problema de Steiner em grafos: ela consiste em encontrar cortes de custo mínimo entre a raiz e todos os demais terminais do grafo usando os valores de x como custos dos arcos. Esse problema pode ser resolvido em tempo polinomial (utilizando-se, por exemplo, o algoritmo introduzido em [27] por Hao e Orlin). Assim, resolver o programa inteiro Pcd é um problema NP-difícil, mas resolver sua relaxação linear resultado da remoção das restrições de integralidade (4) é um problema polinomial. 6.1.2 Formulação Dual Essencial para a programação linear é o conceito de dualidade , discutido, entre outros, em [7]. Para todo programa linear, existe um programa dual correspondente (que tem o mesmo valor ótimo). No caso do problema de Steiner em grafos, o dual da relaxação CAPÍTULO 6. ALGORITMOS DUAIS 85 linear da formulação (Pcd) é o seguinte: X 8 >> max W 2W W >< X W c(a) 8a 2 A (1) (Dcd) > s.a. >> W 2W a2 W >: W 0 8W 2 W (2) Associa-se uma variável dual W a cada corte direcionado associado a W (o número : ( ) de variáveis é exponencial, portanto). O problema consiste em maximizar a soma das variáveis duais. A restrição básica (além da não-negatividade) é a de que a soma das variáveis duais associadas aos cortes que contêm um arco a não pode ser superior ao custo do próprio arco (c(a)). Essas mesmas restrições podem ser descritas de forma mais compacta se for utilizado o conceito de custo reduzido. O custo reduzido c (a) de um arco1 a em relação a uma solução dual é denido como: X c (a) = c(a) W : W 2W :a2 (W ) As restrições mencionadas apenas impõem que os custos reduzidos de todos os arcos sejam não-negativos. Um conceito importante associado à denição de custos reduzidos é o de saturação . Um arco é dito saturado se seu custo reduzido for zero. A existência de um arco com custo reduzido negativo em uma solução indica que alguma restrição foi violada; nesse caso, diz-se que é inviável. 6.1.3 Complementaridade de Folga A formulação do problema de Steiner como um programa linear nos permite utilizar alguns teoremas conhecidos em programação linear (veja, por exemplo, [7]). Em primeiro lugar, sendo a formulação Dcd o dual da relaxação linear da formulação Pcd, pode-se garantir que as soluções ótimas dos dois problemas têm exatamente o mesmo valor. Além disso, a partir de uma solução primal ótima (possivelmente fracionária) x = fx ; x; : : : ; xjAjg, é possível g com as seguintes propriedades, obter uma solução dual ótima = f; ; : : : ; jWj conhecidas como condições de complementaridade de folga : 1 1 2 2 Propriedade 1 Para qualquer arco a, pelo menos uma das seguintes condições deve ser válida: P W 2W :a2 (W ) W = c(a); ou 1 Quando se menciona o custo reduzido de um arco, deve-se ler o custo reduzido da variável associada a um arco. CAPÍTULO 6. ALGORITMOS DUAIS 86 xa = 0. Propriedade 2 Para toda variável dual W , ao menos uma das seguintes condições é válida: P xa = 1; ou a2 (W ) W = 0. De acordo com a primeira propriedade, para que a tenha valor primal positivo (xa > 0), ele obrigatoriamente deve estar saturado. Pela segunda propriedade, se uma variável dual tiver valor positivo, os arcos que fazem parte do corte que ela representa têm valor primal total igual a 1. Considere uma instância para o problema de Steiner em grafos com gap de dualidade nulo. Nesse caso, existe uma solução inteira para a relaxação linear de Pcd: todo arco a tem associada a si uma variável xa de valor 0 ou 1, exatamente. Os arcos de valor 1 compõem uma arborescência de Steiner. De acordo com a Propriedade 2, isso signica que a solução dual correspondente é tal que todos os cortes de custo dual positivo interceptam essa aborescência uma única vez. Esse fato será importante para a discussão do dual adjustment , feita na Seção 6.3.2. 6.1.4 Fixação por Custos Reduzidos A nalidade básica para o cálculo de soluções duais para um problema de minimização é determinar um limite inferior para o problema e, dessa forma, vericar a qualidade da melhor solução (primal) conhecida. No entanto, podem-se utilizar as soluções duais viáveis fornecidas por esses métodos para, em conjunto com limites superiores (primais), reduzir o tamanho da instância processada. Trata-se da xação por custos reduzidos, discutida, por exemplo, em [64]. Esta seção mostra como é feita a xação no caso do problema de Steiner em grafos. Dada uma instância do problema de Steiner em grafos, seja uma solução dual viável de valor v() e seja Zinc o valor da melhor solução primal conhecida. Então, pode-se xar a variável xa em zero (i.e., eliminar o arco a) se a seguinte condição for observada: v() + c (a) > Zinc 1: (6.1) A xação em zero no caso do problema de Steiner baseia-se numa redução ao absurdo. Inicialmente, supõe-se que a está na solução ótima, o que equivale a adicionar a restrição xa 1 ao problema. Se isso for de fato verdade, pode-se então aumentar a variável dual associada a essa restrição em até c (a) unidades sem tornar inviável a solução dual. O custo da solução passa de v() para v() + c (a). Se esse valor não for menor que o da CAPÍTULO 6. ALGORITMOS DUAIS 87 melhor solução primal conhecida, chega-se a uma contradição: um limite inferior pior que a melhor solução conhecida. O termo 1 na Condição 6.1 pode ser utilizado devido à integralidade da função objetivo. Conforme já observado em [12] e [46], a estrutura do problema de Steiner em grafos pode ser usada para que se obtenham condições mais favoráveis à xação, tornando maiores as chances de que ela ocorra. A suposição de que a está na solução ótima na verdade tem outras conseqüências. Se a = (u; v) de fato zer parte de uma arborescência, as seguintes condições devem ser satisfeitas: 1. deve haver um caminho na arborescência entre r (a raiz) e u; 2. se v não for um terminal, deve haver um caminho na arborescência entre v e algum terminal t (que não seja a raiz). Ambas as condições derivam imediatamente do fato de que todos os arcos de uma arborescência de Steiner mínima devem fazer parte de um caminho entre a raiz r e algum terminal. A Figura 6.1 ilustra as condições. -u r v -r r -t u u Figura 6.1: Condição para a xação de (u; v) por custo reduzido 6.1.4.1 Chegada Concentremo-nos na primeira condição: a existência de um caminho na arborescência entre a r e u (um caminho que chega ao arco). Em termos da formulação como programa linear, isso signica que existe um caminho Pru = (a ; a ; : : : ; ak ) em que todos os arcos estão associados a variáveis de valor 1 (x(a ); x(a ); : : : ; x(ak )). Para que isso ocorra, é necessário que todos os arcos nesse caminho estejam saturados, i.e., tenham custo reduzido zero (caso contrário, devido às condições de complementaridade de folga apresentadas na Seção 6.1.3, os valores das variáveis associadas não poderão ser maiores que 0). É conveniente neste ponto denir o grafo auxiliar G , denido a partir do grafo de entrada G e de uma solução dual viável . O grafo G é o subgrafo de G que contém todos os seus vértices, mas apenas as arestas saturadas em relação a (G é o grafo de 0 0 1 1 CAPÍTULO 6. ALGORITMOS DUAIS 88 saturação associado a G e ). Pode-se exigir, portanto, que o caminho Pru esteja contido em G . Dada uma solução dual arbitrária, nada garante que haverá tal caminho; u pode não ser alcançável em G a partir da raiz r. No entanto, a suposição inicial feita na xação por custos reduzidos, de que a pertence à solução ótima, exige a existência desse caminho. Conseqüentemente, podem ser adicionados novos cortes (restrições) à formulação (com custo dual positivo) separando r de u; adicionam-se tantos cortes quantos forem necessários, até que se crie um caminho de custo reduzido total igual a zero entre r e u. Seja Qru a soma dos valores duais adicionados nesse processo. O teste de xação por custos reduzidos (Condição 6.1) passa então a conter um fator adicional: v() + c (u; v) + Qru > Zinc 1: (6.2) Como esse fator é positivo, a chance de a desigualdade acima se vericar é maior que a da desigualdade original. Portanto, é maior a probabilidade de que a seja eliminado. A probabilidade de eliminação será máxima se Qru for maximizado. Para isso, é necessário encontrar o conjunto de cortes separando r de u cuja soma dos respectivos valores duais seja máxima. Isso é equivalente a encontrar o caminho mínimo entre r e u em G usando como pesos os custos reduzidos dos arcos em relação a . Anal, o problema do caminho mínimo entre dois vértices é dual ao problema do empacotamento de cortes entre dois vértices [2]. 6.1.4.2 Saída Pode-se também exigir a existência de um caminho entre v e algum terminal t em G , o grafo de arcos saturados. Se no grafo associado à solução dual original não houver um tal caminho, novos cortes podem ser adicionados à formulação. Sendo Qvt a soma dos custos dos novos cortes, a Condição 6.1 pode ser estendida para: v() + c (u; v) + Qvt > Zinc 1: (6.3) O valor de máximo de Qvt pode ser determinado de forma similar à discutida na seção anterior: calculando-se em G o caminho mínimo (em relação aos custos reduzidos associados à solução dual original) de v ao terminal t mais próximo. 6.1.4.3 Combinação de Chegada e Saída Simplesmente combinando-se as condições 6.2 e 6.3, obtém-se a seguinte condição válida: v() + c (u; v) + max(Qru; Qvt ) > Zinc 1: (6.4) O termo max(Qru; Qvt ) indica que pode ser usado o caminho mais longo entre os obtidos segundo os procedimentos descritos nas seções 6.1.4.1 e 6.1.4.2. CAPÍTULO 6. ALGORITMOS DUAIS 89 Surge aqui uma questão óbvia. Não seria possível utilizar a soma dos custos desses caminhos? Nem sempre. Nada garante que não há interseção entre os cortes (implicitamente) adicionados no cálculo de Qru e os (implicitamente) adicionados no cálculo de Qvt . Se houver alguma interseção, perde-se a garantia de viabilidade da solução dual resultante. Há, no entanto, uma forma de combinar as duas estratégias. Para garantir a viabilidade da solução dual, basta que os custos reduzidos de todos os arcos sejam atualizados entre o cálculo de um dos caminhos mínimos (ru) e o cálculo do outro (vt).2 Para que isso seja feito de forma eciente, é conveniente modicar ligeiramente o algoritmo de Dijkstra. É ele o algoritmo utilizado para determinar Qru e Qvt isoladamente, usando os custos reduzidos como pesos dos arcos. A idéia para melhorar a Condição 6.4 é, ao invés de realizar duas execuções independentes do algoritmo de Dijkstra, fazer a segunda execução depender da primeira. É importante neste ponto relembrar os princípios básicos do algoritmo de Dijkstra (para uma discussão mais completa veja [2], por exemplo). Suponha que se queira encontrar um caminho mínimo entre uma fonte (origem) s = v e todos os demais nós de um grafo com n vértices (v ; v ; : : : ; vn). O algoritmo de Dijkstra baseia-se na atribuição aos nós de potenciais (p ; p ; : : : ; pn) que representam limites superiores para sua distância à origem. Obviamente, p = 0, já que s = v é a origem. Inicialmente innitos, os potenciais dos demais nós são progressivamente reduzidos ao longo da execução do algoritmo; ao nal dela, os potenciais representarão as distâncias exatas de cada nó à origem. Em um ponto qualquer da execução, seja S o conjunto dos nós cujos potenciais já foram xados, ou seja, para os quais o valor do potencial é igual ao valor da distância. No início, S = fsg. Em cada iteração do algoritmo, o potencial de um certo vértice vi é xado e esse nó passa a fazer parte de S . O nó escolhido (vi) é justamente aquele, entre todos os nós que não pertencem a S , que é separado de S pelo corte de valor mínimo. O valor desse corte é a diferença entre o potencial de vi (pi ) e o potencial do nó inserido em S na rodada imediatamente anterior. Ao nal da execução do algoritmo, serão conhecidos os potenciais nais de todos os vértices: p ; p ; : : : ; pn. Pode-se provar o seguinte resultado: 1 2 1 3 2 1 1 1 2 Teorema 3 Seja aij um arco de custo cij e sejam pi e pj os potenciais nais encontrados pelo algoritmo de Dijkstra para a cauda (vi) e a cabeça (vj ) de aij . O custo reduzido cij desse arco em relação aos cortes introduzidos pelo algoritmo de Dijkstra é dado por cij = cij maxf0; pj pig. Prova No algoritmo de Dijkstra, todo corte (implicitamente) adicionado separa o conjunto S do restante do grafo. Como no conjunto S são apenas inseridos novos vértices ao longo da execução do algoritmo, os cortes estão divididos em camadas. Interceptarão o arco aij os cortes adicionados em iterações em que vi zer parte de S e vj , não (isso decorre imediatamente da denição de corte direcionado). O custo reduzido de aij será 2 Assume-se aqui que os caminhos são calculados nessa ordem ( antes de ). O cálculo na ordem inversa também é possível, com algumas poucas modicações nos procedimentos descritos na seqüência. ru vt CAPÍTULO 6. ALGORITMOS DUAIS 90 simplesmente cij = cij sij , sendo sij a soma dos custos dos cortes adicionados após a inserção de vi até a inserção de vj . Resta calcular o valor de sij . Considere inicialmente o caso em que vj é inserido em S antes de vi. Quando isso ocorre, nenhum corte interceptará aij e, portanto, sij = 0. Já no outro caso, em que vi é inserido em S antes de vj , todos os cortes adicionados entre as duas inserções interceptarão o arco aij . O valor dual associado a cada corte é igual à diferença de potencial entre o novo nó e o nó inserido na iteração anterior; para uma seqüência de cortes, o valor total é simplesmente a diferença entre o potencial do último nó inserido e o potencial original (basta fazer uma soma telescópica). Portanto, no caso em que a inserção de vi precede a de vj , sij = pj pi. Lembrando que no caso em que a inserção de vj precede a de vj temos que pi pj , conclui-se que cij = cij maxf0; pj pig. 2 Esse teorema mostra que, se forem conhecidos os potenciais resultantes da execução do algoritmo de Dijkstra, pode-se determinar o custo reduzido de qualquer arco em tempo O(1). Voltando ao problema de Steiner em grafos, considere o que ocorre no algoritmo para xação de um arco (u; v) por custos reduzidos, considerando-se uma solução dual . O custo reduzido original do arco em relação a é denotado por c (u; v). Esse valor e os dos custos reduzidos de todos os demais arcos do grafo são conhecidos. Conforme proposto na Seção 6.1.4.1, a partir da execução de um algoritmo de Dijkstra novos cortes podem ser (implicitamente) adicionados à solução de forma a garantir que haja um caminho entre r (a raiz) e v. Seja 0 a solução dual obtida após essa adição. O Teorema 3 mostra que o novo custo reduzido de um arco a = (i; j ) qualquer é dado por: c0 (i; j ) = c (i; j ) maxf0; pj pig (6.5) Repare que essa fórmula utiliza os custos reduzidos originais (em relação a ), já que são esses os custos utilizados durante o algoritmo de Dijkstra. Por essa fórmula, podem ser determinados todos os custos reduzidos em relação à solução 0, obtida para a computação de Qrv . Utilizando esses custos reduzidos, pode então ser executado um segundo algoritmo de Dijkstra para garantir a existência de um caminho entre w e um terminal qualquer, conforme proposto na seção 6.1.4.2. Seja Qruvt a soma dos cortes adicionados à formulação pelos dois algoritmos de Dijkstra (a partir de r, usando os custos reduzidos originais, e a partir de v, usando os custos reduzidos modicados). A condição para xação do arco (u; v) pode ser reescrita da seguinte forma: v() + c (u; v) + Qruvt > Zinc 1: (6.6) Ao longo deste capítulo, será utilizada a expressão custo reduzido estendido de (u; v) para representar c (u; v) + Qruvt . CAPÍTULO 6. ALGORITMOS DUAIS 91 6.1.4.4 Implementação A discussão acima mostra que duas execuções do algoritmo de Dijkstra podem determinar o custo reduzido estendido de um arco em relação a uma determinada solução dual. O custo dessa operação pode parecer alto, principalmente quando se considera que normalmente não apenas um, mas todos os arcos do grafo são vericados pela rotina de xação. Na verdade, nesse caso apenas duas execuções do algoritmo de Dijkstra são sucientes para medir os custos reduzidos estendidos de todos os arcos: 1. A primeira execução encontra a distância (em relação aos custos reduzidos determinados por ) entre a raiz r e todos os demais vértices do grafo. 2. A segunda execução deve encontrar o caminho mais curto de cada vértice ao terminal mais próximo (em relação aos custos reduzidos corrigidos, denidos pela Equação 6.5). Isso pode ser feito executando-se o algoritmo de Dijkstra no grafo transposto G (obtido a partir de G pela inversão de todos os arcos; veja [9], por exemplo), utilizando-se como origens todos os terminais (exceto a raiz). Depois de feitas as duas execuções do algoritmo de Dijkstra, pode-se testar se cada arco pode ou não ser xado em tempo O(1). Em princípio, a remoção de um arco pode aumentar a probabilidade de que outros sejam removidos, mantida a mesma solução dual. Para atualizar os custos reduzidos estendidos, seria necessário executar mais duas vezes o algoritmo de Dijkstra. No entanto, como as modicações ocorrem muito raramente, não foi feito o recálculo na implementação realizada. 6.1.4.5 Melhorando as Condições de Fixação É possível melhorar ainda mais as condições para xação por custo reduzido. Suponha que se deseje fazer a xação por custo reduzido do arco (u; v), como no exemplo da Figura 6.1. No cálculo do caminho mínimo entre v e t (seguindo a mesma notação utilizada até aqui), utilizam-se custos reduzidos corrigidos para os demais arcos. De acordo com a Equação 6.5, o valor do custo reduzido corrigido de um arco (i; j ) qualquer é c0 (i; j ) = c (i; j ) maxf0; pj pig; sendo pi e pj os potenciais dos vértices i e j ao nal da execução do algoritmo de Dijkstra com raiz em r. Na verdade, se esse custo reduzido for utilizado apenas para a tentativa de eliminação de (u; v), pode-se utilizar a seguinte expressão alternativa: c0 (i; j ) = c (i; j ) maxf0; minfpu; pj g pig: (6.7) CAPÍTULO 6. ALGORITMOS DUAIS 92 Essa expressão leva em conta o fato de que o algoritmo inicial de Dijkstra (com raiz em r) tem o objetivo apenas de encontrar um conjunto de cortes entre r e u. Todos os cortes adicionados depois que u é alcançado são irrelevantes e podem ser ignorados. O fato de o vértice j ter potencial pj maior que pu (o potencial do vértice u) indica que foram adicionados cortes depois que u foi alcançado. Os cortes podem ser desconsiderados e o potencial de u pode ser utilizado no lugar do potencial de j para o cálculo do custo reduzido corrigido de (i; j ). Repare que, se pu pj , nada muda em à Equação 6.5. Por outro lado, se pi pu, nenhuma correção precisa ser feita: o custo reduzido original é utilizado. A nova expressão para o custo reduzido corrigido aumenta a probabilidade de xação de um arco. Entretanto, por ser dependente do arco que se deseja xar, um algoritmo baseado nessa expressão requereria execuções especícas do algoritmo de Dijkstra, uma associada a cada arco que se tenta xar. Deixa de ser possível realizar uma execução global do algoritmo, como proposto na Seção 6.1.4.4. Resultados preliminares mostram que são raras as situações em que a Equação 6.7 permite a xação de um arco e a Equação 6.5 não permite. Em vista disso, os experimentos computacionais descritos neste e no próximo capítulo utilizam o algoritmo mais rápido, basados na Equação 6.5. 6.2 Dual Ascent Em [65], Wong propõe uma heurística construtiva para obter de forma combinatória soluções duais viáveis de boa qualidade para o problema de Steiner em grafos. O algoritmo se baseia na formulação por multiuxos para o problema [8, 65], que é equivalente à formulação baseada em cortes direcionadas. Apresenta-se aqui uma adaptação do algoritmo para a formulação por cortes direcionados. O algoritmo trabalha com dois grafos direcionados: G, o grafo original, e G , subgrafo de G contendo apenas os arcos saturados em relação à solução dual (o grafo de saturação). G permanece imutável ao longo de todo o algortimo; a G são adicionados novos arcos no decorrer da execução. Um conceito essencial no algoritmo de Wong é o de componente-raiz . Seja R uma componente fortemente conexa de G e seja V (R) o conjunto de vértices de R. R será uma componente-raiz se obedecer simultanemente às seguinte condições: 1. V (R) contém pelo menos um terminal; 2. V (R) não contém a raiz r; 3. não existe um caminho em G de um terminal t 62 R até R. É necessário denir algumas outras estruturas relacionadas a uma componente-raiz R. Dene-se w(R) como o conjunto dos vértices que alcançam R em G , incluindo os próprios CAPÍTULO 6. ALGORITMOS DUAIS 93 vértices de R. O conjunto de arcos incidentes em w(R) (partindo de V n w(R)) é representado por (w(R)). A variável dual associada ao corte denido por w(R) é denotada por w R . A Figura 6.2 apresenta o pseudocódigo do algoritmo de Wong. O algoritmo pode ser aplicado sobre qualquer solução dual viável . Em [65], utiliza-se sempre uma solução nula ( = 0); em alguns dos algoritmos propostos nas próximas seções, nem sempre isso ocorre. ( ) 01 function DUAL_ASCENT () { 02 inicialize G com os arcos saturados em relação a ; 03 while (existe uma componente-raiz R em G ) { 04 W w(R); 05 aumente W até que algum arco em (W ) que saturado; 06 adicione a G os novos arcos saturados; 07 } 08 return ; 09 } Figura 6.2: Dual Ascent Considere que = 0 no início. Como todas as variáveis duais são nulas, não há arcos saturados em G e G não contém arco algum. Nesse momento, cada terminal (exceto a própria raiz) é uma componente-raiz. Em cada iteração, o algoritmo escolhe uma componente-raiz e aumenta o valor da variável dual a ela associada até que pelo menos um arco esteja saturado. Portanto, a cada iteração pelo menos um novo arco é adicionado a G , o que pode provocar o aumento de algumas componentes-raízes e/ou a destruição de algumas outras (em razão de passarem a ser alcançáveis a partir de terminais). Ao nal de no máximo O(jE j) iterações, todos os terminais serão alcançáveis a partir de r em G . Não havendo mais componentes-raízes, o algoritmo termina. 6.2.1 Soluções Primais Depois de terminada a execução do dual ascent, haverá pelo menos um caminho da raiz r a qualquer terminal em G . Essa é justamente a condição de parada do algoritmo. Em [65], Wong sugere um método para extrair de G um subgrafo (arborescência) que representa uma solução primal de boa qualidade. Inicialmente, determina-se a arborescência de peso mínimo do subgrafo de G induzido pelos vértices que podem ser alcançados a partir da raiz. Seja A a árvore (não-orientada) correspondente a essa arborescência. Removem-se de A todos os vértices não-terminais de grau 1, bem como as arestas neles incidentes. Repete-se o processo de remoção até que todas as folhas da árvore sejam terminais. O algoritmo utilizado para encontrar a arborescência de peso mínimo é o próprio dual CAPÍTULO 6. ALGORITMOS DUAIS 94 ascent proposto por Wong. Quando aplicado sobre um grafo em que todos os vértices são terminais, a heurística de Wong comporta-se exatamente como o algoritmo de Edmonds [17] para a arborescência de peso mínimo. A única diferença é o fato de que, depois de encontrar a solução dual, o algoritmo de Edmonds realiza um reverse delete step . Esse procedimento consiste em remover os arcos de G na ordem inversa em que foram saturados, tomando-se o cuidado de não desconectar o grafo. Claramente, o resultado desse procedimento é uma arborescência. E, nesse caso, pode-se provar que ela tem peso mínimo. O procedimento para obtenção de soluções primais utilizado nos experimentos detalhados na Seção 6.2.4 é mais simples que o proposto por Wong. Em vez de se calcular a arborescência mínima de G , realiza-se um reverse delete step diretamente sobre G . Nesse caso, no entanto, não se exige que todo o grafo G permaneça conexo; exige-se apenas que sejam preservados caminhos da raiz a todos os terminais do grafo. O resultado será uma arborescência contendo todos os terminais. A qualidade das soluções primais obtidas por esse método é bastante satisfatória, especialmente quando a solução dual encontrada pelo algoritmo é de boa qualidade. Para tornar as soluções primais ainda melhores, podem ser usados métodos de busca local (como os apresentados no Capítulo 4) sobre a árvore (não-orientada) correspondente à arborescência. 6.2.2 Implementação Em cada iteração do dual ascent, pelo menos um arco é saturado. Haverá, portanto, no máximo O(jE j) iterações. Em cada uma delas é gerado um novo corte. Todos os arcos são investigados para que se determine o valor do corte. Como cada corte é composto por até O(jE j) arcos, a complexidade do algoritmo como um todo é O(jE j ). É essa também a complexidade do reverse delete step implementado (para cada aresta, faz-se uma busca em profundidade para determinar se ela pode ser retirada). Na prática, o pior caso do dual ascent é extremamente raro. Em primeiro lugar, a parcela dos cortes que contêm um número de arcos proporcional a jE j é normalmente pequena. Além disso, para acelerar a convergência do algoritmo, é conveniente, ao se aumentar em unidades o valor dual de um corte, que se considere saturado todo arco a (do corte) cujo custo reduzido (antes do aumento) tenha valor c + ", sendo " uma constante não-negativa. Com isso, mais arcos serão saturados em cada iteração; se " for signicativamente pequeno em relação ao valor médio dos arcos, as soluções obtidas serão tão boas quanto as soluções reais. Quando todos os custos são inteiros, usar " = 0 já pode ser muito vantajoso. Nesse caso, garante-se que o número de iterações nunca será maior que o valor da relaxação linear L do problema. Anal, em cada iteração o valor da solução dual subirá no mínimo uma unidade.3 A relevância dessa observação pode ser conrmada com exemplos concre2 Na verdade, para que o número de iterações seja limitado a b c é necessário que a solução dual inicial tenha custos reduzidos inteiros. Se os custos originais forem inteiros mas o dual ascent estiver 3 L CAPÍTULO 6. ALGORITMOS DUAIS 95 tos. A instância e16 (OR-Library), por exemplo, tem 62500 arestas, mas o valor de sua solução ótima (um limite superior para a relaxação linear) é 15. Portanto, um dual ascent executado sobre essa instância não terá mais que 15 iterações. Para outras instâncias, essa observação é menos relevante: a instância i640-301 (Incidência) tem apenas 960 arestas, mas o valor de sua solução ótima é 45005. Como regra geral, o dual ascent tende a ser mais rápido para instâncias com arcos de valor pequeno. 6.2.3 Seleção da Componente-raiz O pseudocódigo apresentado na Figura 6.2 exige que em cada iteração seja escolhida uma componente-raiz, mas não especica exatamente qual deve ser a escolhida. Em [65], Wong não recomenda qualquer critério de seleção. A rigor, isso de fato não é necessário, pois o algoritmo estará correto independentemente do critério de escolha. Ao nal da execução, haverá uma solução dual viável maximal (i.e., com caminhos saturados da raiz até qualquer outro terminal) e, portanto, será possível calcular um limite inferior para o problema. No entanto, não basta encontrar um limite inferior, deseja-se que ele seja tão bom quanto possível. A Seção 6.2.4 mostra que o critério de escolha das componentesraízes tem inuência direta sobre a qualidade das soluções obtidas. Foram testados os seguintes critérios (os nomes são dados em inglês seguindo a nomenclatura já adotada em [44]): random: Escolhe-se qualquer das componentes-raízes com igual probabilidade. rst: Percorre-se a lista de vértices em ordem até que se encontre um vértice que faz parte de uma componente-raiz, que é a escolhida. circular: Similar ao critério anterior, mas a busca na lista numa determinada iteração é feita a partir do vértice seguinte ao selecionado na iteração anterior (quando se chega ao nal da lista, retorna-se ao começo). minarcs/maxarcs: Seleciona-se a componente-raiz que determina o corte com menor (maior) número de arcos. minvalue/maxvalue: Escolhe-se a componente-raiz que determina o corte de menor (maior) valor, ou seja, dentre todos os cortes, aquele cujo arco de menor custo reduzido tem valor mínimo (máximo). minsaturated/maxsaturated: A componente-raiz que determina o corte com menor (maior) número de arcos saturados é selecionada. sendo executado sobre uma solução dual fracionária, apenas o limite superior de de iterações será válido. j j) O( E para o número CAPÍTULO 6. ALGORITMOS DUAIS 96 Implementação Usando um dos três primeiros critérios sugeridos (random, rst e circular), a complexidade do dual ascent permanece sendo O(jE j ), já que a escolha de uma componente-raiz por qualquer desses critérios pode ser feita em tempo O(jV j). Na implementação dos demais critérios, a complexidade aumenta para O(jT jjE j ). 2 2 Esses métodos requerem informações sobre todos os cortes que podem ser escolhidos numa iteração, não apenas sobre o que de fato é selecionado. Cada uma das O(jT j) componentes-raízes determina um corte, e o processamento de cada corte requer que sejam examinados os até O(jE j) arcos que o compõem. A complexidade de cada uma das O(jE j) iterações, portanto, pode chegar a O(jT jjE j). Na prática, contudo, a deterioração no desempenho não é tão grave, mesmo quando o número de terminais é grande. Para isso, é conveniente armazenar as informações sobre todos os potenciais cortes ao invés de simplesmente recalculá-las em cada iteração. Uma vez selecionado um corte numa iteração, uma simples busca em profundidade no grafo de saturação (G ) pode determinar quais componentes-raízes são potencialmente afetadas. Apenas para essas componentes é necessário recalcular o valor, o número total de arcos e o número de arcos que seriam saturados caso o corte correspondente fosse escolhido. Essas componentes também são as únicas candidatas à fusão com outras componentes ou ao desaparecimento (i.e., podem deixar de ser componentes-raízes). Cabe ressaltar que essa é uma heurística para aceleração do algoritmo; no pior caso, todas as O(jT j) componentes podem ser afetadas pela saturação de um corte, o que signica que o algoritmo pode chegar a fazer a O(jT jjE j ) operações. 2 6.2.4 Resultados Experimentais Para analisar o comportamento do algoritmo de dual ascent na prática, ele foi executado sobre as instâncias apresentadas na Seção 2.4.2. Da classe PUC, foram testadas todas as instâncias; das classes OR-Library e VLSI, foram consideradas as instâncias não resolvidas pelo pré-processamento; das instâncias de Incidência, foram testadas apenas as 80 terminadas em 1. Cada execução é composta por uma série de operações. Inicialmente, executa-se um dual ascent. Em seguida, obtém-se uma solução primal com um reverse delete step seguido de uma busca local do tipo NP. Finalmente, a solução dual encontrada pelo dual ascent e o limite primal são utilizados na execução da rotina de xação por custos reduzidos descrita na Seção 6.1.4.4. Para cada instância, foram testados os nove critérios de escolha de componentes-raízes sugeridos na Seção 6.2.3. Para os critérios minarcs, maxarcs, minsaturated, maxsaturated, minvalue e maxvalue, podem ocorrer empates entre duas componentes-raízes durante a execução do algoritmo; os desempates foram feitos de forma aleatória. A Tabela 6.1 mostra os resultados obtidos para cada classe de instâncias. As medidas apresentadas na tabela são as seguintes: CAPÍTULO 6. ALGORITMOS DUAIS classe critério gap (%) inst. redução tempo média max resolv. (%) (s) rst 10.516 31.852 1 15.55 2.68 circular 3.241 11.390 15 49.40 6.09 random 3.790 14.301 14 43.31 6.23 minvalue 10.713 28.303 1 15.35 3.49 Incidência maxvalue 4.199 14.109 12 39.21 28.96 minarcs 2.684 13.368 14 51.96 22.54 maxarcs 11.765 30.864 0 13.84 3.43 minsaturated 4.155 13.295 9 41.07 20.36 maxsaturated 5.489 21.489 6 34.28 9.93 rst 2.904 14.286 18 65.75 0.51 circular 2.047 18.182 27 74.03 0.55 random 1.690 14.286 24 67.88 0.51 minvalue 2.717 17.241 16 60.05 0.52 OR-Library maxvalue 1.853 13.043 26 72.68 0.50 minarcs 0.621 5.556 33 81.05 0.39 maxarcs 4.705 25.000 14 58.21 0.55 minsaturated 0.898 15.385 33 80.66 0.40 maxsaturated 3.953 30.769 16 63.99 0.61 rst 18.616 31.710 0 0.00 28.42 circular 14.962 27.329 0 0.00 27.92 random 16.025 28.659 0 0.00 30.64 minvalue 19.365 34.446 0 0.00 31.54 PUC maxvalue 16.907 31.034 0 0.01 42.74 minarcs 12.322 21.053 0 0.00 44.51 maxarcs 19.550 32.496 0 0.00 33.74 minsaturated 13.748 21.870 0 0.00 40.64 maxsaturated 18.344 30.189 0 0.00 33.69 rst 1.628 15.078 20 44.28 12.23 random 1.367 3.929 13 43.27 10.77 circular 1.199 3.799 15 45.55 9.27 minvalue 2.499 11.196 11 32.78 11.89 VLSI maxvalue 1.326 4.799 14 42.48 11.55 minarcs 1.246 3.945 15 46.69 9.71 maxarcs 2.447 15.084 15 35.44 12.93 minsaturated 1.273 4.597 16 46.21 11.35 maxsaturated 1.609 6.006 13 40.92 12.87 Tabela 6.1: Critérios para escolha da componente-raiz por classe 97 CAPÍTULO 6. ALGORITMOS DUAIS 98 de dualidade (gap): Diferença percentual entre os valores primal e dual obtidos pelo próprio algoritmo (a solução ótima não é levada em consideração). Em cada classe, são apresentados tanto o valor médio quanto o valor máximo observado para o gap. Instâncias resolvidas (inst. resolv.): Número de instâncias resolvidas à otimalidade, ou seja, instâncias para as quais o limite superior (primal) e o limite inferior (dual) foram iguais. Redução percentual média do número de arcos (redução): Porcentagem do total de arcos eliminada pela xação por custos reduzidos (considera-se o valor médio para todas as instâncias da classe). Quando uma instância é resolvida à otimalidade, considera-se que houve uma redução de 100%. Tempo de execução (tempo): Tempo médio de execução em segundos. Considera toda a execução do algoritmo (dual ascent, reverse delete step, busca local e xação por custos reduzidos). Os tempos se referem a execuções feitas em um AMD K6-2 de 350 MHz. Gap A Tabela 6.2 mostra as mesmas medidas, mas considera todas as 264 instâncias testadas. Para tornar mais simples a análise, nessa tabela os métodos estão ordenados de acordo com a redução percentual média obtida (os algoritmos mais ecazes são os primeiros). critério gap (%) inst. redução tempo média max resolv. (%) (s) minarcs 3.645 21.053 62 46.86 18.17 circular 4.608 27.329 57 44.24 9.96 minsaturated 4.428 21.870 58 43.34 17.26 random 4.947 28.659 51 40.40 10.94 maxvalue 5.261 31.034 52 39.96 20.35 maxsaturated 6.460 30.769 35 36.14 13.27 rst 7.814 31.852 39 31.83 9.87 minvalue 8.230 34.446 28 27.18 10.61 maxarcs 8.997 32.496 29 27.10 11.32 Tabela 6.2: Critérios para escolha da componente-raiz (todas as instâncias testadas) Critérios Analisando-se a Tabela 6.2, observa-se que o critério de escolha da componentes-raízes tem grande inuência sobre a eciência do dual ascent. Verica-se que minarcs é o melhor dos critérios testados, com desempenho próximo ao dos métodos circular e minsaturated. Isso indica que os métodos de maior qualidade tendem a ser os que buscam CAPÍTULO 6. ALGORITMOS DUAIS 99 adiar ao máximo a destruição das componentes-raízes. Uma componente-raiz deixa de ser componente-raiz (é destruída) quando passa a ser alcançável em G por um terminal. Para que isso ocorra, é necessário que novos arcos sejam adicionados a G . O método minarcs tenta justamente evitar isso, adicionando o número mínimo possível de arcos em cada iteração. Os métodos circular, minsaturated, random e maxvalue fazem o mesmo, mas de forma indireta. Os métodos circular e random executam um crescimento mais ou menos equilibrado das componentes-raízes, evitando a criação prematura de grandes componentes, indesejáveis porque induzem cortes com muitos arcos (isso é justamente o contrário do que faz o critério rst). O critério random tem comportamento similar. O critério minsaturated possui boa qualidade principalmente para as instâncias em que muitas arestas têm custos semelhantes, como OR-Library, VLSI e parte da solução PUC; nesses casos, o algoritmo acaba se comportando de maneira muito semelhante a minarcs. O método maxvalue também é de certa forma uma aproximação de minarcs. Tudo mais permanecendo igual, o valor de um corte (o critério de seleção de maxvalue) tende a diminuir à medida que aumenta o número de arcos por ele interceptados; anal, o valor do corte é determinado pelo arco de custo reduzido mínimo. Privilegiar cortes com poucos arcos em cada iteração tem como efeito, além do aumento da qualidade das soluções obtidas, um aumento também no tempo de execução. Isso se explica pelo fato de o número de iterações tender a ser maior, já que a destruição de componentes-raízes é executada. Na média, o tempo de execução do método minarcs é quase o dobro do observado para o método maxarcs. Quanto à diferença entre os métodos rst, circular e random (de complexidade O(jE j )) e os demais (de pior caso O(jT jjE j )), ela existe, mas não é tão dramática quanto sugerem as expressões de complexidade. Na média, o método minarcs é executado em tempo igual ao dobro do requerido por circular; para o método maxarcs, por outro lado, a diferença é de apenas 20%, aproximadamente. Isso mostra que o número de iterações do método pode ser até mais importante que o fator jT j na complexidade. No restante deste capítulo e no próximo emprega-se um critério híbrido entre os mais promissores: o critério primário é minarcs; em caso de empate, utiliza-se minsaturated; persistindo a igualdade, a escolha nal é feita aleatoriamente. 2 2 Qualidade da Solução Quando se utilizam os melhores critérios, a eciência do dual ascent é bastante satisfatória. Os resultados do algoritmo, não por acaso, tendem a ser melhores para instâncias que possuem gap de dualidade real pequeno. Considera-se real o gap medido pela comparação entre a relaxação linear e o valor ótimo de cada solução; não é esse o valor a que se refere a tabela. Das classes estudadas, as que possuem menor gap médio são OR-Library e VLSI (veja [32] e [58], por exemplo). São justamente as que apresentaram a menor diferença percentual entre os limites dual e primal obtidos pelo dual ascent. As instâncias de Incidência, apesar de possuírem gaps um pouco maiores, apresentam CAPÍTULO 6. ALGORITMOS DUAIS 100 reduções bastante expressivas no número de arcos. Isso é resultado do fato de maior comprimento médio dos arcos nesses casos, o que facilita a xação. Os piores resultados foram obtidos para as instâncias PUC. Isso já era esperado, uma vez que essas instâncias foram criadas justamente com o propósito de desaar os melhores algoritmos conhecidos para o problema de Steiner em grafos [51]. Ainda assim, a diferença percentual média entre as soluções primais e duais obtidas com o melhor critério (minarcs) foi de apenas 12%, um valor pequeno se considerarmos a natureza especial das instâncias. 6.3 Outras Heurísticas Duais Apesar de as soluções fornecidas pelo algoritmo de dual ascent puro terem excelente qualidade, especialmente se forem utilizados os critérios mais ecazes, é possível melhorálas ainda mais. Nesta seção, são propostas três técnicas que tentam, heuristicamente, atingir dois objetivos relacionados: aumentar o valor da solução dual encontrada e reduzir o tamanho das instâncias de teste através de xações por custos reduzidos. Trata-se das técnicas de dual scaling, dual adjustment e xação ativa. 6.3.1 Dual Scaling O primeiro método implementado para melhorar as soluções fornecidas pelo dual ascent foi o scaling das variáveis duais. Uma solução fornecida pelo dual ascent é maximal , ou seja, é impossível aumentar o valor dual de qualquer corte (inclusive os que possuem valor nulo) sem tornar negativo o custo reduzido de algum arco e por conseguinte tornar a solução inviável. Dada uma solução dual viável maximal , o dual scaling consiste em multiplicar os valores de todas as variáveis duais por um mesmo fator constante e menor que 1. Dessa forma, obtém-se uma nova solução dual, que é viável mas não maximal. Pode-se então aplicar DUAL_ASCENT sobre ela, o que leva a uma nova solução , potencialmente melhor que . Esse método pode ser iterado repetidas vezes. A Figura 6.3 apresenta um pseudocódigo para esse método. Na prática, essa estratégia não teve um bom desempenho. São pouquíssimas as instâncias para a aplicação do scaling eleva signicativamente o valor da solução dual. Além disso, esse método tem um efeito colateral: ele aumenta muito o número de cortes nãonulos na solução dual (note que a técnica só adiciona cortes à solução, jamais os retira). O excesso de cortes pode tornar lenta a aplicação de outras heurísticas duais, além de aumentar a demanda por memória. No entanto, o aumento no número de cortes tem um efeito positivo em uma situação especíca: quando a solução dual obtida é utilizada como conjunto inicial de cortes na resolução exata da relaxação linear (utilizando-se um resolvedor de programas lineares como o CPLEX [29]). Mesmo utilizando os mesmos cortes, a primeira solução dual obtida pelo resolvedor tende a ser muito melhor que a obtida pelas heurísticas duais, já que o CAPÍTULO 6. ALGORITMOS DUAIS 101 01 function DUAL_SCALING () { 02 ; 03 for it 1 to itmax do { 04 DUAL_ASCENT ( ); 05 if (v() > v()) then ; 06 else return ; 07 } 08 return ; 09 } Figura 6.3: Dual Scaling resolvedor pode atribuir valores duais diferentes a eles. Uma das raras situações em que o algoritmo é bem-sucedido é a instância e18, considerada a mais difícil da classe OR-Library. O valor de sua solução ótima é 564 e o valor fornecido pelo dual ascent, 551. Aplicando-se dez iterações de scaling com = 0:875, obtém-se uma solução de valor 556.55. A solução obtida é melhor, mas utiliza 2137 cortes com valor não nulo, cerca de quatro vezes mais que a solução original. Mudando os valores duais atribuídos a esses cortes, o resolvedor é capaz de encontrar uma solução dual de valor 561.49, bastante próxima do ótimo (e utiliza apenas aproximadamente 828 cortes, descartando 1309, considerados redundantes). Prosseguindo a rotina de separação, a otimalidade da solução (564) pode ser provada em 191 segundos (em uma Sun Ultra 1 de 167 MHz). Se apenas a solução do dual ascent for utilizada para inicializar o programa linear, o tempo total sobe para 391 segundos. Nesse caso, o dual scaling é realmente útil. Infelizmente, é um dos poucos: para a grande maioria das instâncias testadas o scaling é incapaz de elevar o valor da solução dual inicial e o número excessivo de cortes muitas vezes retarda a solução do programa linear. 6.3.2 Dual Adjustment O procedimento de dual adjustment pode ser entendido como um método de busca local para soluções duais. Seja uma solução dual viável (normalmente maximal). Uma solução vizinha é obtida retirando-se de um ou mais cortes (tornando nulos seus valores duais) e, em seguida, executando-se a rotina DUAL_ASCENT sobre a solução resultante. Numa aplicação bem-sucedida dessa operação, a solução obtida terá valor maior que a solução inicial. Em princípio, qualquer subconjunto de cortes pode ser escolhido para retirada. No entanto, observou-se que um conjunto particularmente interessante é o formado pelos cortes que interceptam uma solução primal de boa qualidade mais de uma vez. De acordo com as restrições de complementaridade de folga (Seção 6.1.3), se a solução primal utilizada CAPÍTULO 6. ALGORITMOS DUAIS 102 for ótima e não houver gap de dualidade, os cortes retirados com certeza não fazem parte da solução. Uma extensão desse raciocínio para os casos não ideais (soluções sub-ótimas e/ou instâncias com gap ) mostrou-se relativamente eciente na prática, conforme se verá na Seção 6.3.4. 01 function DUAL_ADJUSTMENT (, xr ) { 02 ; 03 repeat { 04 Para todo W > 0 tal que (W ) cruza xr mais de uma vez, W 05 DUAL_ASCENT(); 06 faça xação por custos reduzidos em ; 07 if (v() > v()) then ; 08 else return ; 09 } forever; 10 } 0; Figura 6.4: Dual Adjustment O pseudocódigo para o DUAL_ADJUSTMENT é apresentado na Figura 6.4. Os dois parâmetros básicos de entrada são a solução dual () e uma solucão primal de referência (xr ), em sua versão orientada (com as arestas tranformadas em arcos e utilizando a mesma raiz utilizada pela solução dual). Repare que, enquanto o procedimento for bem-sucedido, ele se repete com as novas soluções duais geradas. Para cada nova solução, seja ela melhor ou pior que a original, é interessante xar por custos reduzidos do maior número possível de variáveis. 6.3.3 Fixação Ativa Como o próprio nome sugere, a xação ativa é um procedimento que tem como principal objetivo aumentar o número de arcos eliminados (xados em zero) por custos reduzidos. De acordo com as Condições 6.1 e 6.6, a probabilidade de um arco xa ser eliminado cresce com o aumento de seu custo reduzido. O custo reduzido será máximo (igual ao comprimento do arco) quanto todos os cortes que interceptam o arco a tiverem custo dual igual a zero; em outras palavras, quando não houver corte algum interceptando o arco. A xação ativa é um procedimento que utiliza o dual ascent para gerar uma solução dual de boa qualidade que garante essa condição para algum arco a. Quando executado a partir de uma solução dual nula, a implementação do procedimento é simples: dado o arco a, basta inseri-lo no grafo saturado G (isso equivale à adição da restrição xa = 1). Executando o algoritmo de dual ascent sobre o grafo G, será obtida uma solução dual que garantidamente não utiliza cortes que interceptam o arco a. O método pode também ser aplicado a partir de uma solução dual inicial viável e CAPÍTULO 6. ALGORITMOS DUAIS 103 não-nula. Basta que todos os cortes que interceptam a em sejam previamente retirados da solução (ou seja, tenham seu valor reduzido a zero). Note que isso pode resultar em alterações no grafo G . Feito isso, adiciona-se a G o arco a e executa-se o dual ascent normalmente. Independentemente da solução inicial, pode-se garantir que a solução nal será dual viável. Portanto, podem-se fazer xações por custos reduzidos sem problemas. É claro que o intuito é xar o próprio a a zero; entretanto, uma análise empírica desse procedimento mostrou que é interessante testar também os demais arcos. Em muitos casos, vários deles são xados. São comuns também os casos em que a solução obtida pela xação ativa é melhor que a obtida pelo dual ascent livre (sem restrições quanto aos arcos que podem ser utilizados). Nesse caso, a xação ativa acaba atuando como um método de dual adjustment. O pseudocódigo para todo o procedimento é apresentado na Figura 6.5. Observe que resta ainda discutir dois detalhes do algoritmo: o critério de parada (linha 03) e o critério de escolha do arco testado em cada iteração (linha 04). 01 function ACTIVE_FIXATION () { 02 ; 03 while (critério de parada) { 04 escolha um arco a; 05 0 f n W ; 8W tal que a 2 W g; 06 insira a em G0 ; 07 DUAL_ASCENT (0); 08 faça xação por custos reduzidos em ; 09 if (v() > v()) then ; 10 } 11 return ; 12 } Figura 6.5: Fixação Ativa Comecemos pela escolha dos arcos. Sendo a solução inicial, consideram-se apenas os arcos cujos custos são superiores ao gap entre a melhor solução primal conhecida e . Entre esses arcos, a prioridade é dada àqueles interceptados em por um conjunto de cortes de pequeno valor dual total. Anal, são esses os arcos mais próximos da xação. Quanto ao critério de parada, na verdade foram utilizados diversos critérios simultaneamente. Em pricípio, não se permitiu que uma única execução do algoritmo acima superasse dez segundos. Há uma única exceção. Quando o dual ascent da linha 07 encontra uma solução melhor que a original, permite-se que o algoritmo seja executado por mais dez segundos. Outro critério de parada utilizado é o número de execuções mal-sucedidas: se forem executadas diversas iterações sem sucesso (i.e., sem que nenhum arco seja eliminado) o algoritmo é interrompido, mesmo que não tenham se passado dez segundos. Nos CAPÍTULO 6. ALGORITMOS DUAIS 104 testes realizados, permitiu-se que o algoritmo tivesse no máximo duas falhas inicialmente; além disso, a cada duas iterações de sucesso, dá-se um crédito extra ao algoritmo. Na prática, portanto, o número de falhas pode ser maior, desde que seja no máximo igual à metade do número de iterações bem-sucedidas. A limitação de tempo é um tanto arbitrária, mas fez-se necessária. Para muitas instâncias grandes, o algoritmo é ecaz ou seja, consegue efetuar a xação do arco selecionado , mas muito lento. Observou-se na prática que é mais conveniente interromper o algoritmo e, por exemplo, executar outro algoritmo ou mesmo dividir o problema em dois subproblemas, conforme será discutido na Seção 7.3. 6.3.4 Resultados Experimentais Nesta seção, apresentam-se resultados empíricos que demonstram a eciência das heurísticas duais apresentadas. Foram utilizadas nos testes aqui apresentados as mesmas instâncias testadas na Seção 6.2.4: instâncias de Incidência terminadas em 1, PUC, VLSI e OR-Library (sendo as duas últimas séries consideradas após o pré-processamento). Foram analisadas três diferentes estratégias de combinação das heurísticas duais. A primeira estratégia é semelhante à testada na Seção 6.2.4: realiza-se um único dual ascent, seguido de um reverse delete step com busca local NP e da xação por custos reduzidos (em relação à solução obtida pelo dual ascent ). A segunda estratégia analisada também utiliza apenas o dual ascent como heurística dual, mas de forma iterada. Executa-se inicialmente o ciclo mais simples (dual ascent, reverse delete step, busca local e xação por custos reduzidos). Se a redução no tamanho da instância for menor que 1% ou igual a 100% (i.e., se a instância for resolvida), o algoritmo pára; caso contrário, executam-se novamente os mesmos algoritmos. Repare que as execuções posteriores podem aproveitar as reduções e os limites primais encontrados nas iterações anteriores. As iterações se sucedem enquanto houver reduções signicativas (de pelo menos 1%) e o problema não for resolvido. A terceira estratégia testada utiliza os métodos de dual adjustment e xação ativa para tentar melhorar os resultados obtidos pela segunda estratégia. Inicialmente, executam-se os algoritmos da segunda estratégia. Caso a instância não seja completamente resolvida, aplica-se um dual adjustment sobre a melhor solução dual encontrada e, em seguida, executa-se a rotina de xação ativa. Se a xação ativa conseguir uma redução superior a 1%, repete-se todo o algoritmo (a menos que a instância seja completamente resolvida, evidentemente). Em todos os testes, na execução do dual ascent, seja como heurística construtiva dual, seja como sub-rotina do dual adjustment e da xação ativa, foi utilizado o critério minarcs para seleção das componentes-raízes, sendo minsaturated e random (nessa ordem) os critérios de desempate. CAPÍTULO 6. ALGORITMOS DUAIS 105 A Tabela 6.3 mostra os resultados obtidos pelas três estratégias. As medidas são as mesmas utilizadas nas Tabelas 6.1 e 6.2. No cálculo do gap, consideram-se a melhor solução primal e a melhor solução dual obtidas ao longo de toda a execução. algoritmo classe Incidence dual ascent OR-Library (uma execução) PUC VLSI Total Incidence dual ascent OR-Library (várias execuções) PUC VLSI Total Incidence dual ascent OR-Library + adjustment PUC + xação ativa VLSI Total gap (%) inst. redução tempo média max resolv. (%) (s) 2.767 0.820 12.675 1.195 3.765 1.740 0.287 12.675 0.782 3.218 1.647 0.251 11.882 0.652 2.994 8.815 15.385 21.053 4.932 21.053 7.708 5.505 21.053 4.932 21.053 7.708 4.587 21.053 4.932 21.053 14 37 0 15 66 38 51 0 37 126 40 51 0 46 137 51.02 83.89 0.00 47.97 47.56 62.46 89.48 0.00 65.52 57.36 64.88 89.48 0.01 71.50 59.84 23.01 0.42 44.32 10.73 18.59 38.10 0.47 44.80 11.67 23.54 46.91 0.56 66.02 16.52 31.66 Tabela 6.3: Resultados obtidos pelas heurísticas duais Observe que a aplicação iterada do dual ascent obtém resultados muito superiores aos obtidos por uma única aplicação do algoritmo e faz isso sem requerer aumentos signicativos no tempo de execução. As novas heurísticas duais sugeridas (dual adjustment e xação ativa) contribuem para aumentar ainda mais a qualidade das soluções obtidas, também sem aumentos expressivos no tempo de execução. Repare, no entanto, que o uso de heurísticas duais mais elaboradas é ecaz apenas para as instâncias que possuem gap de dualidade relativamente pequeno. Para as instâncias PUC, criadas justamente com o objetivo de desaar algoritmos baseados em dualidade, a repetição do dual ascent não tem qualquer efeito (até porque nem chega a ser feita, já que em nenhuma instância a primeira execução obtém reduções maiores que 1%). As novas heurísticas duais conseguem melhorar ligeiramente os resultados obtidos, mas de forma marginal. 6.4 Conclusão O algoritmo básico discutido neste capítulo é o dual ascent, uma heurística dual para o problema de Steiner em grafos. Por sua natureza, seu objetivo principal é fornecer bons limites inferiores para o problema. No entanto, ele tem um subproduto, uma solução CAPÍTULO 6. ALGORITMOS DUAIS 106 primal, em geral de boa qualidade. O resultado da interação entre esses dois elementos (um limite inferior e um limite superior) é um método poderoso para a resolução do problema de Steiner em grafos. Para boa parte das instâncias, foi possível provar a otimalidade das soluções obtidas. Em muitos outros casos, foi possível reduzir signicativamente o tamanho das instâncias por meio da xação por custos reduzidos. No mínimo, o método é capaz de fornecer boas soluções primais com garantia de qualidade. Além disso, conforme se verá no Capítulo 7, as soluções duais fornecidas pelas heurísticas podem ser utilizadas como ponto de partida para o cálculo exato da relaxação linear do problema. Foram apresentadas ainda outras heurísticas duais, todas tendo o dual ascent como sub-rotina: dual scaling, dual adjustment e xação ativa. Esses métodos, especialmente os dois últimos, se mostraram úteis para potencializar os efeitos do dual ascent. Com eles foi possível resolver mais instâncias à otimalidade, eliminar mais arcos por custos reduzidos e diminuir o gap de dualidade médio obtido. Um aspecto das heurísticas alternativas que merece um estudo mais detalhado é o tempo necessário para sua execução. Da forma como foram propostos, os métodos requerem a execução de um dual ascent como sub-rotina. Se para o dual adjustment o tempo de execução é tolerável, para a xação ativa nem sempre é esse o caso. Anal, deve-se executar um dual ascent para cada arco candidato à xação. Para evitar um aumento maior no tempo de execução, foram utilizados diversos critérios de parada simultâneos, baseados no tempo de execução e na ecácia da estratégia. Uma maneira mais apropriada de lidar com esse problema seria executar não o dual ascent de Wong como sub-rotina da xação ativa, mas um algoritmo mais simples e rápido, semelhante à versão modicada do algoritmo de Dijkstra apresentada na Seção 6.1.4 (um algoritmo com essas características é apresentado por Polzin e Daneshmand em [46]). Um dual ascent mais rápido tende a fornecer soluções de pior qualidade quando utilizado como heurística dual construtiva. Entretanto, como sub-rotina do dual adjustment e da xação ativa, o método apenas completa uma solução dual que é quase maximal, o que tende a torná-lo relativamente mais eciente. Outro algoritmo que requer um estudo mais aprofundado é o dual scaling. Na versão apresentada, sua utilidade se limita a algumas poucas instâncias. Seria interessante estudar variantes do método que o tornassem mais eciente ou robusto. Capítulo 7 Algoritmos Exatos Este capítulo trata de algoritmos exatos para o problema de Steiner em grafos: algoritmos capazes de encontrar a solução ótima (e provar sua otimalidade) de qualquer instância em tempo nito. Como o problema de Steiner é NP-difícil, os algoritmos propostos são exponenciais no pior caso. Conforme se verá, no entanto, eles são capazes de resolver rapidamente grande parte das instâncias apresentadas na Seção 2.4.2, muitas vezes em frações de segundo; em vários casos, os algoritmos aqui descritos foram os primeiros a provar a otimalidade da solução. Os dois algoritmos apresentados são instanciações da técnica de branch-and-bound, conceito discutido na Seção 7.1. A Seção 7.2 descreve o funcionamento de um dos algoritmos, um branch-and-cut ; a Seção 7.3 trata do segundo, que recebeu a denominação de branch-and-ascent . Na Seção 7.4, os algoritmos são comparados empiricamente entre si e com outros métodos descritos na literatura. Finalmente, a Seção 7.5 discute a aplicabilidade de cada um dos métodos e possíveis formas de torná-los ainda mais ecientes. Conforme mencionado no capítulo anterior, os métodos aqui discutidos foram originalmente apresentados em [44], artigo escrito em co-autoria com M. Poggi de Aragão e E. Uchoa. Alguns dos resultados apresentados neste capítulo são discutidos também na tese de doutorado de E. Uchoa [57]. 7.1 Branch-and-bound e Enumeração Implícita Ambos os algoritmos propostos neste capítulo são do tipo branch-and-bound. Nesta seção, discute-se brevemente o princípio de funcionamento dessa técnica para problemas de otimização combinatória em geral. Para uma discussão mais completa, veja [64], por exemplo. Um algoritmo de branch-and-bound nada mais é um método de enumeração implícita de todas as possíveis soluções para a instância de entrada. Ele requer duas rotinas básicas: 107 CAPÍTULO 7. ALGORITMOS EXATOS 108 uma para calcular limites superiores, outra para calcular limites inferiores. Inicialmente, aplicam-se as rotinas sobre a instância de entrada. Se o limite inferior for igual ao superior, o algoritmo termina. Se não for esse o caso, faz-se uma ramicação (ou branching ): o problema é particionado em dois ou mais subproblemas que, juntos, são equivalentes ao problema original. No caso de um problema de otimização 0-1, a ramicação é normalmente feita com base no valor de uma variável, a variável de branching , que tem seu valor xado em 0 em um dos subproblemas e em 1 no outro. Cada subproblema é resolvido recursivamente. A melhor solução encontrada para os subproblemas é a solução do problema original. Observe que o branch-and-bound constrói uma árvore de resolução durante sua execução. Cada nó da árvore dá origem a dois ou mais nós-lhos, a menos que os limites inferior e superior coincidam. Nesse caso, o subproblema estará resolvido. Diz-se que o ramo que tem esse nó como raiz foi podado, ou que se fez o pruning do nó. Assim sendo, o número de nós efetivamente visitados na árvore (que é exponencial no pior caso) depende da qualidade dos limites inferiores e superiores calculados ao longo da execução do algoritmo. Com limites melhores, o número de ramições feitas até que se possa fazer um pruning é menor. Os dois algoritmos apresentados nesse capítulo usam como limites inferiores soluções duais para a relaxação linear da formulação por cortes direcionados, decrita na Seção 6.1. A diferença entre os algoritmos está na forma de se obter essa solução: no branch-and-cut, utiliza-se um algoritmo exato; no branch-and-ascent, heurísticas. 7.2 Branch-and-Cut O primeiro algoritmo exato apresentado é do tipo branch-and-cut. Em cada nó da árvore de resolução, resolve-se de forma exata a relaxação linear do subproblema correspondente, sendo o valor obtido utilizado como limite inferior para sua solução ótima. Alguns dos mais ecientes algoritmos para o problema de Steiner em grafos utilizam essa mesma estratégia: Lucena e Beasley [35], Koch e Martin [32], Uchoa et al. [58] e Polzin e Daneshmand [46]. Nas subseções seguintes, descrevem-se os procedimentos para o cálculo de limites inferiores, para realização da ramicação (branching ) e para a determinação de limites superiores. 7.2.1 Limites Inferiores O algoritmo utiliza a formulação por cortes direcionados, a mesma utilizada em [32, 46, 58]. Conforme já mencionado na Seção 6.1, essa formulação dene um número exponencial de restrições, o que torna inviável resolvê-la diretamente. O que se faz é iniciar o algoritmo com apenas um subconjunto de restrições e adicionar novas restrições aos poucos por CAPÍTULO 7. ALGORITMOS EXATOS 109 meio da execução de uma rotina de separação. Com isso, o valor da solução dual (limite inferior) cresce progressivamente até atingir o valor da relaxação linear do problema. Inicialização Em algoritmos do tipo branch-and-cut, é comum que a resolução da re- laxação linear no nó-raiz (o problema original) se inicie com um programa linear sem restrições ou apenas com restrições triviais. É esse o caso, por exemplo, dos algoritmos descritos em [32, 58]. Entretanto, para acelerar a convergência do algoritmo, é conveniente resolver a relaxação a partir de um bom conjunto inicial de cortes. Em [46], utilizam-se soluções fornecidas pelo algoritmo de dual ascent. No algoritmo aqui proposto, utilizamse também as técnicas de dual adjustment e xação ativa para melhorar a solução dual inicial, conforme proposto na Seção 6.3.4. Separação Para a formulação por cortes direcionados para o problema de Steiner em grafos, o procedimento de separação consiste em encontrar um corte que separa a raiz de um terminal e é tal que a soma das variáveis correspondentes aos arcos cortados por ele tenha valor inferior a 1.0. Utilizou-se como rotina de separação uma versão modicada do algoritmo de Hao e Orlin [27] para encontrar o corte mínimo global de um grafo orientado. A modicação foi feita por Eduardo Uchoa [57] sobre a implementação original de G. Skorobohatyj [52]. Um aspecto positivo desse algoritmo é o de que, na busca pelo corte mínimo, ele normalmente encontra até jT j 1 outros cortes violados. Adicionados à formulação, eles aceleram signicativamente a convergência do algoritmo. Cada execução q da rotina de separação é feita em tempo O(jV j jE j) no pior caso. 2 Subproblemas Num algoritmo de branch-and-cut, é necessário resolver a relaxação linear não só do nó-raiz, mas também de todos os subproblemas. No algoritmo aqui introduzido, a resolução de uma subproblema é feita pela aplicação (iterada, se necessário) da rotina de separação sobre o conjunto de cortes de valor dual positivo presentes na relaxação linear do problema pai. Como essa estratégia requer a manutenção simultânea de diversas soluções duais em memória, optou-se por percorrer em profundidade a árvore de resolução (isso limita o número de nós ativos). 7.2.2 Soluções Primais No algoritmo proposto por Koch e Martin [32], o método utilizado para encontrar limites superiores é a heurística construtiva de Prim (Seção 3.3). Para obter melhores qualidades, o algoritmo é executado considerando não os pesos originais das arestas, mas pesos modicados a partir dos valores fornecidos durante a resolução da relaxação linear do problema. Sendo xa o custo da variável associada ao arco a na solução do programa linear, será c(a)(1 xa ) o custo do arco a para a heurística de Prim. Essa modicação privilegia os arcos escolhidos pelo resolvedor LP (xa = 1) e penaliza os que foram preteridos (xa = 0). CAPÍTULO 7. ALGORITMOS EXATOS 110 O algoritmo aqui proposto utiliza procedimento semelhante. A cada execução da rotina de separação, o algoritmo de Prim é executado dez vezes, sendo uma delas exatamente como a descrita acima; nas demais execuções, os custos modicados são aleatoriamente perturbados em até 5%. Além disso, as soluções construtivas mais promissoras são submetidas à busca local NP. 7.2.3 Branching Apesar de a formulação por cortes direcionados associar uma variável a cada arco, decidiuse fazer uma ramicação binária com base nos vértices do grafo. Pode-se fazer isso porque o conjunto de vértices de uma solução é suciente para determiná-la totalmente, conforme demonstrado na Seção 4.1. Como o número de vértices não-terminais em um grafo pode ser menor que o de arestas, usá-los no branching tende a reduzir a profundidade da árvore de resolução. O princípio básico utilizado é o mesmo já descrito anteriormente. Seja v o vértice escolhido para ramicação (um não-terminal, obrigatoriamente). Em um dos subproblemas (ramos), exige-se que v faça parte da solução, o que é feito por meio da tranformação do vértice em um terminal. No outro subproblema, exige-se que o vértice não faça parte de qualquer solução, o que se faz eliminando o vértice (e os arcos associados) do grafo. Na implementação realizada, a seleção do vértice de branching é feita com base na solução primal da relaxação linear. Para cada vértice, calcula-se a soma das variáveis x correspondentes a todos os arcos incidentes; será escolhido o vértice que tiver esse valor mais próximo de 0.5. Utilizar a variável mais próxima de 0.5 é um critério de branching muito adotado na prática. Em caso de empate, tem preferência aquele cuja soma das variáveis associadas aos arcos de saída tem valor máximo. 7.3 Branch-and-Ascent Praticamente todo o tempo de execução do branch-and-cut proposto acima é gasto na resolução de relaxações lineares. Tomadas em conjunto, as execuções da rotina de separação e as reotimizações do programa linear após a adição de cada novo conjunto de cortes são bastante custosas. O algoritmo de branch-and-ascent nasce como uma tentativa de contornar essa diculdade, reduzindo signicativamente o tempo gasto no cálculo de limites inferiores. O branch-and-ascent é um algoritmo de enumeração implícita, assim como o branchand-cut. Entretanto, enquanto o branch-and-cut usa a solução exata da relaxação linear de cada subproblema como limite inferior, o branch-and-ascent usa soluções heurísticas. Como as soluções aproximadas são de pior qualidade, isso acaba se traduzindo em árvores de resolução mais profundas. Por outro lado, reduz-se drasticamente o tempo de processamento de cada nó. Para diversas instâncias, essa troca é extremamente vantajosa. CAPÍTULO 7. ALGORITMOS EXATOS 111 7.3.1 Método de Resolução de um Nó Em cada nó da árvore de resolução executa-se a estratégia apresentada na Seção 6.3.4, composta por uma ou mais execuções do algoritmo de dual ascent, dual adjustment e xação ativa. As soluções primais (limites superiores) são fornecidos pela execução do reverse delete step após cada dual ascent, seguida sempre da aplicação da busca local NP. Optou-se por fazer com que em cada nó da árvore fosse executado um dual ascent a partir do zero. A alternativa seria aplicar o dual ascent sobre a melhor solução dual encontrada pelo pai, mas esse método se revelou pouco interessante. Na grande maioria dos casos uma solução maximal para o nó-pai continua maximal mesmo após transformada em solução para o lho (pela remoção de um nó ou pela conversão de um não-terminal em terminal). Assim, são necessários inúmeros branchings até que uma solução encontrada tenha valor dual elevado. Executado a partir do zero, o dual ascent tem mais liberdade para explorar a nova estrutura do grafo (com menos vértices ou mais terminais), o que se traduz em soluções de melhor qualidade. É claro que, por se tratar de um método heurístico, nada garante que o dual ascent executado em um nó encontrará solução melhor que a encontrada no pai. Assim sendo, não necessariamente a solução fornecida para as rotinas de dual adjustment e xação ativa terá sido encontrada no próprio nó; pode ser a melhor solução encontrada em algum ancestral. Por sinal, tanto o dual adjustment quanto a xação ativa se mostraram muito úteis, especialmente nas instâncias mais difíceis, para melhorar soluções encontradas em outros nós da árvore de resolução. 7.3.2 Branching As ramicações do branch-and-ascent são baseadas em vértices, como no branch-and-cut. Muda apenas o critério de seleção do vértice de branching. No branch-and-ascent, a ramicação é feita sobre o vértice com maior número de arcos saturados incidentes na melhor solução dual conhecida; o desempate é feito com base nos arcos de saída. A motivação é o fato de que, para um vértice com essas características, a probabilidade de que ele de fato pertença à solução ótima (do subproblema) é relativamente alta. Isso ajuda a equilibrar a árvore de resolução, uma vez que, como se vericou empiricamente, subproblemas resultantes da inserção de um terminal tendem a ser mais facilmente resolvidos que os resultantes da remoção de um vértice. Experimentalmente, vericou-se que o critério de branching descrito acima nem sempre é bem-sucedido. O número de arcos saturados numa solução fornecida por heurísticas duais é normalmente muito alto. Não são raras as situações em que mais da metade dos arcos estão saturados ao nal da execução das heurísticas. Assim, há vários vértices com um grande número de arcos incidentes saturados, mas nem todos podem fazer parte da solução nal; o critério acima não garante que uma boa decisão será Strong Branching CAPÍTULO 7. ALGORITMOS EXATOS 112 tomada. Com o objetivo de minimizar esse problema, utilizou-se um método de ramicação forte (strong branching ). Para a seleção do vértice de branching, o algoritmo testa diversos vértices. A lista de candidatos é construída com base nos critérios utilizados para o branching simples: número de arcos saturados incidentes em cada vértice. O teste de um candidato v consiste em duas execuções do dual ascent : na primeira, exige-se que o vértice não faça parte da solução (ele e os arcos nele incidentes são removidos); na outra, exige-se que o ele faça parte da solução (ele é transformado em terminal). Sejam os valores duais obtidos q (v) e q (v), respectivamente. Dene-se a qualidade do nó como 0 1 q(v) = 2 minfq (v); q (v)g + maxfq (v); q (v)g: 0 1 0 1 Escolhe-se como vértice de branching o candidato de maior qualidade. Observe que, ao atribuir um peso maior ao pior ramo, essa estratégia tenta garantir ramicações mais balanceadas. 7.4 Resultados Experimentais Esta seção analisa experimentalmente o desempenho dos algoritmos propostos, que são comparados entre si e com os algoritmos exatos de melhor desempenho disponíveis na literatura. Trata-se dos algoritmos apresentados por Koch e Martin [32], Uchoa et al. [58] e Polzin e Daneshmand [46]. Todos eles são do tipo branch-and-cut. Para evitar ambigüidades, será utilizada a abreviação B&C para o branch-and-cut proposto na Seção 7.2; os demais algoritmos serão identicados pelas iniciais de seus autores: KM (Koch e Martin), UPR (Uchoa, Poggi de Aragão e Ribeiro) e PD (Polzin e Daneshmand). O termo B&A será ocasionalmente utilizado em referências ao branch-and-ascent. Uma análise preliminar dos algoritmos aqui propostos indica que eles são complementares. Para grafos com muitos vértices, o B&C parece ser o método mais adequado. Para grafos com menos vértices, a melhor alternativa parece ser o branch-and-ascent (independentemente do número de arestas). Assim, para as instâncias testadas, o branch-andascent apresentou melhores resultados para as instâncias de incidência; para as instâncias VLSI e OR-Library, a melhor alternativa é B&C. Para as instâncias PUC, ambos os algoritmos são igualmente inecazes, devido aos grandes gap de dualidade e à simetria dessas instâncias.1 As subseções seguintes detalham os resultados obtidos para cada classe. Lembre-se de que as instâncias PUC foram criadas com o propósito de serem difíceis. E os algoritmos aqui propostos foram justamente os utilizados para atestar a diculdade das instâncias. Portanto, podese dizer que eles são, por construção, incapazes de resolvê-las (exceto no caso de algumas poucas instâncias muito pequenas [51]). 1 CAPÍTULO 7. ALGORITMOS EXATOS 113 7.4.1 Incidência As Tabelas 7.1 e 7.2 apresentam os resultados obtidos pelo algoritmo de branch-andascent para as instâncias de Incidência terminadas em 1. Para cada instância, as tabelas apresentam suas dimensões (números de vértices, arestas e terminais) e o valor da solução ótima (caso ela seja conhecida). Além disso, as tabelas mostram ainda o número de nós (nós) e a profundidade máxima (prof.) da árvore de resolução percorrida pelo branch-and-ascent. Apresenta-se também o tempo de execução do algoritmo em cada caso. Os tempos de execução podem ser comparados com os obtidos pelo algoritmo de Koch e Martin, apresentados na coluna KM (os algoritmos PD e UPR não foram testados para instâncias dessa classe). Os tempos para o branch-and-ascent foram obtidos em um Pentium II de 400 MHz; o algoritmo KM foi executado numa Sun Enterprise 3000. Nas tabelas, os tempos de execução são apresentados em segundos. Note que tanto o algoritmo de branch-and-ascent aqui proposto quanto o algoritmo de branch-and-cut de Koch e Martin foram capazes de resolver todas as instâncias das séries i080 e i160. Já a série i320 pôde ser resolvida completamente apenas pelo branch-andascent. Seis das instâncias não puderam ser resolvidas pelo algoritmo de Koch e Martin em 10 mil segundos, o tempo máximo de execução permitido em [32]. Na série i640, ambos os algoritmos tiveram diculdades (o algoritmo de Koch e Martin deixou de resolver oito instâncias; o branch-and-ascent deixou de solucionar seis delas). Em termos do tempo de execução, observe que o algoritmo de branch-and-ascent é normalmente mais rápido, mas o algoritmo de Koch e Martin tem desempenho superior em alguns casos, especialmente para instâncias muito esparsas: seu tempo de execução para a instância i640-331, por exemplo, é inferior à metade do requerido pelo branch-andascent, mesmo sendo executado em uma máquina mais lenta. Para instâncias mais densas, mesmo que apenas ligeiramente, o desempenho do branch-and-ascent é muito superior. Em alguns casos, chega a ser de duas a três ordens de grandeza mais rápido. Trata-se de um método muito interessante para essa classe de instâncias, portanto. Pode-se chegar às mesmas conclusões a partir de uma comparação direta entre o branch-and-ascent e o branch-and-cut. O B&C foi executado sobre as instâncias de nal 1 das séries i080 e i160 que não foram resolvidas pelo branch-andascent no nó-raiz. A Tabela 7.3 mostra, para cada um dos algoritmos (e também para o de Koch e Martin), o número de nós na árvore de resolução e o tempo total de execução (que, no caso do B&C, foi feita numa Sun Ultra de 167 MHz). A tabela deixa clara a principal característica do branch-and-ascent : o número de nós na árvore de resolução pode ser muito maior que no branch-and-cut, mas a resolução de cada nó é muito mais rápida. Mantido constante o número de vértices, a diferença entre os tempos de execução de cada nó é maior para as instâncias mais densas, já que a existência de um grande número de variáveis no programa linear torna a sua resolução mais lenta. Nas heurísticas duais, a dependência em relação ao número de arestas é mais fraca. Branch-and-cut CAPÍTULO 7. ALGORITMOS EXATOS instância i080-001 i080-011 i080-021 i080-031 i080-041 i080-101 i080-111 i080-121 i080-131 i080-141 i080-201 i080-211 i080-221 i080-231 i080-241 i080-301 i080-311 i080-321 i080-331 i080-341 i160-001 i160-011 i160-021 i160-031 i160-041 i160-101 i160-111 i160-121 i160-131 i160-141 i160-201 i160-211 i160-221 i160-231 i160-241 i160-301 i160-311 i160-321 i160-331 i160-341 jV j 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 80 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 jEj 120 350 3160 160 632 120 350 3160 160 632 120 350 3160 160 632 120 350 3160 160 632 240 812 12720 320 2544 240 812 12720 320 2544 240 812 12720 320 2544 240 812 12720 320 2544 jT j 6 6 6 6 6 8 8 8 8 8 16 16 16 16 16 20 20 20 20 20 7 7 7 7 7 12 12 12 12 12 24 24 24 24 24 40 40 40 40 40 114 ótimo nós prof. tempo 1787 1479 1175 1570 1276 2608 2051 1561 2284 1788 4760 3631 3158 4354 3538 5519 4554 3932 5226 4236 2490 1677 1352 2170 1494 3859 2869 2363 3356 2549 6923 5583 4729 6662 5086 11816 9135 7876 10414 8331 1 1 1 1 1 9 1 1 1 1 1 1 1 1 25 1 17 1 5 3 1 1 1 1 1 1 1 1 1 1 27 51 1 19 23 3 231 1 11 283 0 0 0 0 0 4 0 0 0 0 0 0 0 0 9 0 7 0 2 1 0 0 0 0 0 0 0 0 0 0 9 8 0 6 9 1 20 0 5 24 0.02 0.10 0.15 0.01 0.10 0.20 0.34 0.50 0.04 0.40 0.02 0.12 0.58 0.09 14.83 0.02 3.64 0.36 0.50 1.12 0.14 0.29 0.82 0.05 0.40 0.04 0.94 4.06 0.08 12.05 1.05 37.07 5.08 2.76 83.42 0.22 192.86 7.61 1.17 708.06 KM 0.2 0.8 5.5 0.2 2.5 0.1 4.0 7.5 0.4 3.5 0.3 2.3 22.1 0.4 1097.1 0.2 8.6 29.8 1.7 3.1 0.6 1.6 53.4 0.5 29.3 0.8 2.5 808.0 1.3 42.8 1.9 1143.5 889.1 2.3 21008.6 1.1 687.9 2959.4 1.8 54500.7 Tabela 7.1: Resultados do branch-and-ascent nas séries i080 e i160 e comparação de tempos (em segundos) CAPÍTULO 7. ALGORITMOS EXATOS instância i320-001 i320-011 i320-021 i320-031 i320-041 i320-101 i320-111 i320-121 i320-131 i320-141 i320-201 i320-211 i320-221 i320-231 i320-241 i320-301 i320-311 i320-321 i320-331 i320-341 i640-001 i640-011 i640-021 i640-031 i640-041 i640-101 i640-111 i640-121 i640-131 i640-141 i640-201 i640-211 i640-221 i640-231 i640-241 i640-301 i640-311 i640-321 i640-331 i640-341 jV j 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 640 640 640 640 640 640 640 640 640 640 640 640 640 640 640 640 640 640 640 640 jEj 480 1845 51040 640 10208 480 1845 51040 640 10208 480 1845 51040 640 10208 480 1845 51040 640 10208 960 4135 204480 1280 40896 960 4135 204480 1280 40896 960 4135 204480 1280 40896 960 4135 204480 1280 40896 jT j 8 8 8 8 8 17 17 17 17 17 34 34 34 34 34 80 80 80 80 80 9 9 9 9 9 25 25 25 25 25 50 50 50 50 50 160 160 160 160 160 ótimo 115 nós prof. 2672 1 2053 1 1553 1 2673 1 1707 1 5548 1 4273 15 3321 1 5255 5 3606 11 10044 15 8039 355 6679 1 9862 35 7027 297 23279 29 17945 58549 15648 9 21517 1425 16296 13991 4033 1 2392 1 1749 1 3278 1 1897 1 8764 13 6167 393 4906 1 8097 11 16079 9 9821 5 15014 77 45005 367 42796 8377 tempo 0 0.11 0 1.02 0 4.14 0 0.47 0 13.40 0 0.17 5 14.87 0 7.54 2 2.12 5 113.45 7 1.28 21 592.92 0 43.31 8 17.20 61 2567.66 9 6.33 34 134052.94 4 687.94 35 609.36 69 154091.24 0 0.39 0 2.31 0 29.26 0 1.07 0 43.00 5 6.09 15 1038.90 0 200.99 4 16.15 4 5.29 2 798.93 10 145.61 41 277.11 70 7902.88 KM 1.3 25.5 1102.0 10.5 434.6 1.7 206.5 2595.3 7.4 5.0 8274.6 39.5 13.3 266.7 5.2 18.8 11004.9 6.5 2214.9 26.7 13677.5 28.6 31.3 804.1 152.3 3817.9 Tabela 7.2: Resultados do branch-and-ascent nas séries i320 e i640 e comparação de tempos (em segundos) CAPÍTULO 7. ALGORITMOS EXATOS 116 instância dimensões B&A B&C KM jV j jE j jT j nós tempo nós tempo nós tempo i080-101 80 120 8 9 0.20 1 0.14 1 0.1 i080-241 80 632 16 25 14.83 17 671.92 115 1097.1 i080-311 80 350 20 17 3.64 3 6.50 1 8.6 i080-331 80 160 20 5 0.50 3 0.97 5 1.7 i080-341 80 632 20 3 1.12 1 2.22 1 3.1 i160-201 160 240 24 27 1.05 5 3.70 5 1.9 i160-211 160 812 24 51 37.07 11 743.58 15 1143.5 i160-231 160 320 24 19 2.76 1 2.19 1 2.3 i160-241 160 2544 24 23 83.42 19 13032.69 121 21008.6 i160-301 160 240 40 3 0.22 1 0.5 1 1.1 i160-311 160 812 40 231 192.86 13 394.33 25 687.9 i160-331 160 320 40 11 1.17 1 0.87 1 1.8 i160-341 160 2544 40 283 708.06 19 46622.88 131 54500.7 Tabela 7.3: Comparação entre o branch-and-ascent (B&A) e o branch-and-cut (B&C) Melhorando o desempenho do Branch-and-ascent São duas as principais razões pelas quais o número de nós visitados pelo branch-and-ascent tende a ser maior que o número visitado pelo branch-and-cut. Em primeiro lugar, como os limites inferiores são de pior qualidade, o número de ramicações feitas até que um nó seja podado tende a ser maior. Além disso, a escolha do vértice de branching é mais criteriosa no branch-and-cut, que dispõe dos valores primais associados às variáveis. Podem-se tomar medidas para amenizar essas diculdades. Uma delas é o uso do strong branching , que normalmente leva a uma árvore de enumeração mais equilibrada. Outra medida interessante consiste em calcular melhores limites primais; anal, limites superiores são tão importantes quanto limites inferiores para a poda de ramos da árvore de enumeração e para a diminuição dos tamanhos dos subproblemas (pela xação por custos reduzidos). É claro que o algoritmo exato obrigatoriamente achará bons limites superiores ao longo da execução, inclusive o valor ótimo. No entanto, o uso de uma boa solução primal desde o começo da execução tende a torná-la signicativamente mais rápida, especialmente para instâncias consideradas difíceis. Para ilustrar esse fato, o branch-and-ascent foi testado em todas as instâncias da série i320 que, segundo a SteinLib, estavam em aberto até janeiro de 2001 (quando foram resolvidas pela primeira vez, justamente pelo branch-and-ascent [44]). Foram utilizadas nas execuções soluções primais fornecidas pela metaeurística HGP+PR, apresentada no Capítulo 5. Além disso, utilizou-se strong-branching : no nó-raiz (profundidade zero) testavam-se dez diferentes alternativas; em nós de profundidade 1, nove alternativas; na profundidade 2, oito alternativas; e assim sucessivamente. A partir da profundidade 10, utilizou-se branching simples. A Tabela 7.4 mostra os resultados obtidos. CAPÍTULO 7. ALGORITMOS EXATOS 117 instância jV j jE j jT j ótimo nós prof. tempo (s) i320-215 320 1845 34 8015 827 15 1310 i320-223 320 51040 34 6695 1 0 42 i320-224 320 51040 34 6694 1 0 37 i320-225 320 51040 34 6691 1 0 43 i320-241 320 10208 34 7027 15 7 109 i320-242 320 10208 34 7072 59 25 543 i320-243 320 10208 34 7044 31 13 324 i320-244 320 10208 34 7078 61 16 570 i320-245 320 10208 34 7046 21 9 253 i320-311 320 1845 80 17945 33529 29 63279 i320-312 320 1845 80 18122 99311 30 165981 i320-313 320 1845 80 17991 44197 39 79373 i320-314 320 1845 80 18088 70390 44 125664 i320-315 320 1845 80 17987 113494 41 192909 i320-321 320 51040 80 15648 5 2 309 i320-322 320 51040 80 15646 11 4 571 i320-323 320 51040 80 15654 3 1 299 i320-324 320 51040 80 15667 21 10 1210 i320-325 320 51040 80 15649 3 2 430 i320-341 320 10208 80 16296 4007 54 44630 i320-342 320 10208 80 16228 117 15 2629 i320-343 320 10208 80 16281 789 42 10354 i320-344 320 10208 80 16295 1549 34 17944 i320-345 320 10208 80 16289 3025 54 30263 Tabela 7.4: Resultados do branch-and-ascent com strong-branching nas 24 instâncias da série i320 não resolvidas até janeiro de 2001, segundo a SteinLib CAPÍTULO 7. ALGORITMOS EXATOS 118 Repare que a tabela inclui quatro instâncias apresentadas também na Tabela 7.2: i320-241, i320-311, i320-221 e i320-341. Em todos os casos, observaram-se reduções signi- cativas no número de nós visitados e na profundidade máxima da árvore de busca (com destaque para a instância i320-241). Os tempos de execução também foram signicativamente reduzidos, mas é importante lembrar que eles não incluem o cálculo da solução primal inicial (com a metaeurística HGP+PR). De qualquer forma, para as instâncias realmente difíceis, i320-311 e i320-341, o tempo gasto com as heurísticas primais é desprezível (alguns minutos). Para a solução de instâncias em aberto, portanto, uma execução prévia de uma metaeurística é uma boa estratégia. 7.4.2 OR-Library Para a classe OR-Library, os resultados obtidos pelos algoritmos propostos serão comparados com os apresentados em [32] e [46] (em [58], apenas instâncias VLSI são consideradas). Como mostrou a Tabela 6.3, 51 das 57 instâncias dessa série2 são completamente resolvidas pelas heurísticas duais. Nesta seção, serão consideradas apenas as seis instâncias remanescentes: c18, d18, d19, e13, e18 e e19. Um dado importante a respeito dessas instâncias é o fato de que em todas o gap de dualidade é inferior a uma unidade. Como os algoritmos baseados em branch-and-cut resolvem completamente a relaxação linear do problema, todos eles são capazes de resolver todas as instâncias em um único nó. Só ocorrerá branching se a solução ótima não for encontrada no nó-raiz. Entre as instâncias testadas, a única que não teve a solução ótima detectada no primeiro nó foi a e18: tanto o B&C quanto KM zeram branching (PD foi o único algoritmo capaz de encontrar a solução na raiz da árvore de resolução). A Tabela 7.5 mostra os tempos de execução obtidos pelos algoritmos. Como os valores mostrados para os algoritmos de Koch e Martin (KM) e de Polzin e Daneshmand (PD) incluem a etapa de pré-processamento, a tabela reproduz os tempos de pré-processamento mostrados no Apêndice A na coluna prep.. Deve-se, portanto, considerar que o tempo total do algoritmo aqui proposto é a soma das colunas prep. e B&C. Para estas instâncias, o algoritmo KM foi executado em uma Sun SPARC 20/71; o algoritmo PD, em um Pentium de 300 MHz; e o B&C, em uma Sun Ultra de 167 MHz (exceto a fase de pré-processamento, executada em um AMD K6-2 de 350 MHz). A tabela mostra que o algoritmo aqui proposto tem desempenho claramente superior ao de KM. Contribui para isso o fato de que o B&C emprega técnicas de pré-processamento mais completas, introduzidas em [58]. Além disso, o fato de o programa linear ser inicializado com uma solução dual heuristicamente obtida (o que não é feito em KM) acelera a signicativamente convergência do B&C. Já o algoritmo PD tem, em geral, desempenho superior ao do B&C. A razão básica é o conjunto de métodos de redução utilizados por Polzin e Daneshmand. Conforme já 2 Na verdade são 60 instâncias no total, mas três são resolvidas pelo pré-processamento. CAPÍTULO 7. ALGORITMOS EXATOS instância dimensões ótimo tempo (s) jV j jE j jT j prep. + B&C KM c18 500 12500 83 113 8.1 + 5.0 104.8 d18 1000 25000 167 223 38.2 + 6.4 308.0 d19 1000 25000 310 310 45.2 + 1.6 868.8 e13 2500 12500 417 1280 104.1 + 45.3 2816.3 e18 2500 62500 417 564 286.4 + 1757.0 68949.1 e19 2500 62500 625 758 252.8 + 16.6 4581.5 Tabela 7.5: Resultados do branch-and-cut para a classe OR-Library 119 PD 3.0 1.9 1.5 4.3 74.1 14.0 mencionado na Seção 2.5, os autores propõem implementações muito rápidas dos testes disponíveis na literatura. Além disso, eles também fazem uso de técnicas de redução baseadas em dualidade (limites superiores e inferiores), que são extremamente ecientes para instâncias como as dessa classe, com gap muito pequeno. Se for desconsiderado o tempo dedicado pelo algoritmo de B&C às técnicas de pré-processamento, ele se torna mais competitivo. A única exceção é a instância e18: conforme já mencionado, o método PD foi o único capaz de encontrar a solução ótima no nó-raiz e, portanto, não precisou fazer branching. Os autores apresentam algumas heurísticas primais que são muito ecientes para essa classe de instâncias, pois também se baseiam fortemente em informações de dualidade. As mesmas instâncias foram testadas também com o algoritmo de branch-and-ascent. Os resultados da execução desse algoritmo em um Pentium II de 400 MHz são apresentados na Tabela 7.6. Observe que as dimensões apresentadas na tabela referem-se às instâncias pré-processadas, sobre as quais foi aplicado o algoritmo. O tempo de pré-processamento não foi considerado nesse teste. instância jV j jE j jT j ótimo nós prof. tempo c18 391 1044 50 113 21 6 6.2 d18 813 2284 97 223 3 1 4.2 d19 700 1932 100 310 3 1 0.5 e13 1418 2962 232 1280 809 38 1563.0 e18 2219 6185 248 564 e19 1156 2757 174 758 Tabela 7.6: Branch-and-ascent com strong branching para a classe OR-Library A tabela mostra que, para as instâncias c18, d18 e d19, o desempenho do algoritmo é comparável ao do B&C. Para as demais, é muito pior: as instâncias e18 e e19 não puderam sequer ser resolvidas (em uma hora, o tempo máximo permitido nesse caso). Percebe-se que, quando aumenta o número de vértices do grafo, é grande o risco de o branch-and-ascent gerar árvores profundas demais, mesmo quando o strong branching é CAPÍTULO 7. ALGORITMOS EXATOS 120 utilizado; nesses casos, o branch-and-cut deve ser o método preferido. 7.4.3 VLSI Das instâncias aqui analisadas, a classe VLSI é a que possui o maior número de vértices. Assim sendo, o método escolhido para sua análise é o branch-and-cut. Esse método é comparado apenas com os algoritmos de Polzin e Daneshmand (PD) e de Uchoa, Poggi de Aragão e Ribeiro (UPR). O algoritmo de Koch e Martin fornece resultados signicativamente piores que os apresentados por esses dois métodos. O B&C foi executado sobre as instâncias pré-processadas pela implementação apresentada em [58] (que fornece resultados ligeiramente diferentes dos apresentados no Apêndice A), para permitir uma melhor comparação entre esses dois métodos. A Tabela 7.7 mostra os resultados obtidos para todas as instâncias que não foram resolvidas apenas por pré-processamento e heurísticas duais. Os tempos apresentados são tempos totais, incluindo o pré-processamento. Tanto o B&C quanto UPR foram executados em uma Sun Ultra 1; o algoritmo PD foi executado em um Pentium de 300 MHz. Quando se comparam os algoritmos B&C e UPR, observa-se que o primeiro é claramente superior ao segundo. Como a maioria das instâncias dessa série tem gap de dualidade muito pequeno, raramente há branching e, quando há, a árvore não é profunda [58]. O gargalo de ambos os algoritmos é o cálculo da relaxação linear, o que indica que as grandes diferenças observadas entre seus tempos de execução devem-se basicamente ao fato de que B&C usa heurísticas duais para inicializar o programa linear.3 Há também razões secundárias: a rotina de separação utilizada pelo B&C é mais eciente que a rotina utilizada no algoritmo UPR. A comparação entre o B&C e PD é mais equilibrada: ora um dos algoritmos tem melhor desempenho, ora o outro. Nos dois sentidos, há casos em que um método é de uma a duas ordens de grandeza mais rápido que o outro. O algoritmo PD utiliza muitos testes de redução baseados em limites (bound-based reductions ), ecazes apenas quando a diferença entre o valor dual e o valor primal conhecidos é pequena. Assim, PD parece ser mais adequado nos casos em que o dual ascent fornece soluções muito próximas do valor ótimo. Nas demais situações, o B&C tem vantagens. Em primeiro lugar, ele utiliza durante a fase de pré-processamento alguns testes concebidos justamente para instâncias VLSI [58], sendo que todos eles se baseiam apenas em implicações lógicas (o gap de dualidade é irrelevante). Além disso, a rotina de separação implementada parece ser particularmente eciente, acelerando a convergência do algoritmo. A Tabela 7.7 mostra que, para quatro das instâncias propostas, o B&C foi o único Na verdade, para algumas instâncias o gargalo é o pré-processamento, mas ele é idêntico nos dois algoritmos; portanto, não pode ser ele o responsável pelas grandes diferenças. 3 CAPÍTULO 7. ALGORITMOS EXATOS instância dimensões ótimo tempo (s) jV j jE j jT j B&C UPR PD alue3146 3626 5869 64 2240 15.6 20 22.5 alue5345 5179 8165 68 3507 327 3039 1136 alue5623 4472 6938 68 3413 138.8 1755 1298 alue5901 11543 18429 68 3912 119.7 1367 653 alue6179 3372 5213 67 2452 51.7 161 4.17 alue6457 3932 6137 68 3057 101.3 645 4.49 alue6735 4119 6696 68 2696 47.7 129 18.8 alue6951 2818 4419 67 2386 110.8 167 22.1 alue7065 34046 54841 544 23881 163123 alue7066 6405 10454 16 2256 5923 9871 1666 alue7080 34479 55494 2344 62449 18476 alut0805 1160 2089 34 958 1.1 2 1.8 alut1181 3041 5693 64 2353 27.4 82 358 alut2010 6104 11011 68 3307 78.7 143 29.1 alut2288 9070 16595 68 3843 324.8 1459 1078 alut2566 5021 9055 68 3073 103.1 876 604 alut2610 33901 62816 204 12239 1810410 alut2625 36711 68117 879 35459 671406 diw0779 11821 22516 50 4440 1704 35467 1298 diw0820 11749 22384 37 4167 1984 35946 159 dmxa0368 2050 3676 18 1017 2.2 3 1.1 dmxa1801 310 553 17 1365 40.8 120 0.8 gap3128 10393 18043 104 4292 782 2135 23.3 msm2846 3263 5783 89 3135 59.7 275 292 msm4312 5181 8893 10 2016 250.5 1498 10.6 taq0014 6466 11046 128 5326 393 10797 1556 taq0377 6836 11715 136 6393 834 27684 6486 taq0903 6163 10490 130 5099 934 14368 16056 Tabela 7.7: Resultados obtidos para instâncias VLSI 121 CAPÍTULO 7. ALGORITMOS EXATOS 122 dos algoritmos apresentados capaz de resolvê-las. Os tempos de execução, no entanto, foram consideravelmente altos: 2 dias para a instância alue7065, 5 horas para a alue7080, 21 dias para a instância alut2610 e 8 dias para alut2625.4 Os altos tempos de execução se justicam porque essas instâncias não haviam sido previamente resolvidas; o branchand-cut proposto nesta dissertação (apresentado originalmente em [44]) foi o primeiro a fazê-lo. A Figura 7.1, que apresenta a solução exata da instância alue7080, deixa bem clara a não-trivialidade da tarefa. 7.5 Conclusão Os algoritmos exatos propostos neste capítulo, branch-and-cut e branch-and-ascent, estão entre os mais ecientes descritos na literatura. O algoritmo de branch-and-ascent mostrouse extremamente eciente para instâncias com poucos nós. Foi ele o primeiro algoritmo capaz de resolver completamente a série i320, de Incidência. Para instâncias com mais nós, o branch-and-cut é a melhor alternativa. Ele foi o primeiro algoritmo capaz de resolver todas as instâncias da classe VLSI. A eciência dos métodos é inegável, mas é possível melhorar ainda mais os resultados obtidos. Considere, por exemplo, a rotina de pré-processamento utilizada. Seria interessante reimplementar alguns dos testes utilizando as sugestões apresentadas em [46] (preservando os testes introduzidos em [58], responsáveis por signicativas reduções adicionais). Dessa forma, haveria sensível melhora nos tempos de execução, notadamente para as instâncias da série OR-Library. Além disso, seria interessante aplicar os testes de redução também nos nós internos da árvore de resolução. Na versão atual, elas se aplicam somente ao problema original, no nó-raiz. Uma estratégia que poderia ser utilizada para acelerar a convergência do branch-andcut é uma utilização mais freqüente das heurísticas duais. Na implementação discutida ao longo deste capítulo, as heurísticas duais (dual ascent, dual adjustment e xação ativa) são utilizadas apenas para inicializar o programa linear. Depois disso, a solução da relaxação linear é feita da forma tradicional: reotimizações do programa linear (com um resolvedor apropriado) intercaladas com rotinas de separação. Nada impede, no entanto, que as heurísticas duais (dual adjustment, xação ativa, scaling ou mesmo o próprio dual ascent ) sejam executadas tendo como solução inicial uma solução fornecida pelo resolvedor LP. Testes preliminares indicaram que há situações em que a convergência do algoritmo é mais rápida. Não foi possível, no entanto, determinar uma estratégia que garantisse a aceleração de forma sistemática. Isso merece, sem dúvida, uma investigação mais minuciosa. 4 As instâncias alut2610 e alut2625 foram processadas por versão preliminar do algoritmo de branchand-cut, similiar à utilizada em [58], mas inicializada com os cortes fornecidos pelo dual ascent. Não foi utilizada a xação por custos reduzidos estendidos nem a rotina de separação baseada no algoritmo de Hao-Orlin, que sem duvida contribuiriam para diminuir os tempos de execução. CAPÍTULO 7. ALGORITMOS EXATOS Figura 7.1: Solução ótima da instância alue7080 123 CAPÍTULO 7. ALGORITMOS EXATOS 124 Outro aspecto a se considerar são os critérios para escolha do vértice de branching no branch-and-ascent. Mesmo com o strong branching, em alguns casos a árvore de resolução é extremamente desbalanceada. Um critério de branching mais adequado poderia contribuir signicativamente para diminuir o número de nós visitados. Capítulo 8 Conclusão Esta dissertação apresentou diversas estratégias para a solução de um único problema, o de Steiner em grafos. Foram implementadas rotinas de pré-processamento, heurísticas (e metaeurísticas) primais, heurísticas duais e métodos de enumeração implícita, baseados ou não em programação linear. Conforme evidenciaram os experimentos computacionais, cada um desses métodos contribui de forma particular para a solução do problema, seja de forma exata, seja de forma aproximada. Havendo um conjunto tão amplo de ferramentas à disposição, uma questão é inevitável: qual delas é a melhor? Não há uma resposta denitiva. A escolha depende da instância a ser resolvida, do tempo que se pode dedicar à sua resolução e da qualidade desejada. Considere duas instâncias em especial, i320-311 e alue7080. Ambas foram resolvidas à otimalidade pela primeira vez por algoritmos apresentados nesta dissertação. A primeira, uma instância de Incidência, foi resolvida pelo branch-and-ascent ; a segunda, da classe VLSI, foi resolvida pelo branch-and-cut. Ambas podem ser consideradas difíceis, mas por razões diferentes: a instância de Incidência, pelo gap de dualidade; a VLSI, por seu tamanho. A Tabela 8.1 mostra os resultados obtidos para a instância i320-311 por diversos dos métodos estudados. Para cada um deles, apresenta-se o tempo de execução, o valor da solução primal obtida e o desvio desse valor em relação ao ótimo. A ordem de apresentação é determinada pelos tempos de execução. A tabela deixa claro o que foi dito ao longo de toda a dissertação: aumentos na qualidade da solução obtida normalmente requerem aumentos no tempo de execução. Nesse caso, é possível obter uma solução heurística a 26% do ótimo em pouco mais de três milésimos de segundo. Encontrar a solução ótima com a heurística HGP+PR requer cerca de quatro minutos, quase 100 mil vezes mais tempo. A prova da otimalidade (com o branch-and-ascent ) requer cerca de um dia de processamento. A Tabela 8.2 é análoga, mas para a instância alue7080 (após o pré-processamento). Uma comparação com a Tabela 8.1 deixa claro que a ecácia de cada método depende da 125 CAPÍTULO 8. CONCLUSÃO algoritmo Bor·vka Prim-D Kruskal-V Prim-D + busca local NP Prim-D + busca local NPK dual ascent + busca local NP HGP (128 iterações) HGP+PR (128 it., 10 sol. elite, relig. adaptativo) HGP (256 iterações) HGP+PR (256 it., 32 sol. elite, relig. adaptativo) HGP+PR (256 it., 32 sol. elite, relig. perturb.) branch-and-ascent 126 tempo (s) valor erro% 3:4 10 3 4:8 10 3 4:9 10 3 2:1 10 1 2:1 100 3:2 100 2:7 101 3:4 101 5:3 101 1:3 102 2:6 102 6:3 104 22621 23160 23418 18970 18924 18503 18307 18073 18307 18012 17945 17945 26.06 29.06 30.50 5.71 5.46 3.11 2.02 0.71 2.02 0.37 0 Tabela 8.1: Resultados para a instância i320-311 (jV j = 320; jE j = 1845; jT j = 80) instância à qual se aplica. Enquanto uma metaeurística (HGP+PR) é capaz de encontrar a solução ótima de i320-311 em um centésimo do tempo do algoritmo exato, para a instância alue7080 uma metaeurística mais simples (HGP) precisa de um tempo maior que o do algoritmo exato para ser executada e encontra uma solução pior. Nem sempre uma metaeurística é uma boa alternativa ao algoritmo exato. algoritmo Prim-D Bor·vka Kruskal-V Prim-D + busca local NP dual ascent + busca local NP Prim-D + busca local NPK branch-and-cut HGP (128 iterações) HGP+PR (128 it., 10 sol. elite, relig. adaptativo) tempo 0:7 10 1 1:0 10 1 1:5 10 1 1:8 102 2:6 102 2:0 103 1:7 104 2:4 104 4:3 104 valor erro (%) 64305 63956 63922 62734 62598 62577 62449 62653 62555 2.97 2.41 2.36 0.46 0.24 0.20 0.33 0.17 Tabela 8.2: Resultados para a instância alue7080 (jV j = 9272; jE j = 16019; jT j = 1402) Outro exemplo de distinção: a busca por nós-chaves K é muito mais ecaz para a instância alue7080 que para a i320-311. No primeiro caso, uma única iteração da busca NPK encontra um resultado melhor que o HGP com busca NP (em um décimo do tempo). No segundo, a busca NPK é praticamente equivalente à busca NP, mas dez vezes mais lenta. Esses exemplos mostram que não há um algoritmo que seja inequivocamente melhor que todos os outros: cada um tem seus pontos fracos e seus pontos fortes. É isso que justica a implementação dos diversos métodos discutidos ao longo desta dissertação. Da CAPÍTULO 8. CONCLUSÃO 127 integração desses métodos, surge uma estratégia robusta para o tratamento do problema de Steiner em grafos. Apêndice A Resultados do Pré-processamento As tabelas desta seção mostram o resultado da aplicação dos procedimentos de redução citados na Seção 2.5 sobre as instâncias das classes OR-Library e VLSI. Os procedimentos não foram aplicados sobre as instâncias de incidência e sobre a classe PUC, já que seu efeito sobre elas é desprezível. As tabelas mostram as dimensões de cada instância antes e depois da redução, bem como o tempo necessário para a execução das rotinas de préprocessamento. As execuções foram feitas em um AMD K6-2 de 350 MHz. 128 APÊNDICE A. RESULTADOS DO PRÉ-PROCESSAMENTO instância tempo grafo original (s) c01 c02 c03 c04 c05 c06 c07 c08 c09 c10 c11 c12 c13 c14 c15 c16 c17 c18 c19 c20 0.09 0.15 0.17 0.15 0.09 0.62 1.16 0.99 1.13 0.22 11.21 8.76 3.59 1.33 0.58 31.38 13.26 8.09 8.53 3.22 jV j 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 jEj 625 625 625 625 625 1000 1000 1000 1000 1000 2500 2500 2500 2500 2500 12500 12500 12500 12500 12500 jT j 5 10 83 125 250 5 10 83 125 250 5 10 83 125 250 5 10 83 125 250 129 reduzido jV j 108 82 55 51 15 353 359 128 132 15 488 454 177 5 4 499 494 391 266 0 Tabela A.1: OR-Library Série C jEj 188 144 90 81 24 795 802 238 241 21 1692 1395 332 7 5 2715 2294 1044 637 0 jT j 5 8 24 24 10 5 9 30 49 10 5 9 37 4 3 5 8 50 40 0 APÊNDICE A. RESULTADOS DO PRÉ-PROCESSAMENTO instância tempo grafo original (s) d01 d02 d03 d04 d05 d06 d07 d08 d09 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 e01 e02 e03 e04 e05 e06 e07 e08 e09 e10 e11 e12 e13 e14 e15 e16 e17 e18 e19 e20 0.49 0.45 0.52 0.43 0.29 2.24 2.06 6.51 2.56 0.79 46.25 31.08 21.28 7.64 1.83 119.03 163.18 38.17 45.21 12.96 1.54 1.40 2.66 2.95 1.52 9.03 8.20 45.90 27.93 5.74 312.55 386.53 104.13 62.14 12.45 1563.92 751.41 286.41 252.80 79.30 jV j 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 2500 jEj 1250 1250 1250 1250 1250 2000 2000 2000 2000 2000 5000 5000 5000 5000 5000 25000 25000 25000 25000 25000 3125 3125 3125 3125 3125 5000 5000 5000 5000 5000 12500 12500 12500 12500 12500 62500 62500 62500 62500 62500 jT j 5 10 167 250 500 5 10 167 250 500 5 10 167 250 500 5 10 167 250 500 5 10 417 625 1250 5 10 417 625 1250 5 10 417 625 1250 5 10 417 625 1250 130 reduzido jV j jEj jT j 234 433 5 255 459 10 31 48 18 24 36 14 24 37 16 746 1694 5 725 1647 10 287 515 84 69 116 34 47 78 30 975 3749 5 956 3061 9 423 828 84 79 137 32 23 37 14 1000 6725 5 1000 6332 10 813 2284 97 700 1932 100 0 0 0 657 1239 5 678 1256 9 120 195 64 50 82 31 16 25 11 1830 4277 5 1876 4321 10 894 1714 196 506 899 187 89 152 51 2487 10861 5 2462 9836 10 1418 2962 232 339 589 119 26 41 17 2500 19698 5 2500 17045 10 2119 6185 248 1156 2757 174 0 0 0 Tabela A.2: OR-Library Séries D e E APÊNDICE A. RESULTADOS DO PRÉ-PROCESSAMENTO instância tempo grafo original (s) alue2087 alue2105 alue3146 alue5067 alue5345 alue5623 alue5901 alue6179 alue6457 alue6735 alue6951 alue7065 alue7066 alue7080 alue7229 alut0787 alut0805 alut1181 alut2010 alut2288 alut2566 alut2610 alut2625 alut2764 0.85 0.89 13.03 28.86 53.89 27.34 68.48 26.18 44.25 24.23 12.33 2233.35 324.77 1021.30 0.75 0.33 1.06 7.03 38.40 299.47 34.24 4359.47 1843.85 0.02 jV j 1244 1220 3626 3524 5179 4472 11543 3372 3932 4119 2818 34046 6405 34479 940 1160 966 3041 6104 9070 5021 33901 36711 387 jEj jT j 131 reduzido jV j jEj jT j 1971 34 36 58 13 1858 34 7 10 3 5869 64 113 187 29 5560 68 305 509 42 8165 68 1191 2012 64 6938 68 807 1377 58 18429 68 1077 1847 58 5213 67 480 817 51 6137 68 849 1451 57 6696 68 360 592 49 4419 67 473 790 58 54841 544 13073 23017 445 10454 16 1791 3151 9 55494 2344 9272 16019 1402 1474 34 0 0 0 2089 34 9 13 5 1666 34 92 154 23 5693 64 234 412 42 11011 68 435 742 45 16595 68 1224 2195 59 9055 68 715 1242 62 62816 204 10760 20185 182 68117 879 12196 22457 764 626 34 0 0 0 Tabela A.3: VLSI Séries alue e alut APÊNDICE A. RESULTADOS DO PRÉ-PROCESSAMENTO instância tempo grafo original (s) diw0234 diw0250 diw0260 diw0313 diw0393 diw0445 diw0459 diw0460 diw0473 diw0487 diw0495 diw0513 diw0523 diw0540 diw0559 diw0778 diw0779 diw0795 diw0801 diw0819 diw0820 taq0014 taq0023 taq0365 taq0377 taq0431 taq0631 taq0739 taq0741 taq0751 taq0891 taq0903 taq0910 taq0920 taq0978 jV j jEj jT j 132 reduzido jV j jEj jT j 248.20 5349 10086 25 860 1599 21 0.02 353 608 11 0 0 0 0.15 539 985 12 0 0 0 0.03 468 822 14 0 0 0 0.03 212 381 11 0 0 0 1.34 1804 3311 33 31 49 11 1.08 3636 6789 25 16 25 7 0.02 339 579 13 0 0 0 0.75 2213 4135 25 14 22 6 1.73 2414 4386 25 4 5 3 0.21 938 1655 10 0 0 0 0.07 918 1684 10 0 0 0 0.05 1080 2015 10 0 0 0 0.03 286 465 10 0 0 0 10.52 3738 7013 18 27 45 9 42.35 7231 13727 24 178 318 15 1046.62 11821 22516 50 2387 4508 37 84.62 3221 5938 10 290 529 9 90.35 3023 5575 10 393 719 10 468.78 10553 20066 32 1186 2214 22 1626.92 11749 22384 37 1891 3563 32 137.69 6466 11046 128 1766 3155 86 0.85 572 963 11 48 80 8 35.84 4186 7074 22 998 1797 21 103.39 6836 11715 136 2040 3603 118 2.77 1128 1905 13 123 216 10 0.07 609 932 10 7 9 4 2.41 837 1438 16 73 121 12 1.91 712 1217 16 107 183 14 2.23 1051 1791 16 136 233 15 0.02 331 560 10 0 0 0 96.09 6163 10490 130 1851 3294 90 0.06 310 514 17 0 0 0 0.00 122 194 17 0 0 0 0.07 777 1239 10 0 0 0 Tabela A.4: VLSI Séries diw e taq APÊNDICE A. RESULTADOS DO PRÉ-PROCESSAMENTO instância tempo grafo original (s) dmxa0296 dmxa0368 dmxa0454 dmxa0628 dmxa0734 dmxa0848 dmxa0903 dmxa1010 dmxa1109 dmxa1200 dmxa1304 dmxa1516 dmxa1721 dmxa1801 gap1307 gap1413 gap1500 gap1810 gap1904 gap2007 gap2119 gap2740 gap2800 gap2975 gap3036 gap3100 gap3128 jV j jEj jT j 133 reduzido jV j jEj 0.02 233 386 12 0 0 1.27 2050 3676 18 47 76 0.55 1848 3286 16 15 22 0.01 169 280 10 0 0 0.10 663 1154 11 0 0 0.74 499 861 16 37 60 1.90 632 1087 10 58 99 1.46 3983 7108 23 0 0 0.13 343 559 17 9 13 0.31 770 1383 21 32 48 0.04 298 503 10 0 0 0.12 720 1269 11 0 0 0.24 1005 1731 18 6 8 16.83 2333 4137 17 354 641 0.19 342 552 17 0 0 0.04 541 906 10 0 0 0.02 220 374 17 0 0 0.09 429 702 17 0 0 2.39 735 1256 21 50 84 1.11 2039 3548 17 36 61 1.53 1724 2975 29 0 0 0.60 1196 2084 14 33 56 0.03 386 653 12 0 0 0.02 179 293 10 0 0 0.16 346 583 13 28 42 1.63 921 1558 11 14 22 390.94 10393 18043 104 2251 4091 Tabela A.5: VLSI Séries dmxa e gap jT j 0 9 5 0 0 11 8 0 5 13 0 0 4 17 0 0 0 0 11 9 0 5 0 0 9 7 62 APÊNDICE A. RESULTADOS DO PRÉ-PROCESSAMENTO instância tempo grafo original (s) msm0580 msm0654 msm0709 msm0920 msm1008 msm1234 msm1477 msm1707 msm1844 msm1931 msm2000 msm2152 msm2326 msm2492 msm2525 msm2601 msm2705 msm2802 msm2846 msm3277 msm3676 msm3727 msm3829 msm4038 msm4114 msm4190 msm4224 msm4312 msm4414 msm4515 0.23 0.18 1.06 1.14 0.17 0.23 0.51 0.01 0.00 0.15 0.14 6.99 0.07 7.64 1.54 18.04 0.24 0.50 15.68 0.28 0.16 14.39 46.54 0.03 0.04 0.03 0.03 292.91 0.03 0.37 jV j 338 1290 1442 752 402 933 1199 278 90 875 898 2132 418 4045 3031 2961 1359 1709 3263 1704 957 4640 4221 237 402 391 191 5181 317 777 jEj 541 2270 2403 1264 695 1632 2078 478 135 1522 1562 3702 723 7094 5239 5100 2458 2963 5783 2991 1554 8255 7255 390 690 666 302 8893 476 1358 jT j 134 reduzido jV j jEj 11 12 17 10 0 0 16 42 68 26 13 18 11 13 18 13 7 9 31 12 17 11 0 0 10 6 8 10 0 0 10 0 0 37 176 309 14 9 12 12 56 91 12 11 15 16 176 303 13 4 5 18 9 14 89 395 682 12 0 0 10 0 0 21 45 74 12 400 705 11 0 0 16 0 0 16 0 0 11 0 0 10 1342 2433 11 0 0 13 31 50 Tabela A.6: VLSI Série msm jT j 6 0 8 7 6 4 6 0 4 0 0 23 5 10 6 12 3 5 61 0 0 4 10 0 0 0 0 10 0 8 Bibliograa [1] E. Aarts e J. K. Lenstra, editores. Local Search in Combinatorial Optimization . Wiley, 1997. [2] R. Ahuja, T. Magnanti e J. Orlin. Network Flows: Theory, algorithms, and applications . Prentice-Hall, 1993. [3] M. P. Bastos e C. C. Ribeiro. Reactive tabu search with path-relinking for the Steiner problem in graphs. Em Essays and Surveys in Metaheuristics , editado por C. C. Ribeiro e P. Hansen, págs. 3958. Kluwer, 2001. [4] J. Beasley. OR-Library: Distributing test problems by electronic mail. Journal of the Operational Research Society , 41:10691072, 1990. http://mscmga.ms.ic.ac.uk/info.html. [5] M. de Berg, M. van Kreveld, M. Overmars e O. Schwarzkopt. Computational Geometry: Algorithms and Applications . Springer-Verlag, 1997. [6] O. Bor·vka. P°íspev¥k k °e²ení otázky ekonomické stavby elektrovodních sítí. Elektrotechnicky Obzor , 15:153154, 1926. [7] V. Chvátal. Linear Programming . Freeman, 1983. [8] A. Claus e N. Maculan. Une nouvelle formulation du problème de Steiner sur un graphe. Relatório Técnico 280, Centre de Recherche sur les Transports, Université de Montréal, 1983. [9] T. Cormen, C. Leiserson e R. Rivest. Introduction to Algorithms . MIT Press/McGraw Hill, 1990. [10] E. W. Dijkstra. A note on two problems in connexion with graphs. Numerische Mathematik , 1:269271, 1959. [11] K. Dowsland. Hill-climbing, simulated annealing and the Steiner problem in graphs. Engineering Optimization , 17:91107, 1991. [12] C. Duin. Steiner's Problem in Graphs: Aproximation, reduction, variation . Tese de Doutorado, Institute for Actuarial Science and Economics, University of Amsterdam, 1993. 135 BIBLIOGRAFIA 136 [13] C. Duin, 2001. Personal communication. [14] C. Duin e A. Volgenant. Reduction tests for the Steiner problem in graphs. Networks , 19:549567, 1989. [15] C. Duin e S. Voss. Ecient path and vertex exchange in Steiner tree algorithms. Networks , 29:89105, 1997. [16] C. Duin e S. Voss. The Pilot method: A strategy for heuristic repetition with application to the Steiner problem in graphs. Networks , 34:181191, 1999. [17] J. Edmonds. Optimum branchings. Journal of Research of the National Bureau of Standards , B71:233240, 1967. [18] H. Esbensen. Computing near-optimal solutions to the steiner problem in a graph using a genetic algorithm. Networks , 26:173185, 1995. [19] T. A. Feo e M. G. Resende. Greedy randomized adaptive search procedures. Journal of Global Optimization , 6:109133, 1995. [20] M. L. Fredman e R. E. Tarjan. Fibonacci heaps and their uses in improved network optimization algorithms. Journal of the ACM , 34(3):596615, 1987. [21] M. Gendreau, J.-F. Larochelle e B. Sansó. A tabu search heuristic for the Steiner tree problem. Networks , 34:163172, 1999. [22] M. Goemans. The Steiner tree polytope and related polyhedra. Mathematical Programming , 63:157182, 1994. [23] A. V. Goldberg e R. E. Tarjan. Expected performance of dijkstra's shortest path algorithm. Relatório técnico, Computer Science Department, Princeton University, 1996. [24] B. L. Golden e W. R. Stewart. Probabilistic analysis of heuristics. Em The Travelling Salesman Problem: A Guided Tour of Combinatorial Optimization , editado por E. L. Lawler, J. K. Lenstra, A. H. G. R. Kan e D. B. Shmoys, págs. 207250. Wiley, 1985. [25] M. Grötschel, L. Lovász e A. Schrijver. The ellipsoid method and its consequences in combinatorial optimization. Combinatorica , 1:169197, 1981. [26] M. Grötschel, L. Lovász e A. Schrijver. Corrigendum to our paper The ellipsoid method and its consequences in combinatorial optimization. Combinatorica , 4:291 295, 1984. [27] J. Hao e J. Orlin. A faster algorithm for nding the minimum cut in a directed graph. Journal of Algorithms , 17:424446, 1994. BIBLIOGRAFIA 137 [28] F. Hwang, D. Richards e P. Winter. The Steiner tree problem , volume 53 de Annals of Discrete Mathematics . North-Holland, Amsterdam, 1992. [29] ILOG. Using the CPLEX Callable Library Version 5 , 1997. [30] A. Kapsalis, V. J. Rayward-Smith e G. D. Smith. Solvign the graphical steiner tree problem using genetic algorithms. Journal of the Operational Research Society , 44:397406, 1993. [31] R. Karp. Reducibility among combinatorial problems. Em Complexity of Computer Computations , editado por R. Miller e J. Thatcher, págs. 85103. Plenum Press, New York, 1972. [32] T. Koch e A. Martin. Solving Steiner tree problems in graphs to optimality. Networks , 32:207232, 1998. [33] T. Koch, A. Martin e S. Voss. SteinLib: An updated library on Steiner tree problems in graphs. Relatório Técnico ZIB-Report 00-37, Konrad-Zuse-Zentrum für Informationstechnik Berlin, 2000. http://elib.zib.de/steinlib. [34] J. B. Kruskal. On the shortest spanning tree of a graph and the traveling salesman problem. Proceedings of the American Mathematical Society , 7:4850, 1956. [35] A. Lucena e J. Beasley. A branch and cut algorithm for the Steiner problem in graphs. Networks , 31:3959, 1998. [36] N. Maculan. The Steiner problem in graphs. Annals of Discrete Mathematics , 31:185 212, 1987. [37] U. Manber. Introduction to Algorithms: A Creative Approach . Addison-Wesley, 1989. [38] S. L. Martins, P. M. Pardalos, M. G. C. Resende e C. C. Ribeiro. A parallel GRASP for the Steiner tree problem in graphs using a hybrid local search strategy. Journal of Global Optimization , 17:267283, 2000. [39] K. Melhorn. A faster approximation algorithm for the Steiner problem in graphs. Information Processing Letters , 27:125128, 1988. [40] M. Minoux. Ecient greedy heuristics for Steiner tree problems using reoptimization and supermodularity. INFOR, 28:221233, 1990. [41] B. M. E. Moret e H. D. Shapiro. An empirical assessment of algorithms for constructing a minimum spanning tree. DIMACS Monographs in Discrete Mathematics and Theoretical Computer Science , 15:99117, 1994. [42] G. Nemhauser e L. Wolsey. Integer and Combinatorial Optimization . Wiley, New York, 1988. BIBLIOGRAFIA 138 [43] M. Poggi de Aragão, C. C. Ribeiro, E. Uchoa e R. F. Werneck. Hybrid local search for the Steiner problem in graphs. Em Extended Abstracts of the 4th Metaheuristics International Conference , págs. 429433, Porto, Portugal, 2001. [44] M. Poggi de Aragão, E. Uchoa e R. F. Werneck. Dual heuristics on the exact solution of large Steiner problems. Em Proceedings of the Brazilian Symposium on Graphs, Algoritms and Combinatorics , volume 7 de Electronic Notes in Discrete Mathematics , Fortaleza, Brazil, 2001. [45] T. Polzin e S. V. Daneshmand. A comparison of Steiner tree relaxations. Discrete Applied Mathematics , 112(13):241261, 2001. [46] T. Polzin e S. V. Daneshmand. Improved algorithms for the Steiner problem in networks. Discrete Applied Mathematics , 112(13):263300, 2001. [47] F. P. Preparata e M. I. Shamos. Computational Geometry: An Introduction . Springer-Verlag, 1985. [48] R. C. Prim. Shortest connection networks and some generalizations. Bell System Tech. Journal , 36:13891401, 1956. [49] C. C. Ribeiro e M. C. Souza. Tabu search for the Steiner problem in graphs. Networks , 36:138146, 2000. [50] C. C. Ribeiro, E. Uchoa e R. F. Werneck. A hybrid GRASP with perturbations and adaptive path-relinking for the Steiner problem in graphs. Em Proceedings of the Workshop on Algorithm Engineering as a New Paradigm , págs. 76116. Kyoto University, 2000. [51] I. Rosseti, M. Poggi de Aragão, C. C. Ribeiro, E. Uchoa e R. F. Werneck. New benchmark instances for the Steiner problem in graphs. Em Extended Abstracts of the 4th Metaheuristics International Conference , págs. 557561, Porto, Portugal, 2001. [52] G. Skorobohatyj. MATHPROG: A collection of codes for solving various mathematical programming problems. http://elib.zib.de/pub/Packages/mathprog/index.html. [53] P. M. Spira e A. Pan. On nding and updating spanning trees and shortest paths. SIAM Journal on Computing , 4(3):375380, 1975. [54] H. Takahashi e A. Matsuyama. An approximate solution for the Steiner problem in graphs. Math. Japonica , 24:573577, 1980. [55] R. E. Tarjan. Eciency of a good but not linear set union algorithm. Journal of the Association for Computing Machinery , 2:212225, 1975. BIBLIOGRAFIA 139 [56] R. E. Tarjan. Data Structures and Network Algorithms . SIAM Press, Philadelphia, PA, 1983. [57] E. Uchoa. Algoritmos para Problemas de Steiner com Aplicações em Projeto de Circuitos VLSI . Tese de Doutorado, Department of Informatics, Catholic University of Rio de Janeiro, 2001. [58] E. Uchoa, M. Poggi de Aragão e C. C. Ribeiro. Preprocessing Steiner problems from VLSI layout. Relatório Técnico MCC 32/99, Department of Informatics, Catholic University of Rio de Janeiro, 1999. [59] M. G. A. Verhoeven, M. E. M. Severens e E. H. L. Aarts. Local search for Steiner trees in graphs. Em Modern Heuristic Search Methods , editado por V. J. Rayward-Smith, I. H. Osman e C. R. Reeves. Wiley, 1996. [60] S. Voss. Steiner-Probleme in Graphen . Anton Hain, 1990. [61] S. Voss. Steiner's problem in graphs: Heuristic methods. Discrete Applied Mathmeatics , 40:4572, 1992. [62] P. Winter. Steiner problems in networks: A survey. Networks , 17:129167, 1987. [63] P. Winter. Reductions for the rectlinear Steiner tree problem. Networks , 26:187198, 1995. [64] L. Wolsey. Integer Programming . Wiley-Interscience, 1998. [65] R. Wong. A dual ascent approach for Steiner tree problems on a directed graph. Mathematical Programming , 28:271287, 1984. Índice corte, 86 custo reduzido, 88 estendido, 94 xação, 89 custos das arestas, 4 desvio relativo percentual, 31 Diagrama de Voronoi, 15 Dijkstra (algoritmo), 7, 16, 92 diversicação, 67, 70 DNH (Distance Network Heuristic ), 15 DNH-Bor·vka, 18 DNH-Prim, 17 DNHz, 19 dual adjustment, 89, 105 dual ascent, 86, 95 dual scaling, 104 dualidade, 88 enumeração implícita, 111 xação por custos reduzidos, 89 xação ativa, 106 formulação por cortes direcionados, 86 FT (metaeurística), 77 gap de dualidade, 85 grafo de distâncias, 15 grafo de saturação, 91 GRASP, 66 heap, 5 binário, 5 Fibonacci, 5 heurísticas construtivas na metaeurística HGP+PR, 68 HGP (metaeurística), 67, 71 Ackermann (função inversa), 6 (; ), 6 arborescência, 86 aresta de fronteira, 16, 26 árvore geradora mínima, 6 algoritmo de Bor·vka, 7 algoritmo de Kruskal, 6 algoritmo de Prim, 6 B&A (algoritmo exato), 116 B&C (algoritmo exato), 116 Bor·vka algoritmo para MST, 7 heurística construtiva, 19 branch-and-ascent, 78, 111, 115 branch-and-bound, 111 branch-and-cut, 111, 112 branching, 112 no branch-and-ascent, 116 no branch-and-cut, 114 strong (forte), 116, 121 variável de, 112 busca local, 47 estratégia geral, 51 implementação, 51 na metaeurística HGP+PR, 69 por caminhos-chaves, 54 por vértices de Steiner, 52 por vértices-chaves, 56 caminhos mínimos dualidade, 91 caminhos mínimos, 7 complementaridade de folga, 88, 105 componente-raiz, 96 critérios de seleção, 98 140 ÍNDICE HGP+PR (metaeurística), 12, 66, 67, 121 implementaçao de referência, 76 resultados experimentais, 76 Incidência (instâncias), 10 intensicação, 67, 70 KM (algoritmo exato), 116 Kruskal algoritmo para MST, 6, 53 heurística construtiva, 25 Kruskal-B, 25 Kruskal-E, 26 Kruskal-V, 28 Método Piloto (metaeurística), 78 metaeurísticas, 66 para o problema de Steiner, 66 MST, ver árvore geradora mínima Multispan, 30 nós cruciais, 48 OR-Library (instâncias), 9 oscilação estratégica, 67 ótimo local, 47, 51, 64 path-relinking, ver religamento PD (algoritmo exato), 116 perturbação na metaeurística HGP+PR, 67, 70 no religamento, 74 vantagens, 83 pré-processamento, 12 Prim algoritmo para MST, 6 heurística construtiva, 21 Prim-D, 22 Prim-T, 21 pruning de uma solução, 30 no branch-and-bound, 112 PUC (instâncias), 11 melhores soluções conhecidas, 12 ramicação, ver branching 141 rank, 32 relaxação linear, 85 religamento, 71 adaptativo, 74 critérios de parada, 75 movimentos complementares, 73 por perturbações, 74 reverse delete step, 97 rotina de separação, 87 RTS (metaeurística), 77 saturação, 88 scaling, 104 separação para o problema de Steiner, 113 Shortest Path Heuristic (SPH), 21 soluções de elite, 71, 72 gerações, 72 lista, 72 solução dual inviável, 88 maximal, 104 solução primal, 85 SPG (Steiner problem in graphs ), 4 SteinLib, 9 melhores soluções conhecidas, 12 strong branching, 116 superaresta, 15 SV (metaeurística), 77 tempo relativo, 36 TS (metaeurística), 77 union-nd, 5 UPR (algoritmo exato), 116 vértices cruciais, 48 vértices de Steiner, 48 vértices-chaves, 48 vizinhança, 47 para o problema de Steiner, 49 por caminhos-chaves, 50 por vértices de Steiner, 49 por vértices-chaves, 50 ÍNDICE VLSI (instâncias), 11 Voronoi, 15 142