O algoritmo de Dijkstra é o método mais eficiente para encontrar o caminho mais curto de um vértice de origem a todos os outros vértices em um grafo com pesos não negativos nas arestas. Criado por Edsger Dijkstra em 1956, é um dos algoritmos mais importantes da ciência da computação e continua sendo amplamente usado em sistemas de navegação GPS, roteamento de pacotes em redes, jogos e otimização logística.
Em Rust, a implementação usa BinaryHeap (fila de prioridade baseada em heap máximo) invertido para funcionar como heap mínimo, combinado com listas de adjacência. O sistema de tipos do Rust garante segurança de memória sem overhead, tornando a implementação tanto segura quanto performática.
Como Funciona
O Dijkstra é um algoritmo guloso (greedy) que mantém uma tabela de distâncias mínimas conhecidas e, a cada passo, escolhe o vértice não processado com a menor distância acumulada.
- Inicializa a distância da origem como 0 e todos os outros como infinito.
- Insere a origem na fila de prioridade (min-heap).
- Remove o vértice com menor distância da fila.
- Para cada vizinho, calcula a distância passando pelo vértice atual.
- Se essa distância for menor que a conhecida, atualiza e insere na fila.
- Repete até que a fila esteja vazia.
Diagrama Visual — Atualização de Distâncias
Grafo de exemplo:
(0)
/ | \
4/ 2| \1
/ | \
(1) (2) (3)
| / \ |
1| 3/ \1 |5
| / \ |
(4)-----(5)
2
Arestas e pesos:
0-1: 4, 0-2: 2, 0-3: 1
1-4: 1, 2-4: 3, 2-5: 1
3-5: 5, 4-5: 2
Execução a partir do vértice 0:
Passo | Processa | Fila (distância, vértice) | dist[0] | dist[1] | dist[2] | dist[3] | dist[4] | dist[5]
------|----------|---------------------------|---------|---------|---------|---------|---------|--------
Init | - | [(0,0)] | 0 | INF | INF | INF | INF | INF
1 | 0 | [(1,3),(2,2),(4,1)] | 0 | 4 | 2 | 1 | INF | INF
2 | 3 | [(2,2),(4,1),(6,5)] | 0 | 4 | 2 | 1 | INF | 6
3 | 2 | [(3,5),(4,1),(5,4)] | 0 | 4 | 2 | 1 | 5 | 3
4 | 5 | [(4,1),(5,4),(5,2)] | 0 | 4 | 2 | 1 | 5 | 3
5 | 1 | [(5,4),(5,2)] | 0 | 4 | 2 | 1 | 5 | 3
6 | 4 | [(5,2)] | 0 | 4 | 2 | 1 | 5 | 3
7 | - | [] | 0 | 4 | 2 | 1 | 5 | 3
Resultado: distâncias mínimas a partir de 0: [0, 4, 2, 1, 5, 3]
Caminho mais curto de 0 a 5: 0 -> 2 -> 5 (distância = 3)
Representação do Grafo
Para Dijkstra, cada aresta precisa de um peso. Representamos com lista de adjacência onde cada vizinho é um par (destino, peso).
/// Grafo ponderado representado como lista de adjacência.
/// grafo[u] contém pares (vizinho, peso) para cada aresta saindo de u.
fn criar_grafo(num_vertices: usize) -> Vec<Vec<(usize, u64)>> {
let mut grafo = vec![vec![]; num_vertices];
// Arestas bidirecionais com peso
let arestas = [
(0, 1, 4), (0, 2, 2), (0, 3, 1),
(1, 4, 1), (2, 4, 3), (2, 5, 1),
(3, 5, 5), (4, 5, 2),
];
for (u, v, peso) in arestas {
grafo[u].push((v, peso));
grafo[v].push((u, peso));
}
grafo
}
Complexidade
| Caso | Tempo | Espaço |
|---|---|---|
| Melhor | O(E log V) | O(V + E) |
| Médio | O((V + E) log V) | O(V + E) |
| Pior | O((V + E) log V) | O(V + E) |
Onde V = número de vértices e E = número de arestas.
- Tempo O((V + E) log V): cada vértice é inserido na fila pelo menos uma vez (O(V log V)) e cada aresta pode gerar uma inserção na fila (O(E log V)).
- Espaço O(V + E): armazena o grafo (O(V + E)), o vetor de distâncias (O(V)) e a fila de prioridade (até O(V) elementos úteis, podendo acumular entradas duplicadas).
- Com heap de Fibonacci, a complexidade cai para O(V log V + E), mas a biblioteca padrão do Rust usa
BinaryHeap.
Implementação em Rust
Dijkstra com BinaryHeap
O BinaryHeap de Rust é um max-heap. Para usá-lo como min-heap, invertemos a ordenação usando std::cmp::Reverse ou implementando Ord com comparação invertida.
use std::cmp::Reverse;
use std::collections::BinaryHeap;
/// Calcula a menor distância de `origem` a todos os vértices.
/// Retorna vetor de distâncias (u64::MAX = inalcançável).
fn dijkstra(grafo: &Vec<Vec<(usize, u64)>>, origem: usize) -> Vec<u64> {
let n = grafo.len();
let mut dist = vec![u64::MAX; n];
let mut heap = BinaryHeap::new();
dist[origem] = 0;
heap.push(Reverse((0u64, origem))); // (distância, vértice)
while let Some(Reverse((custo, u))) = heap.pop() {
// Se já encontramos um caminho melhor, ignora
if custo > dist[u] {
continue;
}
for &(vizinho, peso) in &grafo[u] {
let nova_dist = custo + peso;
if nova_dist < dist[vizinho] {
dist[vizinho] = nova_dist;
heap.push(Reverse((nova_dist, vizinho)));
}
}
}
dist
}
fn main() {
let mut grafo = vec![vec![]; 6];
let arestas = [
(0, 1, 4), (0, 2, 2), (0, 3, 1),
(1, 4, 1), (2, 4, 3), (2, 5, 1),
(3, 5, 5), (4, 5, 2),
];
for (u, v, peso) in arestas {
grafo[u].push((v, peso));
grafo[v].push((u, peso));
}
let dist = dijkstra(&grafo, 0);
for (v, d) in dist.iter().enumerate() {
if *d == u64::MAX {
println!("Distância de 0 a {}: inalcançável", v);
} else {
println!("Distância de 0 a {}: {}", v, d);
}
}
// Saída:
// Distância de 0 a 0: 0
// Distância de 0 a 1: 4
// Distância de 0 a 2: 2
// Distância de 0 a 3: 1
// Distância de 0 a 4: 5
// Distância de 0 a 5: 3
}
Dijkstra com Reconstrução de Caminho
use std::cmp::Reverse;
use std::collections::BinaryHeap;
/// Retorna (distâncias, predecessores) a partir de `origem`.
fn dijkstra_com_caminho(
grafo: &Vec<Vec<(usize, u64)>>,
origem: usize,
) -> (Vec<u64>, Vec<Option<usize>>) {
let n = grafo.len();
let mut dist = vec![u64::MAX; n];
let mut predecessor: Vec<Option<usize>> = vec![None; n];
let mut heap = BinaryHeap::new();
dist[origem] = 0;
heap.push(Reverse((0u64, origem)));
while let Some(Reverse((custo, u))) = heap.pop() {
if custo > dist[u] {
continue;
}
for &(vizinho, peso) in &grafo[u] {
let nova_dist = custo + peso;
if nova_dist < dist[vizinho] {
dist[vizinho] = nova_dist;
predecessor[vizinho] = Some(u);
heap.push(Reverse((nova_dist, vizinho)));
}
}
}
(dist, predecessor)
}
/// Reconstrói o caminho do `origem` até `destino` usando predecessores.
fn reconstruir_caminho(
predecessor: &Vec<Option<usize>>,
origem: usize,
destino: usize,
) -> Option<Vec<usize>> {
let mut caminho = Vec::new();
let mut atual = destino;
// Se o destino não tem predecessor e não é a origem, não há caminho
if predecessor[destino].is_none() && destino != origem {
return None;
}
while atual != origem {
caminho.push(atual);
match predecessor[atual] {
Some(pred) => atual = pred,
None => return None,
}
}
caminho.push(origem);
caminho.reverse();
Some(caminho)
}
fn main() {
let mut grafo = vec![vec![]; 6];
let arestas = [
(0, 1, 4), (0, 2, 2), (0, 3, 1),
(1, 4, 1), (2, 4, 3), (2, 5, 1),
(3, 5, 5), (4, 5, 2),
];
for (u, v, peso) in arestas {
grafo[u].push((v, peso));
grafo[v].push((u, peso));
}
let (dist, predecessor) = dijkstra_com_caminho(&grafo, 0);
// Mostra caminho mínimo para cada vértice
for destino in 0..grafo.len() {
if dist[destino] == u64::MAX {
println!("Vértice {}: inalcançável", destino);
} else if let Some(caminho) = reconstruir_caminho(&predecessor, 0, destino) {
println!(
"Vértice {} (dist={}): {:?}",
destino, dist[destino], caminho
);
}
}
// Saída:
// Vértice 0 (dist=0): [0]
// Vértice 1 (dist=4): [0, 1]
// Vértice 2 (dist=2): [0, 2]
// Vértice 3 (dist=1): [0, 3]
// Vértice 4 (dist=5): [0, 1, 4]
// Vértice 5 (dist=3): [0, 2, 5]
}
Exemplo de Execução
Considere um mapa rodoviário simplificado:
São Paulo --(400)--> Campinas
| |
(100) (200)
| |
v v
Santos ---(350)---> Ribeirão Preto
| |
(500) (150)
| |
v v
Curitiba <--(300)--- Uberlândia
Vértices: 0=SP, 1=Campinas, 2=Santos, 3=R.Preto, 4=Curitiba, 5=Uberlândia
Dijkstra a partir de São Paulo (0):
Inicialização: dist = [0, INF, INF, INF, INF, INF]
Passo 1: Processa SP(0), dist=0
-> Campinas: 0+400=400 < INF, atualiza dist[1]=400
-> Santos: 0+100=100 < INF, atualiza dist[2]=100
dist = [0, 400, 100, INF, INF, INF]
Passo 2: Processa Santos(2), dist=100
-> R.Preto: 100+350=450 < INF, atualiza dist[3]=450
-> Curitiba: 100+500=600 < INF, atualiza dist[4]=600
dist = [0, 400, 100, 450, 600, INF]
Passo 3: Processa Campinas(1), dist=400
-> R.Preto: 400+200=600 > 450, não atualiza
dist = [0, 400, 100, 450, 600, INF]
Passo 4: Processa R.Preto(3), dist=450
-> Uberlândia: 450+150=600 < INF, atualiza dist[5]=600
dist = [0, 400, 100, 450, 600, 600]
Passo 5: Processa Curitiba(4), dist=600 (ou Uberlândia, mesma distância)
Passo 6: Processa Uberlândia(5), dist=600
-> Curitiba: 600+300=900 > 600, não atualiza
Resultado final: SP->SP=0, SP->Campinas=400, SP->Santos=100,
SP->R.Preto=450, SP->Curitiba=600, SP->Uberlândia=600
Variações e Otimizações
1. Dijkstra com Parada Antecipada
Se você precisa apenas da distância para um destino específico, pode encerrar assim que esse vértice for processado (removido da fila):
if u == destino {
return dist[destino];
}
2. Dijkstra Bidirecional
Executa Dijkstra simultaneamente da origem e do destino, parando quando as duas buscas se encontram. Reduz significativamente o número de vértices explorados em grafos grandes.
3. A* (A-estrela)
Uma extensão do Dijkstra que usa uma heurística para guiar a busca na direção do destino. É mais eficiente que Dijkstra para encontrar o caminho entre dois pontos específicos quando uma boa heurística está disponível (por exemplo, distância euclidiana em mapas).
Quando Usar
Use Dijkstra quando:
- O grafo tem arestas com pesos não negativos.
- Precisa do caminho mais curto de um vértice a todos os outros (ou a um destino específico).
- O grafo é esparso (E muito menor que V^2) — a representação com lista de adjacência e heap binário é ideal.
- Está implementando roteamento em redes, navegação GPS ou otimização logística.
Evite Dijkstra quando:
- O grafo tem arestas com peso negativo — use Bellman-Ford.
- O grafo não tem pesos — use BFS, que é mais simples e tem mesma complexidade.
- Precisa de todos os pares de caminhos mínimos — use Floyd-Warshall.
Comparação com Outros Algoritmos
| Característica | Dijkstra | Bellman-Ford | Floyd-Warshall | BFS |
|---|---|---|---|---|
| Pesos negativos | Nao | Sim | Sim | Nao (sem peso) |
| Ciclos negativos | Nao detecta | Detecta | Detecta | N/A |
| Fonte | Única | Única | Todos os pares | Única |
| Complexidade | O((V+E) log V) | O(V * E) | O(V^3) | O(V + E) |
| Melhor para | Grafos esparsos | Pesos negativos | Grafos densos/pequenos | Sem peso |
| Reconstrução de caminho | Sim | Sim | Sim | Sim |
Exercícios Práticos
Custo mínimo de voo: Dados N aeroportos e M voos com custos, encontre o voo mais barato de uma cidade de origem a uma cidade de destino usando Dijkstra. Reconstrua o itinerário completo.
Labirinto ponderado: Dada uma matriz NxN onde cada célula tem um custo para atravessar, encontre o caminho de custo mínimo do canto superior esquerdo ao inferior direito.
Rede de computadores: Dados N computadores conectados por links com latência variável, encontre o caminho com menor latência total entre dois computadores. Identifique os links que são gargalos.
Dijkstra com no máximo K arestas: Modifique o Dijkstra para encontrar o caminho mais curto usando no máximo K arestas. (Dica: adicione o número de arestas ao estado.)
Veja Também
BinaryHeap— Fila de prioridade — estrutura de dados essencial para DijkstraHashMap— Mapa hash — para representação de grafos e armazenamento de predecessoresVec— Vetor dinâmico — para lista de adjacência e vetores de distância- Otimização e Performance em Rust — técnicas para melhorar desempenho
- Algoritmo de Bellman-Ford — alternativa para grafos com pesos negativos
- Algoritmo de Floyd-Warshall — caminho mínimo entre todos os pares
- Busca em Largura (BFS) — caminho mínimo em grafos sem peso