Caixeiro Viajante ✈️

Filipe Costa

Francisco Sousa

Universidade Federal do Piauí

Vinicios Castro

Introdução

Definição simples:

Um caixeiro precisa visitar n cidades exatamente uma vez cada e voltar para a cidade de origem, percorrendo a menor distância (ou custo) total possível.

  • É representado como um grafo completo ponderado (todas as cidades têm ligação direta com todas as outras, com um custo/distância).

  • O objetivo é encontrar o ciclo hamiltoniano de menor custo.

Algoritmo

Algoritmo: Caixeiro Viajante

Entrada: Matriz de distâncias entre n elementos.

Inicializar:

    Conjunto de pontos = {1,2,...,n}

    MelhorCusto=∞

    MelhorRota=[ ]

Para cada permutação P de pontos:

    a. Calcular Custo(P)

    b. Se Custo(P)<MelhorCusto:

        MelhorCusto=Custo(P)

Implementação da Instância do Problema

  • O trabalho propõe a implementação e análise computacional de uma instância do Problema do Caixeiro Viajante aplicada ao contexto de deslocamento entre capitais brasileiras.

Estrutura do Grafo e Coordenadas

O problema foi modelado como um grafo completo \(G=(V,E)\), onde:

  • \(V\) representa o conjunto de vértices (capitais)
  • \(E\) representa o conjunto de arestas (ligações entre capitais)
  • Cada aresta possui um peso associado correspondente à distância entre duas capitais

As coordenadas geográficas de cada capital foram utilizadas para calcular as distâncias entre os vértices por meio da fórmula de Haversine, que permite estimar a distância entre dois pontos na superfície terrestre.

Estrutura do Grafo e Coordenadas

Dessa forma, o grafo gerado é:

  • Completo (todas as capitais estão conectadas entre si)
  • Ponderado (cada conexão possui um custo)
  • Não direcionado (distâncias simétricas)

Implementação do Algoritmo de Referência (BASELINE)

A abordagem segue os seguintes passos:

  1. Fixação da cidade inicial (Brasília);

  2. Geração de todas as permutações possíveis das demais capitais;

  3. Construção das rotas completas (ida e volta);

  4. Cálculo da distância total de cada rota;

  5. Seleção da rota com menor custo

Implementação do Algoritmo de Referência (BASELINE)

Análise de Complexidade

O algoritmo de força bruta possui complexidade temporal:

\[ O(n!) \]

É necessário calcular a soma das distâncias entre as cidades, o que possui custo:

\[ O(n) \]

Assim, a complexidade total pode ser representada como:

\[ O(n . n!) \]

Implementação do Algoritmo de Referência (BASELINE)

Resultados

Análise de complexidade e tempo de execução das rotas.
Teste Nº de Capitais Nº de Possibilidades Rotas Testadas % do Espaço Tempo (s)
1 5 120 120 100% 0,01
2 8 40.320 40.320 100% 2,09
3 9 362.880 362.880 100% 21,27
4 10 3.628.800 3.628.000 100% 232,53
5 13 6.227.020.800 20.892.479 0,3355% 1.800
6 27 1,09 × 10²⁸ 67.081.582 ~0,0000% 10.800

Implementação do Algoritmo de Referência (BASELINE)

Resultados

Implementação do Algoritmo de Referência (BASELINE)

Taxa de Processamento

Podemos estimar a taxa média de processamento: Exemplo (teste 13 capitais):

\[ \frac{20892479}{1800} = 11606 \text{ rotas/segundo} \]

Mesmo com essa taxa seriam necessários bilhões de anos para explorar todo o espaço das 27 capitais.

Implementação e Ambiente de Execução

  • A implementação foi realizada em linguagem Python 3.x.
import pandas as pd
import itertools
import time
import math

# ============================================================
# CAIXEIRO VIAJANTE - FORÇA BRUTA COM CONDIÇÃO DE PARADA
# Ponto inicial: Brasília
# Para quando atingir um tempo máximo em minutos
# ============================================================

ARQUIVO_CSV = "matriz_distancias_capitais_brasil.csv"
CAPITAL_INICIAL = "Brasília"

# Quantidade de capitais mais próximas de Brasília que serão testadas
QUANTIDADE_CAPITAIS = 5

# Tempo máximo de execução em minutos
TEMPO_LIMITE_MINUTOS = 180

print("=" * 70)
print("CAIXEIRO VIAJANTE - FORÇA BRUTA COM TEMPO LIMITE")
print("=" * 70)

