Arestas vs Vértices: Euler e Hamilton
1. As Pontes de Königsberg (Caminho Euleriano)
Leonhard Euler fundou a Teoria dos Grafos em 1736 ao provar que era impossível cruzar as 7 pontes de Königsberg sem repetir nenhuma e voltar ao início.
Definição: Um Ciclo Euleriano percorre todas as arestas do grafo exatamente uma vez e retorna ao início.
Teorema de Euler: Um grafo conexo possui um Ciclo Euleriano se, e somente se, todos os vértices têm Grau Par. (Se houver 0 ou 2 vértices de grau ímpar, existe um Caminho Euleriano, mas não ciclo).
Algoritmo de Hierholzer \(O(E)\)
Para encontrar o ciclo (não apenas dizer que existe): 1. Comece de um vértice qualquer \(v\). 2. Siga um caminho arbitrário até voltar a \(v\) (ou travar), removendo as arestas usadas. 3. Se sobrar arestas no grafo, elas formam sub-ciclos. Funda-os ao ciclo principal.
2. O Caixeiro Viajante (Caminho Hamiltoniano)
William Rowan Hamilton propôs um jogo (“Icosian Game”) em 1857: visitar todas as cidades (vértices) de um dodecaedro exatamente uma vez.
Definição: Um Ciclo Hamiltoniano visita todos os vértices exatamente uma vez e retorna ao início.
O Abismo da Complexidade: Diferente do Euleriano (que olhamos o grau e pronto), saber se um grafo é Hamiltoniano é NP-Completo. Não existe critério simples “se e somente se”.
3. Resolvendo TSP (Traveling Salesman Problem)
O problema do Caixeiro Viajante busca o Ciclo Hamiltoniano de menor peso total. Como é NP-Hard, para \(N\) grande usamos heurísticas (Ant Colony, Genetic) ou aproximações (2-OPT). Para \(N\) pequeno (até 20), podemos usar Programação Dinâmica.
DP com Bitmask (Máscara de Bits)
Estado: dp[mask][u] = menor custo para visitar o conjunto de cidades representado por mask, terminando na cidade u. - mask é um inteiro onde o \(i\)-ésimo bit é 1 se a cidade \(i\) foi visitada.
Transição: Para ir para um estado onde visitamos mask terminando em u, devemos ter vindo de um estado prev_mask (sem \(u\)) terminando num vizinho v. \[ dp[mask][u] = \min_{v \in vizinhos} (dp[mask \setminus \{u\}][v] + peso(v, u)) \]
Complexidade: \(O(2^N \cdot N^2)\). - \(N=20 \implies 2^{20} \approx 10^6 \times 400 \approx 4 \cdot 10^8\) operações. (Limite de 1 a 2 segundos).
Implementação Java (TSP)
public class TSP {
static final int INF = 100000000;
public static int solveTSP(int[][] dist) {
int N = dist.length;
int VISITED_ALL = (1 << N) - 1;
int[][] dp = new int[1 << N][N];
// Inicializa com Infinito
for(int[] row : dp) Arrays.fill(row, INF);
// Base: Começar na cidade 0 (mask = 0...01, terminando em 0)
dp[1][0] = 0;
// Itera por todas as máscaras
for (int mask = 1; mask < (1 << N); mask++) {
for (int u = 0; u < N; u++) {
// Se a cidade u está no set mask
if ((mask & (1 << u)) != 0) {
int prevMask = mask ^ (1 << u);
if (prevMask == 0) continue; // Base case
for (int v = 0; v < N; v++) {
if ((prevMask & (1 << v)) != 0) {
dp[mask][u] = Math.min(dp[mask][u], dp[prevMask][v] + dist[v][u]);
}
}
}
}
}
// Final: conectar o último nó de volta ao 0
int ans = INF;
for (int i = 1; i < N; i++) {
ans = Math.min(ans, dp[VISITED_ALL][i] + dist[i][0]);
}
return ans;
}
}Observe o salto gigantesco de dificuldade: - Euleriano (\(N=10^6\)): 0.1 segundos. - Hamiltoniano (\(N=25\)): Séculos.