Caminhos mínimos
Relaxação, Dijkstra, Bellman-Ford, DAG, Floyd-Warshall e Johnson, com condições e reconstrução.
No grafo abaixo, a ligação direta de a custa 4, mas passar por custa . O custo de um caminho é a soma dos pesos das suas arestas. Para encontrar o mínimo, mantemos distâncias provisórias e melhoramo-las quando descobrimos uma alternativa mais curta.
A possibilidade de fixar uma distância depende das hipóteses sobre os pesos. Dijkstra exige pesos não negativos e fixa cada vértice uma vez. Num DAG, a ordem topológica permite também pesos negativos, porque todos os antecessores já foram processados. A secção sobre Bellman-Ford mostra por que a ausência de ciclo negativo não chega para salvar Dijkstra.
Relaxação e predecessores
Inicializamos e para os outros vértices. Relaxar a aresta de peso significa testar se
Se melhorar, atribuímos e pai[v]=u. Só fazemos a soma quando é finito. Cada valor finito é o custo de algum caminho já encontrado, ou seja é um limite superior para a distância mínima verdadeira. A relaxação melhora esse limite, mas não certifica sozinha que ele já é ótimo.
Para reconstruir , seguimos os pais a partir de e invertemos a sequência. Um destino inalcançável não tem caminho. A origem tem o caminho de zero arestas. Atualizar apenas em melhoria estrita evita alterações desnecessárias em empates e ajuda a manter a cadeia de pais.
Dijkstra
Dijkstra exige pesos não negativos. Mantém vértices definitivos e escolhe sempre o não definitivo com menor distância provisória. Quando escolhe , a sua distância passa a definitiva. Depois relaxa as arestas que saem de .
Por que pode fixá-la? Considera um caminho mínimo para e o primeiro vértice desse caminho que ainda não é definitivo. O predecessor de já foi processado e relaxou a entrada em . Como todos os pesos são não negativos, o restante caminho até tem custo não negativo. Como Dijkstra escolheu antes de , temos . Como é o custo de um caminho concreto, a desigualdade oposta também vale. Logo .
O ponto onde a prova usa a hipótese é o passo “o restante caminho tem custo não negativo”. Com uma aresta negativa, esse restante pode reduzir o custo e pode ainda descer abaixo de depois de estar fixado. É esse o erro que as dúvidas sobre Dijkstra com pesos negativos costumam revelar.
| Vértice fixado | ||||
|---|---|---|---|---|
| 4 | 1 | |||
| 3 | 1 | 6 | ||
| 3 | 1 | 4 | ||
| 3 | 1 | 4 | 7 | |
| 3 | 1 | 4 | 7 |
O caminho mínimo para é , custo 7. A entrada antiga de com distância 4 pode continuar na heap, por isso ignora-a quando sair.
O programa usa vértices 0 a n-1. Lê n m origem destino, depois arestas dirigidas u v peso. Rejeita pesos negativos, aceita pesos zero e arestas paralelas. Os limites declarados são 2000 vértices, 20000 arestas e pesos até um milhão.
#include <algorithm>
#include <functional>
#include <iostream>
#include <queue>
#include <utility>
#include <vector>
int main() {
int n, m, s, t;
if (!(std::cin >> n >> m >> s >> t) || n < 1 || n > 2000 ||
m < 0 || m > 20000 || s < 0 || s >= n || t < 0 || t >= n) {
std::cout << "entrada invalida\n";
return 0;
}
using Aresta = std::pair<int, int>;
std::vector<std::vector<Aresta>> adj(n);
for (int i = 0; i < m; ++i) {
int u, v, w;
if (!(std::cin >> u >> v >> w) || u < 0 || u >= n || v < 0 ||
v >= n || w < 0 || w > 1000000) {
std::cout << "entrada invalida\n";
return 0;
}
adj[u].push_back({v, w});
}
constexpr long long INF = 1LL << 60;
std::vector<long long> dist(n, INF);
std::vector<int> pai(n, -1);
using Entrada = std::pair<long long, int>;
std::priority_queue<Entrada, std::vector<Entrada>, std::greater<Entrada>> heap;
dist[s] = 0;
heap.push({0, s});
while (!heap.empty()) {
auto [d, u] = heap.top();
heap.pop();
if (d != dist[u]) continue;
for (auto [v, w] : adj[u]) {
if (d + w >= dist[v]) continue;
dist[v] = d + w;
pai[v] = u;
heap.push({dist[v], v});
}
}
if (dist[t] == INF) {
std::cout << "sem caminho\n";
return 0;
}
std::cout << "distancia " << dist[t] << "\ncaminho";
std::vector<int> caminho;
for (int v = t; v != -1; v = pai[v]) caminho.push_back(v);
std::reverse(caminho.begin(), caminho.end());
for (int v : caminho) std::cout << ' ' << v;
std::cout << '\n';
}Executa o código para ver a saída e os gráficos.
Com m=0 e origem diferente do destino, devolve sem caminho.
Uma heap com uma entrada por vértice e decreaseKey dá . A variante publicada pode criar entradas e custa tempo e memória, incluindo a heap e o grafo. Para grafos simples é comum expressar o tempo como . Com matriz e escolha linear do mínimo, o custo é .
Pesos negativos e Bellman-Ford
Não foi possível carregar a animação. Abre o vídeo ou acompanha a descrição abaixo.
Bellman-Ford aceita pesos negativos. Relaxa todas as arestas repetidamente, no máximo passagens. Após a passagem , todos os caminhos com até arestas já foram contemplados, embora a implementação in-place possa já ter usado mais do que arestas dentro da mesma passagem quando processa as arestas por uma ordem favorável. Com arestas de peso 1 e de peso 1 nessa ordem, uma só passagem já põe , que não é a distância mínima com no máximo uma aresta. A garantia correta é por camadas: se leres apenas a camada anterior em cada passagem, a passagem dá exatamente os melhores caminhos com até arestas. Se não há ciclos negativos alcançáveis, um caminho mínimo pode ser simples e ter no máximo arestas. Esta é a razão do limite de passagens.
Para o grafo anterior, se a ordem das arestas for , , , a primeira passagem produz e a segunda produz . Uma terceira não muda nada, por isso podemos terminar mais cedo quando uma passagem não faz alterações.
Faz ainda uma passagem de diagnóstico. Se uma aresta com origem alcançável continua a melhorar, há um ciclo negativo alcançável de . Os vértices desse ciclo e os que ele alcança não têm distância mínima finita: dar mais voltas diminui o custo sem limite. Um ciclo negativo numa componente inalcançável não afeta o problema dessa origem.
O custo é tempo e memória auxiliar. Para assinalar todos os destinos afetados, recolhe os vértices melhoráveis e percorre as suas saídas. Não devolvas uma cadeia de predecessores como caminho mínimo quando ela pode atravessar o ciclo negativo.
Caminhos num DAG
Num DAG, processa os vértices em ordem topológica e relaxa as saídas de cada um. Quando chega a vez de , todos os possíveis predecessores já foram processados, por isso já é final, mesmo com pesos negativos.
Ordenação e relaxações custam . Vértices anteriores à origem na ordem podem permanecer inalcançáveis; as suas saídas não devem produzir somas com infinito. O método é também programação dinâmica sobre um DAG.
Floyd-Warshall
Para todos os pares, define como o menor custo usando apenas vértices como intermédios. Os extremos e não ficam sujeitos a essa restrição. Inicializa com infinito, põe zero na diagonal e incorpora cada aresta pelo mínimo entre o valor atual e o seu peso. Assim preservas o menor peso paralelo e uma autoaresta negativa, que já é um ciclo negativo.
Um melhor caminho ou não usa , ou divide-se em e . Assim,
O ciclo de deve ser o exterior, para conservar o significado do estado. Sem ciclos negativos, podemos atualizar a mesma matriz. O custo é tempo e espaço.
Com , , , , começamos com
Permitir cria ; permitir melhora para 2; permitir cria . A matriz final é
Para reconstruir, guarda next[i][j], o primeiro passo de para . Numa melhoria através de , atribui next[i][j] = next[i][k]. Um par infinito continua sem caminho. Se no fim , há um ciclo negativo. Todo o par com e é afetado; as entradas numéricas calculadas para esses pares não representam distâncias mínimas finitas.
Se só queremos alcançabilidade, a mesma estrutura dá o fecho transitivo: troca mínimo por OU e soma por E. Inicializar a diagonal a verdadeiro inclui caminhos de comprimento zero.
Johnson
Johnson calcula todos os pares num grafo esparso, admitindo arestas negativas mas rejeitando ciclos negativos. Não podemos somar uma constante a cada aresta: caminhos com mais arestas seriam penalizados mais vezes, podendo trocar a ordem dos custos.
Acrescenta uma origem com arestas de peso zero para todos os vértices. Corre Bellman-Ford e define os potenciais . Se detetar ciclo negativo, termina. Caso contrário, transforma cada peso em
Como , temos . Num caminho de a , os potenciais internos cancelam-se, pelo que . Todos os caminhos com os mesmos extremos recebem a mesma diferença, conservando o caminho mínimo.
Corre Dijkstra a partir de cada origem com e recupera
No exemplo , , , a origem nova tem arestas zero para os três vértices. Bellman-Ford dá , pelo caminho , e pela ligação direta . Os outros caminhos até custam 4 e 1, por isso não melhoram esse zero.
| Aresta | Peso original | Transformação | Peso novo |
|---|---|---|---|
| 0 | |||
| 3 | 1 | ||
| 4 | 4 |
Dijkstra encontra por . Como os potenciais dos extremos são iguais, recuperar dá . Para , os valores já diferem: a distância transformada é 1, mas a original é . O peso transformado serve para executar Dijkstra; a recuperação devolve a unidade de custo original.
Com heap binário e decreaseKey, a complexidade é , incluindo Bellman-Ford e as execuções de Dijkstra. A matriz de respostas ocupa , mesmo que o grafo seja esparso. Johnson é mais adequado a grafos esparsos; Floyd-Warshall oferece uma implementação direta com custo cúbico.
Exercícios
Escolher algoritmo com uma aresta negativa
Há arestas dirigidas s→a=3, s→b=7 e b→a=-6. Não há outras. Calcula a distância de s a a e explica por que Dijkstra não tem garantia nesta entrada. Indica dois métodos válidos.
Pista
Compara o caminho direto com o que passa por b.
Mais uma pista
7-6=1. O grafo é um DAG, além de não ter ciclos negativos.
Solução
A distância é 1 pelo caminho s,b,a. Dijkstra pode fixar a com 3 antes de retirar b com 7; a aresta negativa permite uma melhoria tardia que a prova excluía. Bellman-Ford é válido, e relaxar em ordem topológica s,b,a também é válido e custa O(n+m). Não há ciclo negativo, mas a hipótese de Dijkstra já falha.
Confere a tua resposta:
- Calculei distância 1 e o caminho.
- Expliquei a fixação prematura de a.
- Indiquei Bellman-Ford e ordem topológica.
Erros frequentes
Verificar apenas ausência de ciclos negativos não basta para Dijkstra. Uma BFS contaria arestas e também resolveria outro objetivo.
Melhorar através de um intermediário
As únicas arestas são a→b de peso 6, b→c de peso -2 e a→c de peso 5. Qual é a distância de a a c depois de permitir b como intermediário em Floyd-Warshall?
Pista
Compara a entrada atual com D[a,b]+D[b,c].
Solução
A distância passa de 5 para 4 pelo caminho a,b,c. O estado compara não usar b com usá-lo entre os extremos. O grafo não tem ciclos negativos; os pares sem caminho continuam infinitos. A soma só se calcula se as duas parcelas forem finitas.
Erros frequentes
Minimizar pesos de arestas isoladas sem somar o caminho não é relaxação. Não somes o sentinela de infinito com um peso negativo.
Pesos novos e distância original
As únicas arestas são , e . Os potenciais são , e . Calcula os três pesos reponderados e a distância original de b a c a partir da transformada. Explica por que a transformação conserva a ordem dos caminhos com os mesmos extremos.
Pista
Solução
Os pesos novos são , e . De b a c só há o caminho direto, com distância transformada 1. Recuperar dá .
Num caminho, cada potencial interior aparece com os dois sinais e cancela-se. Só sobra , a mesma diferença para todos os caminhos entre os mesmos extremos. Devolver 1 confundiria distância transformada com original.
Confere a tua resposta:
- Obtive pesos 0, 1, 4.
- Recuperei distância original 3 de b a c.
- Expliquei o cancelamento dos potenciais.
