Programming

O Problema do Caixeiro-Viajante: Por Que Alguns Problemas São Impossíveis de Resolver Perfeitamente

✍️ Taylson Martinez
8 min read
O Problema do Caixeiro-Viajante: Por Que Alguns Problemas São Impossíveis de Resolver Perfeitamente

Entenda por que alguns problemas de otimização são tão difíceis e como usar algoritmos simples para encontrar soluções boas o suficiente na prática.

🌐

Este artigo também está disponível em inglês

Ler em Inglês →

O Problema Real: Otimizar Rotas de Entrega

Imagine que você precisa criar a melhor rota para um entregador visitar 5 clientes diferentes e voltar ao ponto de partida. Parece simples, não é? Você pode até calcular todas as possibilidades manualmente.

Mas e se fossem 10 clientes? Ou 20? Ou 100?

Aqui está o problema: o número de rotas possíveis cresce de forma absurda. Para 20 cidades, existem mais combinações do que você conseguiria calcular em toda a sua vida, mesmo com o computador mais potente do mundo.

A lição importante: Às vezes, buscar a solução perfeita é impossível. Em vez disso, precisamos encontrar uma solução “boa o suficiente” que funcione na prática.

O Que É o Problema do Caixeiro-Viajante?

O Problema do Caixeiro-Viajante é um dos problemas mais famosos da ciência da computação. A ideia é simples: você tem várias cidades para visitar e precisa encontrar a rota mais curta que passe por todas elas exatamente uma vez e volte ao ponto de partida.

Por Que É Tão Difícil?

O problema é que o número de rotas possíveis cresce de forma exponencial. Veja alguns exemplos:

  • 5 cidades: 120 rotas diferentes para testar
  • 10 cidades: 3.628.800 rotas diferentes
  • 20 cidades: Mais de 2 quintilhões de rotas (um número com 18 zeros!)

Para entender melhor: se você testasse 1 bilhão de rotas por segundo, ainda levaria mais de 60 anos para testar todas as possibilidades de apenas 20 cidades.

Por Que Não Podemos Resolver Perfeitamente?

A ciência ainda não descobriu uma forma de resolver esse tipo de problema de forma rápida e perfeita. Quanto mais cidades você adiciona, mais tempo o computador precisa para encontrar a solução ideal. Para problemas reais (com dezenas ou centenas de pontos), isso se torna impossível.

A Solução Prática: Algoritmos “Gulosos”

Como não podemos encontrar a solução perfeita em tempo útil, usamos uma estratégia chamada algoritmo guloso (ou “greedy” em inglês). A ideia é simples: em cada passo, escolhemos a melhor opção disponível naquele momento, sem pensar muito no futuro.

É como quando você está com fome e escolhe o prato mais próximo no restaurante, em vez de analisar todo o cardápio para encontrar a refeição perfeita.

Onde Usamos Isso?

Apps de entrega: Quando você pede comida e o app calcula a rota do entregador.

GPS e mapas: Quando você pede a rota mais rápida entre vários destinos.

Planejamento de rotas: Empresas de logística usam isso para otimizar entregas.

Quando NÃO Usar?

Se você tem poucos pontos (menos de 10), pode ser que valha a pena testar todas as possibilidades para encontrar a solução perfeita. Mas para problemas maiores, o algoritmo guloso é a melhor opção.

Como Funciona na Prática: O Algoritmo do Vizinho Mais Próximo

Vamos implementar uma solução simples em Kotlin. A estratégia é: sempre vá para a cidade mais próxima que ainda não foi visitada. É como um GPS que sempre escolhe o próximo destino mais perto.

data class Cidade(val nome: String)

/**
 * Representa o mapa de distâncias: Cidade de Origem -> (Cidade de Destino -> Distância)
 */
typealias MapaDistancias = Map<Cidade, Map<Cidade, Int>>

fun caixeiroViajanteGuloso(cidadeInicial: Cidade, mapa: MapaDistancias): List<Cidade> {
    val rota = mutableListOf<Cidade>()
    val visitadas = mutableSetOf<Cidade>()
    
    var cidadeAtual = cidadeInicial
    var distanciaTotal = 0
    
    rota.add(cidadeAtual)
    visitadas.add(cidadeAtual)

    // Enquanto houver cidades não visitadas no grafo
    while (visitadas.size < mapa.size) {
        val vizinhos = mapa[cidadeAtual] ?: emptyMap()
        
        // Estratégia Gulosa: Filtra não visitadas e seleciona a de menor distância
        val proximaEntrada = vizinhos
            .filter { it.key !in visitadas }
            .minByOrNull { it.value }
            
        if (proximaEntrada != null) {
            val (proximaCidade, distancia) = proximaEntrada
            visitadas.add(proximaCidade)
            rota.add(proximaCidade)
            distanciaTotal += distancia
            cidadeAtual = proximaCidade
        } else {
            // Grafo desconexo: não há caminho para as cidades restantes
            break 
        }
    }
    
    // Fechamento do ciclo: Retorno à cidade de origem
    mapa[cidadeAtual]?.get(cidadeInicial)?.let { distanciaRetorno ->
        rota.add(cidadeInicial)
        distanciaTotal += distanciaRetorno
    }

    println("Análise de Rota Concluída. Distância Total Aproximada: ${distanciaTotal}km")
    return rota
}

