Comparação de Eficiência entre Backtracking e Branch and Bound no Problema do Caixeiro Viajante

O Problema do Caixeiro Viajante (Traveling Salesman Problem - TSP) é um desafio clássico de otimização combinatória. Este artigo analisa duas abordagens principais para sua resolução: o algoritmo de Retrocesso (Backtracking) e o algoritmo de Limitação e Ramificação (Branch and Bound), avaliando sua eficiência em diferentes escalas de complexidade.

Análise Teórica dos Algoritmos

1. Algoritmo de Retrocesso (Backtracking)

O Backtracking é uma técnica de busca exaustiva que explora o espaço de estados utilizando uma estratégia de busca em profundidade (DFS). Para o TSP, o algoritmo tenta construir um caminho visitando cidades uma a uma. Ele utiliza a técnica de "poda" para descartar caminhos que já superam o custo da melhor solução encontrada até o momento.

  • Espaço de Estados: Representado por uma árvore de permutação.
  • Poda: Se o custo parcial atual somado à próxima aresta for maior ou igual ao melhor custo global, o ramo é abandonado.

2. Algoritmo de Branch and Bound

Diferente do Backtracking, o Branch and Bound utiliza busca em largura (BFS) ou busca por prioridade (Best-First Search). Ele se baseia em estimativas de limites inferiores (Lower Bounds) e superiores (Upper Bounds) para decidir qual nó da árvore de busca deve ser expandido.

  • Limite Superior (UB): Geralmente calculado por uma heurística gulosa para encontrar uma solução inicial aceitável.
  • Limite Inferior (LB): Uma estimativa do custo mínimo para completar o ciclo a partir do nó atual.
  • Fila de Prioridade: Armazana os nós vivos, priorizando aqueles com menor limite inferior de custo.

Implementação e Estrutura de Código

Exemplo de Implementação: Backtracking

Abaixo, apresentamos uma estrutura em Java para o algoritmo de retrocesso com poda básica.


public class TspBacktracking {
    private int[][] matrizAdj;
    private int totalCidades;
    private int[] rotaAtual;
    private int[] melhorRota;
    private int custoMinimo = Integer.MAX_VALUE;

    public void resolver(int[][] matriz, int n) {
        this.matrizAdj = matriz;
        this.totalCidades = n;
        this.rotaAtual = new int[n];
        for (int i = 0; i < n; i++) rotaAtual[i] = i;
        this.melhorRota = new int[n];
        
        executarBusca(1, 0);
    }

    private void executarBusca(int nivel, int custoParcial) {
        if (nivel == totalCidades) {
            int custoFinal = custoParcial + matrizAdj[rotaAtual[nivel - 1]][rotaAtual[0]];
            if (custoFinal < custoMinimo) {
                custoMinimo = custoFinal;
                System.arraycopy(rotaAtual, 0, melhorRota, 0, totalCidades);
            }
            return;
        }

        for (int i = nivel; i < totalCidades; i++) {
            int custoAresta = matrizAdj[rotaAtual[nivel - 1]][rotaAtual[i]];
            if (custoAresta != -1 && custoParcial + custoAresta < custoMinimo) {
                trocar(nivel, i);
                executarBusca(nivel + 1, custoParcial + custoAresta);
                trocar(nivel, i); // Backtrack
            }
        }
    }

    private void trocar(int i, int j) {
        int temp = rotaAtual[i];
        rotaAtual[i] = rotaAtual[j];
        rotaAtual[j] = temp;
    }
}

Exemplo de Implementação: Brench and Bound

Esta versão utiliza uma fila de prioridade para expandir os nós com base em uma estimativa de custo mínimo.


import java.util.*;

public class TspBranchAndBound {
    private static class NodoBusca implements Comparable<NodoBusca> {
        int[] caminho;
        int custoAtual;
        int estimativaLB;
        int nivel;

        public NodoBusca(int[] caminho, int custo, int lb, int nivel) {
            this.caminho = caminho.clone();
            this.custoAtual = custo;
            this.estimativaLB = lb;
            this.nivel = nivel;
        }

        @Override
        public int compareTo(NodoBusca outro) {
            return Integer.compare(this.estimativaLB, outro.estimativaLB);
        }
    }

    public int calcularLimiteInferior(int[][] matriz, int[] caminho, int nivel) {
        // Implementação simplificada de LB usando a soma das arestas mínimas
        int lb = 0;
        for (int i = 0; i < matriz.length; i++) {
            int min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;
            for (int j = 0; j < matriz.length; j++) {
                if (i == j || matriz[i][j] == -1) continue;
                if (matriz[i][j] <= min1) {
                    min2 = min1;
                    min1 = matriz[i][j];
                } else if (matriz[i][j] < min2) {
                    min2 = matriz[i][j];
                }
            }
            lb += (min1 + min2);
        }
        return (int) Math.ceil(lb / 2.0);
    }

    public void buscar(int[][] matriz) {
        int n = matriz.length;
        PriorityQueue<NodoBusca> fila = new PriorityQueue<>();
        int[] inicial = new int[n];
        // Inicialização do nó raiz...
        // Loop de expansão de nós com poda baseada em custo acumulado e LB
    }
}

Comparação de Eficiência

Ao comparar as duas abordagens, observamos comportamentos distintos dependendo do tamanho da entrada (número de cidades):

  • Pequena Escala (n < 15): Ambos os algoritmos apresentam tempos de execução semelhantes. O Backtracking é mais simples de implementar e consome menos memória, pois não precisa manter uma fila de nós vivos.
  • Média e Larga Escala (n > 20): O Branch and Bound supera significativamente o Backtracking. Através do uso de limites inferiores mais precisos, o Branch and Bound consegue podar subárvores inteiras muito antes do Backtracking, reduzindo drasticamente o número de nós visitados.
  • Impacto das Heurísticas: A eficiência do Branch and Bound é altamente dependente da qualidade da função de cálculo do Limite Inferior (LB). Um LB apertado resulta em menos expansões de nós, enquanto um LB frouxo aproxima o desempenho do algoritmo ao de uma busca em largura comum.

Em resumo, para probelmas reais de TSP onde a otimalidade é necessária, o Branch and Bound é a escolha preferencial devido à sua capacidade de gerenciar o espaço de busca de forma mais inteligente através de estimativas de custo.

Tags: Traveling-Salesman-Problem java Branch-and-Bound backtracking Algoritmos-de-Otimizacao

Publicado em 7-3 23:09