Algoritmo de Dijkstra
Se da un grafo ponderado dirigido o no dirigido con vértices y aristas. Los pesos de todas las aristas son no negativos. También se da un vértice de partida . Este artículo trata de encontrar las longitudes de los caminos más cortos desde un vértice de partida hacia todos los demás vértices, y de devolver los propios caminos más cortos.
Este problema también se llama problema de caminos más cortos desde una sola fuente (single-source shortest paths).
Algoritmo
Aquí está un algoritmo descrito por el científico de la computación neerlandés Edsger W. Dijkstra en 1959.
Creamos un arreglo donde para cada vértice guardamos en la longitud actual del camino más corto de a . Inicialmente , y para todos los demás vértices esta longitud es igual a infinito. En la implementación se elige como infinito un número suficientemente grande (del que se garantiza que es mayor que cualquier longitud de camino posible).
Además, mantenemos un arreglo booleano que almacena, para cada vértice , si está marcado. Inicialmente todos los vértices están desmarcados:
El algoritmo de Dijkstra realiza iteraciones. En cada iteración se elige un vértice no marcado que tenga el menor valor :
Evidentemente, en la primera iteración se selecciona el vértice de partida .
El vértice seleccionado se marca. A continuación, desde el vértice se realizan relajaciones (relaxations): se consideran todas las aristas de la forma , y para cada vértice el algoritmo intenta mejorar el valor . Si la longitud de la arista actual es , el código de la relajación es:
Después de considerar todas esas aristas, termina la iteración actual. Finalmente, tras iteraciones, todos los vértices estarán marcados y el algoritmo termina. Afirmamos que los valores encontrados son las longitudes de los caminos más cortos desde hasta todos los vértices .
Nótese que si algunos vértices son inalcanzables desde el vértice de partida , los valores para ellos permanecerán infinitos. Obviamente, las últimas iteraciones del algoritmo elegirán esos vértices, pero no se hará ningún trabajo útil con ellos. Por lo tanto, el algoritmo se puede detener en cuanto el vértice seleccionado tenga distancia infinita.
Restauración de los caminos más cortos
Por lo general no solo hacen falta las longitudes de los caminos más cortos, sino también los propios caminos más cortos. Veamos cómo mantener información suficiente para restaurar el camino más corto de a cualquier vértice. Mantendremos un arreglo de predecesores en el que, para cada vértice , es el penúltimo vértice en el camino más corto de a . Aquí usamos el hecho de que si tomamos el camino más corto hasta algún vértice y quitamos de este camino, obtenemos un camino que termina en el vértice , y este camino será el más corto para el vértice . Este arreglo de predecesores se puede usar para restaurar el camino más corto hasta cualquier vértice: empezando por , tomamos repetidamente el predecesor del vértice actual hasta llegar al vértice de partida , y así obtenemos el camino más corto pedido con los vértices listados en orden inverso. Así, el camino más corto hasta el vértice es igual a:
Construir este arreglo de predecesores es muy simple: en cada relajación exitosa, es decir, cuando para algún vértice seleccionado hay una mejora en la distancia hasta algún vértice , actualizamos el vértice predecesor de con el vértice :
Demostración
La afirmación principal sobre la que se basa la corrección del algoritmo de Dijkstra es la siguiente:
Después de que cualquier vértice queda marcado, la distancia actual hasta él es la más corta, y ya no volverá a cambiar.
La demostración se hace por inducción. Para la primera iteración este enunciado es obvio: el único vértice marcado es , y la distancia hasta él es efectivamente la longitud del camino más corto hasta . Ahora supongamos que este enunciado es cierto para todas las iteraciones anteriores, es decir, para todos los vértices ya marcados; vamos a probar que no se viola después de que termina la iteración actual. Sea el vértice seleccionado en la iteración actual, es decir, es el vértice que el algoritmo va a marcar. Ahora hay que demostrar que es efectivamente igual a la longitud del camino más corto hasta él .
Consideremos el camino más corto hasta el vértice . Este camino se puede partir en dos partes: , que consiste solo en nodos marcados (al menos el vértice de partida forma parte de ), y el resto del camino (puede incluir un vértice marcado, pero siempre empieza con un vértice no marcado). Denotemos el primer vértice del camino como , y el último vértice del camino como .
Primero demostramos nuestro enunciado para el vértice , es decir, vamos a probar que . Esto es casi obvio: en una de las iteraciones anteriores elegimos el vértice y realizamos una relajación desde él. Como (en virtud de la elección del vértice ) el camino más corto hasta es el camino más corto hasta más la arista , la relajación desde fijó el valor de a la longitud del camino más corto .
Como los pesos de las aristas son no negativos, la longitud del camino más corto (que acabamos de demostrar que es igual a ) no supera la longitud del camino más corto hasta el vértice . Dado que (porque el algoritmo de Dijkstra no pudo haber encontrado un camino más corto que el más corto posible), obtenemos la desigualdad:
Por otro lado, como ambos vértices y están desmarcados, y la iteración actual eligió el vértice y no , obtenemos otra desigualdad:
De estas dos desigualdades concluimos que , y entonces a partir de las igualdades encontradas antes obtenemos:
Q.E.D.
Implementación
El algoritmo de Dijkstra realiza iteraciones. En cada iteración selecciona un vértice no marcado con el menor valor , lo marca y revisa todas las aristas intentando mejorar el valor .
El tiempo de ejecución del algoritmo consiste en:
- búsquedas de un vértice con el menor valor entre vértices no marcados
- intentos de relajación
Para la implementación más simple de estas operaciones, en cada iteración la búsqueda del vértice requiere operaciones, y cada relajación se puede realizar en . Por lo tanto, el comportamiento asintótico resultante del algoritmo es:
Esta complejidad es óptima para un grafo denso, es decir, cuando . Sin embargo, en grafos dispersos, cuando es mucho menor que el número máximo de aristas , el problema se puede resolver con complejidad . El algoritmo y la implementación se pueden encontrar en el artículo Dijkstra en grafos dispersos.
const int INF = 1000000000;
vector<vector<pair<int, int>>> adj;
void dijkstra(int s, vector<int> & d, vector<int> & p) {
int n = adj.size();
d.assign(n, INF);
p.assign(n, -1);
vector<bool> u(n, false);
d[s] = 0;
for (int i = 0; i < n; i++) {
int v = -1;
for (int j = 0; j < n; j++) {
if (!u[j] && (v == -1 || d[j] < d[v]))
v = j;
}
if (d[v] == INF)
break;
u[v] = true;
for (auto edge : adj[v]) {
int to = edge.first;
int len = edge.second;
if (d[v] + len < d[to]) {
d[to] = d[v] + len;
p[to] = v;
}
}
}
}Aquí el grafo se almacena como lista de adyacencia: para cada vértice , contiene la lista de aristas que salen de este vértice, es decir, la lista de pair<int,int> donde el primer elemento del par es el vértice en el otro extremo de la arista, y el segundo elemento es el peso de la arista.
La función toma el vértice de partida y dos vectores que se usarán como valores de retorno.
En primer lugar, el código inicializa los arreglos: distancias , marcas y predecesores . Luego realiza iteraciones. En cada iteración se selecciona el vértice que tiene la menor distancia entre todos los vértices no marcados. Si la distancia al vértice seleccionado es igual a infinito, el algoritmo se detiene. En caso contrario el vértice se marca, y se revisan todas las aristas que salen de este vértice. Si es posible relajar a lo largo de la arista (es decir, se puede mejorar la distancia ), se actualizan la distancia y el predecesor .
Después de realizar todas las iteraciones, el arreglo almacena las longitudes de los caminos más cortos hasta todos los vértices, y el arreglo almacena los predecesores de todos los vértices (excepto el vértice de partida ). El camino hasta cualquier vértice se puede restaurar de la siguiente manera:
vector<int> restore_path(int s, int t, vector<int> const& p) {
vector<int> path;
for (int v = t; v != s; v = p[v])
path.push_back(v);
path.push_back(s);
reverse(path.begin(), path.end());
return path;
}Referencias
- Edsger Dijkstra. A note on two problems in connexion with graphs [1959]
- Thomas Cormen, Charles Leiserson, Ronald Rivest, Clifford Stein. Introduction to Algorithms [2005]
Problemas de práctica
- Timus - Ivan’s Car [Difficulty:Medium]
- Timus - Sightseeing Trip
- SPOJ - SHPATH [Difficulty:Easy]
- Codeforces - Dijkstra? [Difficulty:Easy]
- Codeforces - Shortest Path
- Codeforces - Jzzhu and Cities
- Codeforces - The Classic Problem
- Codeforces - President and Roads
- Codeforces - Complete The Graph
- TopCoder - SkiResorts
- TopCoder - MaliciousPath
- SPOJ - Ada and Trip
- LA - 3850 - Here We Go(relians) Again
- GYM - Destination Unknown (D)
- UVA 12950 - Even Obsession
- GYM - Journey to Grece (A)
- UVA 13030 - Brain Fry
- UVA 1027 - Toll
- UVA 11377 - Airport Setup
- Codeforces - Dynamic Shortest Path
- UVA 11813 - Shopping
- UVA 11833 - Route Change
- SPOJ - Easy Dijkstra Problem
- LA - 2819 - Cave Raider
- UVA 12144 - Almost Shortest Path
- UVA 12047 - Highest Paid Toll
- UVA 11514 - Batman
- Codeforces - Team Rocket Rises Again
- UVA - 11338 - Minefield
- UVA 11374 - Airport Express
- UVA 11097 - Poor My Problem
- UVA 13172 - The music teacher
- Codeforces - Dirty Arkady’s Kitchen
- SPOJ - Delivery Route
- SPOJ - Costly Chess
- CSES - Shortest Routes 1
- CSES - Flight Discount
- CSES - Flight Routes