fun main() {
    // Cidades do estado de São Paulo
    val saoPaulo = Cidade("São Paulo")
    val campinas = Cidade("Campinas")
    val santos = Cidade("Santos")
    val sorocaba = Cidade("Sorocaba")
    val ribeiraoPreto = Cidade("Ribeirão Preto")

    val mapa: MapaDistancias = mapOf(
        saoPaulo to mapOf(campinas to 100, santos to 80, sorocaba to 100, ribeiraoPreto to 320),
        campinas to mapOf(saoPaulo to 100, santos to 150, sorocaba to 120, ribeiraoPreto to 220),
        santos to mapOf(saoPaulo to 80, campinas to 150, sorocaba to 180, ribeiraoPreto to 400),
        sorocaba to mapOf(saoPaulo to 100, campinas to 120, santos to 180, ribeiraoPreto to 280),
        ribeiraoPreto to mapOf(saoPaulo to 320, campinas to 220, santos to 400, sorocaba to 280)
    )

    val rotaFinal = caixeiroViajanteGuloso(saoPaulo, mapa)
    println("Sequência de Visitação: ${rotaFinal.joinToString(" -> ") { it.nome }}")
}

Como o Código Funciona

O código é bem simples de entender:

  1. Começamos em São Paulo: Escolhemos a cidade inicial.

  2. Procuramos a cidade mais próxima: Olhamos todas as cidades conectadas e escolhemos a que está mais perto e ainda não visitamos.

  3. Vamos até ela: Adicionamos essa cidade à nossa rota e marcamos como visitada.

  4. Repetimos: Continuamos esse processo até visitar todas as cidades (Campinas, Santos, Sorocaba, Ribeirão Preto).

  5. Voltamos para São Paulo: Por fim, retornamos à cidade de partida.

O código usa um loop simples (for) para encontrar a cidade mais próxima, tornando tudo mais fácil de entender do que usar funções complexas. O resultado será uma rota que visita todas as cidades de forma eficiente!

Comparação: Solução Perfeita vs. Solução Prática

Por Que Não Usar a Solução Perfeita?

CaracterísticaTestar Todas as PossibilidadesAlgoritmo Guloso
ResultadoSempre a melhor rota possívelRota boa (geralmente 95-99% da ótima)
VelocidadeImpossível para mais de 15 cidadesRápido mesmo com centenas de cidades
Uso PráticoNão funciona em sistemas reaisFunciona perfeitamente em produção

Por Que Isso Importa?

Na prática, a diferença é mínima: Uma rota que é 5% mais longa que a perfeita ainda é excelente. E você consegue calcular em milissegundos, não em milênios.

Escala bem: Se você tem 10 cidades ou 1000 cidades, o algoritmo guloso continua rápido e útil.

Fácil de entender e manter: O código é simples, qualquer desenvolvedor consegue entender e modificar.

Lições Importantes

  1. Perfeição nem sempre é possível: Alguns problemas são simplesmente muito difíceis para resolver de forma perfeita. E está tudo bem!

  2. Bom o suficiente é suficiente: Uma solução que funciona bem e é rápida é melhor que uma solução perfeita que nunca termina.

  3. Algoritmos gulosos são úteis: Eles transformam problemas impossíveis em soluções práticas que funcionam no mundo real.

  4. Pense no contexto: Em sistemas reais, uma rota que é 5% mais longa mas calcula em milissegundos é muito melhor que a rota perfeita que leva anos para calcular.

Conclusão

O Problema do Caixeiro-Viajante nos ensina uma lição valiosa: nem sempre precisamos da solução perfeita. Às vezes, uma solução boa o suficiente que funciona rápido é exatamente o que precisamos.

Na programação e na vida, saber quando parar de buscar a perfeição e aceitar uma solução prática é uma habilidade importante.


Este artigo faz parte da série “Entendendo Algoritmos”, baseada no livro de Aditya Y. Bhargava.