Navegación por la ciudad
En resumen: Un proyecto de algoritmos: un programa lee el mapa de la ciudad como un grafo, encuentra con BFS la ruta con menos calles y con Dijkstra la más rápida, y muestra dónde se diferencian.
Problema
El mensajero pide una ruta
La empresa de mensajería del módulo «Grafos» tiene un almacén en la ciudad. El mapa es un archivo de texto con una calle por línea: desde dónde, hasta dónde y cuántos minutos se tarda en recorrerla. - es una calle de doble sentido, -> una de sentido único:
Almacén - Estación 4Almacén - Mercado 3Estación - Plaza 3Mercado - Plaza 2Mercado -> Parque 4Plaza - Cine 5Cine - Hospital 3Parque - Estadio 2Estadio - Hospital 2Almacén - Estadio 16El despachador necesita una ruta del almacén al hospital. El mensajero en bicicleta pide la ruta con menos cruces, porque se detiene en cada uno. El mensajero de la furgoneta pide la más rápida. Las respuestas no son las mismas.
Este proyecto es el programa que lee el mapa y responde a las dos preguntas. Lo construyes por pasos, cada uno con una salida con la que comparar la tuya.
La idea
El plan
- El mapa como grafo. Los cruces son vértices, las calles son aristas con peso: los minutos. Como en «Grafo: lista de adyacencia y matriz», guardas una lista de adyacencia: un diccionario de un cruce a una lista de calles.
- Menos calles. BFS desde el almacén por capas: primero los cruces a una calle, luego a dos.
- Los minutos de una ruta. La suma de los minutos de sus calles, para comparar las dos respuestas.
- Lo más rápido. Dijkstra con una cola de prioridad: siempre saca el cruce con menos minutos hasta ahora.
- La vuelta. El mismo programa del hospital al almacén. La calle de sentido único hace distinto el camino de vuelta.
El programa lee el mapa de la entrada estándar, para funcionar con cualquier mapa. Lo ejecutas con el archivo: dotnet run < map.txt.
Ejercicio
Paso 1: el mapa
Lee el mapa en un diccionario de un cruce a una lista de calles (a qué cruce y cuántos minutos). Una calle de doble sentido va a las listas de los dos extremos, una de sentido único solo a la lista de su inicio. Un cruce sin calles de salida también debe estar en el diccionario, con una lista vacía.
Imprime el número de cruces y los vecinos del Almacén, el Mercado y el Parque, en el orden de las líneas del archivo:
cruces: 8Almacén: Estación 4, Mercado 3, Estadio 16Mercado: Almacén 3, Plaza 2, Parque 4Parque: Estadio 2Solución
record Street(string To, int Minutes);
static void AddStreet(Dictionary<string, List<Street>> city, string a, string b, int minutes, bool twoWay){ if (!city.ContainsKey(a)) city[a] = []; if (!city.ContainsKey(b)) city[b] = []; city[a].Add(new Street(b, minutes)); if (twoWay) city[b].Add(new Street(a, minutes));}
static Dictionary<string, List<Street>> ReadMap(string text){ var city = new Dictionary<string, List<Street>>(); foreach (var line in text.Trim().Split('\n')) { var p = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); AddStreet(city, p[0], p[2], int.Parse(p[3]), p[1] == "-"); } return city;}
static string Neighbors(Dictionary<string, List<Street>> city, string place) => $"{place}: {string.Join(", ", city[place].Select(s => $"{s.To} {s.Minutes}"))}";
var city = ReadMap(Console.In.ReadToEnd());Console.WriteLine($"cruces: {city.Count}");Console.WriteLine(Neighbors(city, "Almacén"));Console.WriteLine(Neighbors(city, "Mercado"));Console.WriteLine(Neighbors(city, "Parque"));record Street va encima de class Program. Console.In.ReadToEnd() lee toda la entrada de una vez.El Parque tiene un vecino, el Estadio: Mercado -> Parque es de sentido único y el Mercado no está en la lista del Parque.
Ejercicio
Paso 2: menos calles
Escribe fewestStreets(city, start, end): un BFS desde start que se detiene en cuanto saca end y devuelve la ruta como lista de cruces. Para construir la ruta, guarda para cada cruce de dónde viniste (prev), luego retrocede desde end y da la vuelta a la lista.
menos calles: Almacén → Estadio → HospitalSolución
static List<string> Route(Dictionary<string, string> prev, string end){ var places = new List<string>(); for (string? at = end; at != null; at = prev.GetValueOrDefault(at)) places.Add(at); places.Reverse(); return places;}
static List<string> FewestStreets( Dictionary<string, List<Street>> city, string start, string end){ var prev = new Dictionary<string, string>(); var seen = new HashSet<string> { start }; var queue = new Queue<string>([start]); while (queue.TryDequeue(out var at)) { if (at == end) return Route(prev, end); foreach (var s in city[at]) { if (seen.Add(s.To)) { prev[s.To] = at; queue.Enqueue(s.To); } } } return [];}
var city = ReadMap(Console.In.ReadToEnd());Console.WriteLine($"menos calles: {string.Join(" → ", FewestStreets(city, "Almacén", "Hospital"))}");Un cruce se marca como visto cuando entra en la cola, no cuando sale: si no, un mismo cruce entra varias veces.
Ejercicio
Paso 3: cuántos minutos
Añade minutesOf(city, route), que suma los minutos de las calles a lo largo de la ruta, y describe(city, route), que devuelve la ruta, el número de calles y los minutos. Imprime la ruta del paso 2 con describe:
menos calles: Almacén → Estadio → Hospital (2 calles, 18 min)Solución
static int MinutesOf(Dictionary<string, List<Street>> city, List<string> route){ int total = 0; for (int i = 1; i < route.Count; i++) { total += city[route[i - 1]].First(s => s.To == route[i]).Minutes; } return total;}
static string Describe(Dictionary<string, List<Street>> city, List<string> route) => $"{string.Join(" → ", route)} ({route.Count - 1} calles, {MinutesOf(city, route)} min)";
var city = ReadMap(Console.In.ReadToEnd());Console.WriteLine($"menos calles: {Describe(city, FewestStreets(city, "Almacén", "Hospital"))}");Dos calles, pero 18 minutos: dieciséis de ellos en la circunvalación.
Míralo
Dos rutas en un mapa
static List<string> Fastest( Dictionary<string, List<Street>> city, string start, string end){ var dist = new Dictionary<string, int> { [start] = 0 }; var prev = new Dictionary<string, string>(); var queue = new PriorityQueue<string, int>(); queue.Enqueue(start, 0); while (queue.TryDequeue(out var at, out int minutes)) { if (minutes > dist[at]) continue; if (at == end) return Route(prev, end); foreach (var s in city[at]) { int t = minutes + s.Minutes; if (!dist.TryGetValue(s.To, out int d) || t < d) { dist[s.To] = t; prev[s.To] = at; queue.Enqueue(s.To, t); } } } return [];}static List<string> FewestStreets( Dictionary<string, List<Street>> city, string start, string end){ var prev = new Dictionary<string, string>(); var seen = new HashSet<string> { start }; var queue = new Queue<string>([start]); while (queue.TryDequeue(out var at)) { if (at == end) return Route(prev, end); foreach (var s in city[at]) { if (seen.Add(s.To)) { prev[s.To] = at; queue.Enqueue(s.To); } } } return [];}Paso a paso, en palabras
- El mapa: 8 cruces y 10 calles con sus minutos. Mercado → Parque es de sentido único, la circunvalación de Almacén a Estadio son 16 min.
- BFS, a una calle de Almacén: Estación, Mercado, Estadio. A él no le importan los minutos.
- A dos calles: Plaza, Parque, Hospital. Hospital ya está en la cola, desde Estadio.
- Menos calles: Almacén → Estadio → Hospital, 2 calles, pero 18 min: 16 de ellos son de la circunvalación.
- Dijkstra: la cola está ordenada por minutos. Almacén 0, sus vecinos entran en la cola: Estación 4, Mercado 3, Estadio 16.
- Lo más rápido en la cola es Mercado, 3 min. Nuevo: Plaza 5, Parque 7.
- Estación, 4 min: por él nada se vuelve más rápido.
- Lo más rápido en la cola es Plaza, 5 min. Nuevo: Cine 10.
- Parque con 7 min. Por él Estadio baja de 16 a 9.
- Lo más rápido en la cola es Estadio, 9 min. Nuevo: Hospital 11.
- Cine, 10 min: por él nada se vuelve más rápido.
- Hospital sale con 11 min: nada en la cola es más rápido, así que esta es la respuesta.
- Más rápido: Almacén → Mercado → Parque → Estadio → Hospital, 4 calles, 11 min.
- Dos respuestas a dos preguntas: 2 calles y 18 min o 4 calles y 11 min. Para el mensajero cuentan los minutos.
BFS solo ve el número de calles y elige la circunvalación. Dijkstra saca los cruces por minutos y llega al hospital en 11 min por el Mercado, la calle de sentido único y el Estadio. El Estadio primero tiene 16 min por la circunvalación, luego baja a 9 por el Parque.
Ejercicio
Paso 4: lo más rápido
Escribe fastest(city, start, end) con Dijkstra: dist guarda los minutos mínimos hasta ahora, la cola está ordenada por minutos y prev guarda la ruta. Imprime las dos rutas:
menos calles: Almacén → Estadio → Hospital (2 calles, 18 min)más rápido: Almacén → Mercado → Parque → Estadio → Hospital (4 calles, 11 min)Solución
static List<string> Fastest( Dictionary<string, List<Street>> city, string start, string end){ var dist = new Dictionary<string, int> { [start] = 0 }; var prev = new Dictionary<string, string>(); var queue = new PriorityQueue<string, int>(); queue.Enqueue(start, 0); while (queue.TryDequeue(out var at, out int minutes)) { if (minutes > dist[at]) continue; if (at == end) return Route(prev, end); foreach (var s in city[at]) { int t = minutes + s.Minutes; if (!dist.TryGetValue(s.To, out int d) || t < d) { dist[s.To] = t; prev[s.To] = at; queue.Enqueue(s.To, t); } } } return [];}
var city = ReadMap(Console.In.ReadToEnd());Console.WriteLine($"menos calles: {Describe(city, FewestStreets(city, "Almacén", "Hospital"))}");Console.WriteLine($"más rápido: {Describe(city, Fastest(city, "Almacén", "Hospital"))}");PriorityQueue<string, int> guarda el cruce con los minutos como prioridad. TryDequeue(out var at, out int minutes) da las dos cosas. TryGetValue dice si el cruce ya tiene minutos.Pruébalo
Completa
Un cruce puede entrar en la cola varias veces, cuando bajan los minutos hasta él. La entrada vieja se queda. ¿Qué hace el programa cuando la saca?
if (minutes > dist[at]) ;continue: el cruce ya salió con menos minutos y sus vecinos ya se miraron. La entrada vieja se salta. Así nunca hace falta borrar entradas de la cola.
Pruébalo
Predice la vuelta
El mensajero vuelve del hospital al almacén. El programa pregunta fastest(city, "Hospital", "Almacén"). ¿Qué imprime?
var city = ReadMap(Console.In.ReadToEnd());Console.WriteLine($"vuelta: {Describe(city, Fastest(city, "Hospital", "Almacén"))}");Mercado -> Parque es de sentido único: del Parque al Mercado no se puede, así que la ruta de 11 min no sirve al revés. Lo más rápido es por el Cine y la Plaza: 3 + 5 + 2 + 3 = 13 min. La ruta por la circunvalación son 18 min.
Ejercicio
Paso 5: el programa completo
Añade la vuelta. El programa completo imprime:
menos calles: Almacén → Estadio → Hospital (2 calles, 18 min)más rápido: Almacén → Mercado → Parque → Estadio → Hospital (4 calles, 11 min)vuelta: Hospital → Cine → Plaza → Mercado → Almacén (4 calles, 13 min)El programa completo
record Street(string To, int Minutes);
class Program{ static void AddStreet(Dictionary<string, List<Street>> city, string a, string b, int minutes, bool twoWay) { if (!city.ContainsKey(a)) city[a] = []; if (!city.ContainsKey(b)) city[b] = []; city[a].Add(new Street(b, minutes)); if (twoWay) city[b].Add(new Street(a, minutes)); }
static Dictionary<string, List<Street>> ReadMap(string text) { var city = new Dictionary<string, List<Street>>(); foreach (var line in text.Trim().Split('\n')) { var p = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); AddStreet(city, p[0], p[2], int.Parse(p[3]), p[1] == "-"); } return city; }
static List<string> Route(Dictionary<string, string> prev, string end) { var places = new List<string>(); for (string? at = end; at != null; at = prev.GetValueOrDefault(at)) places.Add(at); places.Reverse(); return places; }
static List<string> FewestStreets( Dictionary<string, List<Street>> city, string start, string end) { var prev = new Dictionary<string, string>(); var seen = new HashSet<string> { start }; var queue = new Queue<string>([start]); while (queue.TryDequeue(out var at)) { if (at == end) return Route(prev, end); foreach (var s in city[at]) { if (seen.Add(s.To)) { prev[s.To] = at; queue.Enqueue(s.To); } } } return []; }
static int MinutesOf(Dictionary<string, List<Street>> city, List<string> route) { int total = 0; for (int i = 1; i < route.Count; i++) { total += city[route[i - 1]].First(s => s.To == route[i]).Minutes; } return total; }
static string Describe(Dictionary<string, List<Street>> city, List<string> route) => $"{string.Join(" → ", route)} ({route.Count - 1} calles, {MinutesOf(city, route)} min)";
static List<string> Fastest( Dictionary<string, List<Street>> city, string start, string end) { var dist = new Dictionary<string, int> { [start] = 0 }; var prev = new Dictionary<string, string>(); var queue = new PriorityQueue<string, int>(); queue.Enqueue(start, 0); while (queue.TryDequeue(out var at, out int minutes)) { if (minutes > dist[at]) continue; if (at == end) return Route(prev, end); foreach (var s in city[at]) { int t = minutes + s.Minutes; if (!dist.TryGetValue(s.To, out int d) || t < d) { dist[s.To] = t; prev[s.To] = at; queue.Enqueue(s.To, t); } } } return []; }
static void Main() { var city = ReadMap(Console.In.ReadToEnd()); Console.WriteLine($"menos calles: {Describe(city, FewestStreets(city, "Almacén", "Hospital"))}"); Console.WriteLine($"más rápido: {Describe(city, Fastest(city, "Almacén", "Hospital"))}"); Console.WriteLine($"vuelta: {Describe(city, Fastest(city, "Hospital", "Almacén"))}"); }}Pruébalo
Encuentra el error
Un compañero ha acortado addStreet. Ahora la vuelta sale en 11 min:
vuelta: Hospital → Estadio → Parque → Mercado → Almacén (4 calles, 11 min)Elige la línea con el error.
El sentido contrario se añade para cada calle, también para la de sentido único. Mercado -> Parque se vuelve de doble sentido y la vuelta circula por ella en contra del tráfico. La calle inversa solo debe añadirse cuando twoWay es verdadero.
Míralo
Mide: la ciudad crece
Una ciudad real tiene miles de cruces. La cuadrícula es una prueba de velocidad: n cruces en un cuadrado, calles entre vecinos, minutos según una fórmula. El mensajero sale de la esquina superior izquierda y va a la inferior derecha:
static Dictionary<string, List<Street>> GridCity(int size){ var city = new Dictionary<string, List<Street>>(); for (int r = 0; r < size; r++) { for (int c = 0; c < size; c++) { string at = $"{r}:{c}"; if (c + 1 < size) AddStreet(city, at, $"{r}:{c + 1}", 1 + (r * 7 + c * 13) % 9, true); if (r + 1 < size) AddStreet(city, at, $"{r + 1}:{c}", 1 + (r * 11 + c * 5) % 9, true); } } return city;}
var city = GridCity(100);Console.WriteLine($"cruces: {city.Count}");var few = FewestStreets(city, "0:0", "99:99");Console.WriteLine($"menos calles: {few.Count - 1} calles, {MinutesOf(city, few)} min");var fast = Fastest(city, "0:0", "99:99");Console.WriteLine($"más rápido: {MinutesOf(city, fast)} min");cruces: 10000menos calles: 198 calles, 990 minmás rápido: 693 minEjecuta los dos algoritmos en el navegador. BFS mira cada calle una vez: O(n). Dijkstra también, pero cada entrada del heap cuesta O(log n): O(n log n). Con n = 1000 son 3966 frente a 15.778 operaciones, y con n = 100 000 son 398.158 frente a 2.348.779:
- BFS: la menor cantidad de calles
O(n) - Dijkstra: el más rápido
O(n log n)
| Algoritmo | n = 10 | n = 1000 | n = 100.000 |
|---|---|---|---|
| BFS: la menor cantidad de calles | 22 | 3966 | 398.158 |
| Dijkstra: el más rápido | 34 | 15.778 | 2.348.779 |
static List<string> Fastest( Dictionary<string, List<Street>> city, string start, string end){ var dist = new Dictionary<string, int> { [start] = 0 }; var prev = new Dictionary<string, string>(); var queue = new PriorityQueue<string, int>(); queue.Enqueue(start, 0); while (queue.TryDequeue(out var at, out int minutes)) { if (minutes > dist[at]) continue; if (at == end) return Route(prev, end); foreach (var s in city[at]) { int t = minutes + s.Minutes; if (!dist.TryGetValue(s.To, out int d) || t < d) { dist[s.To] = t; prev[s.To] = at; queue.Enqueue(s.To, t); } } } return [];}Más precisamente, para curiosos
Dijkstra es más lento, pero responde a la pregunta de los minutos, y BFS no puede. Los dos se detienen en cuanto sacan el destino: para un destino cercano miran una parte pequeña de la ciudad.
La navegación de países enteros no ejecuta un Dijkstra simple desde cada lugar. Preprocesa el mapa (por ejemplo con contraction hierarchies) y busca con A*, que añade una estimación de cuánto falta hasta el destino. La base sigue siendo Dijkstra.
Ejercicio
Obras en la calle
Resuelve esta en tu editor.
Una calle cerrada. La calle Parque – Estadio está cerrada por obras. Dale a readMap un conjunto de calles cerradas ("Parque Estadio") que salte esas líneas en los dos sentidos. Imprime las dos rutas del almacén al hospital:
menos calles: Almacén → Estadio → Hospital (2 calles, 18 min)más rápido: Almacén → Mercado → Plaza → Cine → Hospital (4 calles, 13 min)Más tareas
- Solo los minutos. Escribe una variante de
fastestque devuelva los minutos hasta cada cruce sin detenerse en el destino. ¿Qué cruce es el más lejano del almacén? - Sin un cruce. La Plaza está cerrada por un evento. ¿Cómo la dejas fuera sin tocar el mapa?
- Una circunvalación vacía. De noche la circunvalación está vacía y se recorre en 6 min. Cambia los minutos en el mapa: ¿cuál es ahora la ruta más rápida y es la misma que la de BFS?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Cola de prioridad · Grafo: lista de adyacencia y matriz · BFS: las menos paradas · Dijkstra
- Ejercicios sobre el tema: Proyecto: navegación por la ciudad