Algoritmo de Dijkstra
1. O GPS dos Grafos
Se você abre o Google Maps e pede uma rota, ele usa uma variante do algoritmo de Edsger W. Dijkstra (1956). Diferente da BFS (que só serve para arestas de peso 1), o Dijkstra encontra o Caminho Mínimo em grafos onde as arestas têm pesos variados não-negativos.
Analogia da Água: Imagine que a água começa a fluir da origem. Ela percorre canos (arestas) de comprimentos diferentes. O algoritmo determina em que “momento” a água chega em cada cidade.
2. Conceito de Relaxamento
A base de todos os algoritmos de menor caminho é a operação de Relaxamento (Relaxation). Seja D[v] a melhor distância conhecida até agora da origem até v.
Imagine que conhecemos um caminho para \(v\) que custa \(10\). Descobrimos uma aresta \(u \to v\) com peso \(2\), e sabemos que chegamos em \(u\) com custo \(5\). Novo custo potencial via \(u\): \(D[u] + peso(u, v) = 5 + 2 = 7\). Como \(7 < 10\), nós relaxamos a aresta: atualizamos D[v] = 7 e dizemos que o pai de \(v\) agora é \(u\).
3. O Algoritmo Guloso
- Inicialização:
dist[S] = 0, todos os outrosdist[v] = ∞. - Priority Queue: Coloque par
(0, S)na fila. - Enquanto a PQ não estiver vazia:
- Extraia o vértice \(u\) com a menor distância atual.
- Se essa distância for maior que
dist[u](info obsoleta), ignore. - Para cada vizinho \(v\) de \(u\), tente relaxar a aresta \((u,v)\).
- Se relaxar, empurre
(novo_dist, v)para a PQ.
Por que Guloso?
O algoritmo assume que, se extraímos \(u\) da fila com distância \(X\), não existe nenhum outro caminho no mundo que chegue em \(u\) com custo menor que \(X\). Isso é verdade porque arestas negativas não existem.
4. Implementação em Java
Atenção: A PriorityQueue do Java guarda objetos. Devemos criar uma classe ou usar array.
import java.util.*;
class Node implements Comparable<Node> {
int id;
int distance;
public Node(int id, int dist) {
this.id = id;
this.distance = dist;
}
@Override
public int compareTo(Node other) {
return Integer.compare(this.distance, other.distance);
}
}
public class Dijkstra {
public static int[] dijkstra(int V, List<List<Edge>> adj, int startNode) {
int[] dist = new int[V];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[startNode] = 0;
PriorityQueue<Node> pq = new PriorityQueue<>();
pq.add(new Node(startNode, 0));
while (!pq.isEmpty()) {
Node current = pq.poll();
int u = current.id;
int d = current.distance;
// Lazy Deletion: Se já achamos um caminho melhor antes, esse é lixo
if (d > dist[u]) continue;
// Explora vizinhos
for (Edge e : adj.get(u)) {
int v = e.destino;
int peso = e.peso;
// Relaxamento
if (dist[u] + peso < dist[v]) {
dist[v] = dist[u] + peso;
pq.add(new Node(v, dist[v]));
}
}
}
return dist;
}
}5. Limitações e Complexidade
O Problema dos Pesos Negativos
Se houver uma aresta com peso \(-10\), o critério guloso falha. Podemos “voltar no tempo” e achar um caminho melhor depois de ter fechado um vértice. Pior ainda, se houver um Ciclo Negativo, podemos ficar girando nele e diminuindo o custo infinitamente (\(-\infty\)). Para pesos negativos, use Bellman-Ford.
Complexidade
- Com Binary Heap (PriorityQueue padrão): \(O(E \log V)\).
- Com Fibonacci Heap: \(O(E + V \log V)\) (Teórico, complexo demais para prática).
- Array Simples: \(O(V^2)\) (Bom apenas para grafos muito densos).
Aplicações
- Roteamento OSPF (Internet).
- GPS e malhas viárias.
- Games (Pathfinding, embora A* seja melhor).