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: java Main.java < 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) {}
record Stop(String place, int minutes) {}
static void addStreet(Map<String, List<Street>> city, String a, String b, int minutes, boolean twoWay) { city.computeIfAbsent(a, k -> new ArrayList<>()).add(new Street(b, minutes)); city.computeIfAbsent(b, k -> new ArrayList<>()); if (twoWay) city.get(b).add(new Street(a, minutes));}
static Map<String, List<Street>> readMap(String text) { Map<String, List<Street>> city = new HashMap<>(); for (String line : text.strip().split("\n")) { String[] p = line.trim().split("\\s+"); addStreet(city, p[0], p[2], Integer.parseInt(p[3]), p[1].equals("-")); } return city;}
static String neighbors(Map<String, List<Street>> city, String place) { List<String> parts = new ArrayList<>(); for (Street s : city.get(place)) parts.add(s.to() + " " + s.minutes()); return place + ": " + String.join(", ", parts);}
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("cruces: " + city.size());System.out.println(neighbors(city, "Almacén"));System.out.println(neighbors(city, "Mercado"));System.out.println(neighbors(city, "Parque"));import java.io.IOException;, import java.nio.charset.StandardCharsets; e import java.util.*;, y main declara throws IOException por readAllBytes.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(Map<String, String> prev, String end) { List<String> places = new ArrayList<>(); for (String at = end; at != null; at = prev.get(at)) places.add(at); Collections.reverse(places); return places;}
static List<String> fewestStreets( Map<String, List<Street>> city, String start, String end) { Map<String, String> prev = new HashMap<>(); Set<String> seen = new HashSet<>(List.of(start)); Deque<String> queue = new ArrayDeque<>(List.of(start)); while (!queue.isEmpty()) { String at = queue.poll(); if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { if (seen.add(s.to())) { prev.put(s.to(), at); queue.add(s.to()); } } } return List.of();}
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("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(Map<String, List<Street>> city, List<String> route) { int total = 0; for (int i = 1; i < route.size(); i++) { for (Street s : city.get(route.get(i - 1))) { if (s.to().equals(route.get(i))) { total += s.minutes(); break; } } } return total;}
static String describe(Map<String, List<Street>> city, List<String> route) { return String.join(" → ", route) + " (" + (route.size() - 1) + " calles, " + minutesOf(city, route) + " min)";}
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("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( Map<String, List<Street>> city, String start, String end) { Map<String, Integer> dist = new HashMap<>(); Map<String, String> prev = new HashMap<>(); PriorityQueue<Stop> queue = new PriorityQueue<>( Comparator.comparingInt(Stop::minutes)); dist.put(start, 0); queue.add(new Stop(start, 0)); while (!queue.isEmpty()) { Stop stop = queue.poll(); String at = stop.place(); if (stop.minutes() > dist.get(at)) continue; if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { int t = stop.minutes() + s.minutes(); Integer best = dist.get(s.to()); if (best == null || t < best) { dist.put(s.to(), t); prev.put(s.to(), at); queue.add(new Stop(s.to(), t)); } } } return List.of();}static List<String> fewestStreets( Map<String, List<Street>> city, String start, String end) { Map<String, String> prev = new HashMap<>(); Set<String> seen = new HashSet<>(List.of(start)); Deque<String> queue = new ArrayDeque<>(List.of(start)); while (!queue.isEmpty()) { String at = queue.poll(); if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { if (seen.add(s.to())) { prev.put(s.to(), at); queue.add(s.to()); } } } return List.of();}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( Map<String, List<Street>> city, String start, String end) { Map<String, Integer> dist = new HashMap<>(); Map<String, String> prev = new HashMap<>(); PriorityQueue<Stop> queue = new PriorityQueue<>( Comparator.comparingInt(Stop::minutes)); dist.put(start, 0); queue.add(new Stop(start, 0)); while (!queue.isEmpty()) { Stop stop = queue.poll(); String at = stop.place(); if (stop.minutes() > dist.get(at)) continue; if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { int t = stop.minutes() + s.minutes(); Integer best = dist.get(s.to()); if (best == null || t < best) { dist.put(s.to(), t); prev.put(s.to(), at); queue.add(new Stop(s.to(), t)); } } } return List.of();}
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("menos calles: " + describe(city, fewestStreets(city, "Almacén", "Hospital")));System.out.println("más rápido: " + describe(city, fastest(city, "Almacén", "Hospital")));PriorityQueue<Stop> con Comparator.comparingInt(Stop::minutes) saca la entrada con menos minutos. record Stop es el par de cruce y minutos. dist.get va a un Integer, porque para un cruce nuevo es null.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 (stop.minutes() > dist.get(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?
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("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
import java.io.IOException;import java.nio.charset.StandardCharsets;import java.util.*;
record Street(String to, int minutes) {}
record Stop(String place, int minutes) {}
public class Main { static void addStreet(Map<String, List<Street>> city, String a, String b, int minutes, boolean twoWay) { city.computeIfAbsent(a, k -> new ArrayList<>()).add(new Street(b, minutes)); city.computeIfAbsent(b, k -> new ArrayList<>()); if (twoWay) city.get(b).add(new Street(a, minutes)); }
static Map<String, List<Street>> readMap(String text) { Map<String, List<Street>> city = new HashMap<>(); for (String line : text.strip().split("\n")) { String[] p = line.trim().split("\\s+"); addStreet(city, p[0], p[2], Integer.parseInt(p[3]), p[1].equals("-")); } return city; }
static List<String> route(Map<String, String> prev, String end) { List<String> places = new ArrayList<>(); for (String at = end; at != null; at = prev.get(at)) places.add(at); Collections.reverse(places); return places; }
static List<String> fewestStreets( Map<String, List<Street>> city, String start, String end) { Map<String, String> prev = new HashMap<>(); Set<String> seen = new HashSet<>(List.of(start)); Deque<String> queue = new ArrayDeque<>(List.of(start)); while (!queue.isEmpty()) { String at = queue.poll(); if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { if (seen.add(s.to())) { prev.put(s.to(), at); queue.add(s.to()); } } } return List.of(); }
static int minutesOf(Map<String, List<Street>> city, List<String> route) { int total = 0; for (int i = 1; i < route.size(); i++) { for (Street s : city.get(route.get(i - 1))) { if (s.to().equals(route.get(i))) { total += s.minutes(); break; } } } return total; }
static String describe(Map<String, List<Street>> city, List<String> route) { return String.join(" → ", route) + " (" + (route.size() - 1) + " calles, " + minutesOf(city, route) + " min)"; }
static List<String> fastest( Map<String, List<Street>> city, String start, String end) { Map<String, Integer> dist = new HashMap<>(); Map<String, String> prev = new HashMap<>(); PriorityQueue<Stop> queue = new PriorityQueue<>( Comparator.comparingInt(Stop::minutes)); dist.put(start, 0); queue.add(new Stop(start, 0)); while (!queue.isEmpty()) { Stop stop = queue.poll(); String at = stop.place(); if (stop.minutes() > dist.get(at)) continue; if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { int t = stop.minutes() + s.minutes(); Integer best = dist.get(s.to()); if (best == null || t < best) { dist.put(s.to(), t); prev.put(s.to(), at); queue.add(new Stop(s.to(), t)); } } } return List.of(); }
public static void main(String[] args) throws IOException { String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8); Map<String, List<Street>> city = readMap(text); System.out.println("menos calles: " + describe(city, fewestStreets(city, "Almacén", "Hospital"))); System.out.println("más rápido: " + describe(city, fastest(city, "Almacén", "Hospital"))); System.out.println("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 Map<String, List<Street>> gridCity(int size) { Map<String, List<Street>> city = new HashMap<>(); 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;}
Map<String, List<Street>> city = gridCity(100);System.out.println("cruces: " + city.size());List<String> few = fewestStreets(city, "0:0", "99:99");System.out.println("menos calles: " + (few.size() - 1) + " calles, " + minutesOf(city, few) + " min");List<String> fast = fastest(city, "0:0", "99:99");System.out.println("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( Map<String, List<Street>> city, String start, String end) { Map<String, Integer> dist = new HashMap<>(); Map<String, String> prev = new HashMap<>(); PriorityQueue<Stop> queue = new PriorityQueue<>( Comparator.comparingInt(Stop::minutes)); dist.put(start, 0); queue.add(new Stop(start, 0)); while (!queue.isEmpty()) { Stop stop = queue.poll(); String at = stop.place(); if (stop.minutes() > dist.get(at)) continue; if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { int t = stop.minutes() + s.minutes(); Integer best = dist.get(s.to()); if (best == null || t < best) { dist.put(s.to(), t); prev.put(s.to(), at); queue.add(new Stop(s.to(), t)); } } } return List.of();}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