print("\n[1] Carregando matriz de distâncias...")

matriz = pd.read_csv(ARQUIVO_CSV, index_col=0)

print("Arquivo carregado com sucesso!")
print(f"Total de capitais disponíveis: {len(matriz)}")


print("\n[2] Selecionando capitais mais próximas de Brasília...")

distancias_brasilia = matriz.loc[CAPITAL_INICIAL]
distancias_brasilia = distancias_brasilia.drop(CAPITAL_INICIAL)

capitais_proximas = distancias_brasilia.sort_values().head(QUANTIDADE_CAPITAIS)
capitais_para_visitar = list(capitais_proximas.index)

print(f"\nCapital inicial: {CAPITAL_INICIAL}")
print(f"Quantidade de capitais escolhidas: {QUANTIDADE_CAPITAIS}")

print("\nCapitais selecionadas:")
for i, capital in enumerate(capitais_para_visitar, start=1):
    print(f"{i}. {capital} - {capitais_proximas[capital]} km de Brasília")


print("\n[3] Calculando quantidade total de possibilidades...")

total_possibilidades = math.factorial(QUANTIDADE_CAPITAIS)

print(f"Total de rotas possíveis: {total_possibilidades:,}")
print(f"Tempo limite: {TEMPO_LIMITE_MINUTOS} minuto(s)")


def calcular_distancia_rota(rota, matriz_distancias):
    """
    Calcula a distância total de uma rota.
    A rota começa em Brasília, passa pelas capitais e retorna para Brasília.
    """

    distancia_total = 0

    for i in range(len(rota) - 1):
        origem = rota[i]
        destino = rota[i + 1]

        distancia_total += matriz_distancias.loc[origem, destino]

    return distancia_total


print("\n[4] Iniciando busca por força bruta...")

tempo_limite_segundos = TEMPO_LIMITE_MINUTOS * 60

inicio_tempo = time.time()

menor_distancia = float("inf")
melhor_rota = None
rotas_testadas = 0

# Não usamos list(), porque para 27 capitais isso ocuparia muita memória.
# O itertools.permutations gera uma rota por vez.
for rota_permutada in itertools.permutations(capitais_para_visitar):

    tempo_atual = time.time()
    tempo_decorrido = tempo_atual - inicio_tempo

    # Condição de parada por tempo
    if tempo_decorrido >= tempo_limite_segundos:
        print("\nTempo limite atingido.")
        break

    rota_completa = [CAPITAL_INICIAL] + list(rota_permutada) + [CAPITAL_INICIAL]

    distancia_total = calcular_distancia_rota(rota_completa, matriz)

    rotas_testadas += 1

    if distancia_total < menor_distancia:
        menor_distancia = distancia_total
        melhor_rota = rota_completa

        print("\nNova melhor rota encontrada!")
        print(f"Rota número: {rotas_testadas}")
        print(f"Distância: {menor_distancia:.2f} km")
        print(" -> ".join(melhor_rota))

    # Mostra progresso a cada 10.000 rotas testadas
    if rotas_testadas % 10000 == 0:
        percentual = (rotas_testadas / total_possibilidades) * 100
        print("\nProgresso:")
        print(f"Rotas testadas: {rotas_testadas:,}")
        print(f"Percentual aproximado: {percentual:.8f}%")
        print(f"Tempo decorrido: {tempo_decorrido:.2f} segundos")
        print(f"Melhor distância até agora: {menor_distancia:.2f} km")


fim_tempo = time.time()
tempo_total = fim_tempo - inicio_tempo


print("\n" + "=" * 70)
print("RESULTADO FINAL")
print("=" * 70)

print(f"\nCapital inicial: {CAPITAL_INICIAL}")
print(f"Capitais visitadas: {QUANTIDADE_CAPITAIS}")

print(f"\nTotal de rotas possíveis: {total_possibilidades:,}")
print(f"Rotas testadas: {rotas_testadas:,}")

percentual_final = (rotas_testadas / total_possibilidades) * 100
print(f"Percentual testado: {percentual_final:.8f}%")

print(f"\nTempo limite configurado: {TEMPO_LIMITE_MINUTOS} minuto(s)")
print(f"Tempo real de execução: {tempo_total:.2f} segundos")

print("\nMelhor rota encontrada dentro do tempo:")
print(" -> ".join(melhor_rota))

print(f"\nMenor distância encontrada: {menor_distancia:.2f} km")

print("\n" + "=" * 70)
print("FIM DO PROCESSAMENTO")
print("=" * 70)