Está en la página 1de 13

Introducción

En el siguiente informe enmarcado en el curso flujo en redes se resuelve el problema de Rutas


Mínimas. Para esta tarea se cuenta con una base de datos de 264.346 intersecciones
correspondientes a la ciudad de Nueva York además de 730.100 calles que conectan estas
intersecciones con los respectivos tiempos de traslado y distancia. También están las coordenadas de
cada intersección. Con estos datos primero se construyó el grafo en Python.

Para resolver las rutas mínimas se aplicarán los algoritmos aprendidos en el curso: Dijkstra, Dijkstra
Bidireccional, Bellman-Ford y A*. Estos fueron implementados computacionalmente específicamente
para las rutas desde los orígenes {100.608, 3.092, 445, 213.417, 191.421, 56.877, 83.129, 6.257,
91.541, 238.491} a los destinos {179.337, 30.445, 80.302, 85.081, 172.143, 4.354, 225.531, 171.839,
97.368, 125.487}. Se analizaron los órdenes de complejidad computacional, uso de memoria y
tiempo de ejecución, siendo el de menor tiempo promedio de ejecución el Dijkstra Bidireccional con
1,617 segundos para el problema de tiempo y con 1,988 segundos para el problema de distancia. El
algoritmo de menor uso de memoria fue el Dijkstra Bidireccional con 1,189 GB.

Luego se realizó el mismo problema de rutas mínimas, para los mismos orígenes y destinos, pero
usando el algoritmo de Heap Binario. Este algoritmo mejoró los tiempos de solución y fue el de
menor tiempo respecto de los 4 algoritmos previamente realizados con 1,465 segundos para ruta
mínima de tiempo y tuvo 1,38 segundos para el problema con distancia. En cuanto al uso de
memoria se obtuvo una considerable baja con 339,2 MB para el algoritmo con Heap.

Por último, al problema de rutas mínimas se le añadirán restricciones de virajes en cierto sentido,
pasar por cierto número de intersecciones, entre otras. Estos casos fueron analizados y comparados
con el mundo real para complementar lo teórico y lo práctico.

Parte 1

A la base de datos descrita previamente se debe calcular la ruta mínima entre los orígenes y destinos
mencionados. Los algoritmos de ruta mínima que se deben implementar son: Bellman-Ford, Dijkstra,
Dijkstra Bidireccional y A*. Estos algoritmos se implementaron con la librería Networkx en Python. A
continuación se da una breve descripción teórica y se analiza la complejidad de los algoritmos. (Sea
n=nodos(intersecciones), m=arcos(calles))

Dijkstra: algoritmo iterativo que proporciona la ruta más corta desde un nodo inicial particular a
todos los otros nodos en el grafo. La complejidad es O(n^2), sin embargo no es tan eficiente cuando
el grado es muy grande.

Bellman-Ford: ​determina la ruta más corta desde un nodo origen hacia los demás nodos para ello es
requerido como entrada un grafo cuyas aristas posean pesos o costos. La diferencia de este
algoritmo con los demás es que los pesos o costos pueden tener valores negativos ya que
Bellman-Ford me permite detectar la existencia de un ciclo negativo​.El algoritmo parte de un origen y
usando el paso de relajación selecciona el vértice de menor peso y actualiza sus distancias. La
complejidad de este algoritmo es O(n*m).

Dijkstra Bidireccional: ​Ejecuta dos búsquedas simultáneas, una adelante del origen y uno hacia atrás
del destino, deteniéndose cuando los dos se encuentran en el medio. Esta implementación puede
utilizarse con un grafo dirigido y uno no dirigido.​No tiene mejor complejidad que Dijkstra. Realiza
2*(n/2)^2.
Algoritmo A*: este es un algoritmo heurístico, ya que mediante el uso de una función de evaluación
heurística etiquetando los nodos de la red para determinar la probabilidad de estos nodos de
pertenecer a la ruta óptima. La función de evaluación es f(n) = g(n) + h(n), donde g(n) es la distancia
desde el nodo origen s al n, h(n) es la distancia estimada desde el nodo n al destino t. En teoría, si la
heurística es óptima, la complejidad del algoritmo es O(n).

Al comparar estos algoritmos cualitativamente sin tener los resultados de su implementación se


espera que el algoritmo de Bellman-Ford es el que más tarde en la ejecución, debido a que este
algoritmo tiene un paso de relajación, ya que asume que puede venir un arco con costo de pasada
negativo, pero en el caso de esta base de datos no cuenta con costos negativos. Al comparar los
algoritmos Dijkstra se espera que ambos tarden menos que el Bellman-Ford, sin embargo el
bidireccional se espera que tarde menos que el Dijkstra tradicional, ya que el bidireccional resuelve
simultáneamente desde el origen hacia adelante y desde el destino hacia atrás, por lo que se
encontraran en algún momento y este tiempo será menor al comparar que solo vaya desde el origen
hacia adelante. Respecto al algoritmo A*, se espera que sea menor que Bellman-Ford. Sin embargo,
al comparar con el algoritmo Dijkstra va a depender de cómo funcione la heurística con los datos del
problema en específico, pero se espera que sea relativamente similar.

Para resolver los algoritmos descritos, se debió crear un grafo. Para esto se usa la librería Networkx,
la cual permite agregar los nodos y los arcos con sus respectivos pesos. Los pasos a seguir para crear
el grafo son:
1. Creación Grafo:
→ Usando las opciones que ofrece la librería Network, creamos un Grafo usando la notación
nx.DiGraph() . Esto permite crear un grafo dirigido, ya que dado que es un problema real de
tráfico, los arcos pueden poseer diferentes direcciones por lo que este grafo de Networkx
resulta útil. Dado que se pide según distancia y tiempo, se crean 2 grafos en donde los nodos
y los arcos que poseen son los mismos pero el peso de cada arco variará según los tiempos y
distancias.

2. Creación nodos (intersecciones):


→ Se procede a leer el archivo de coordenadas, creando un diccionario que tiene como llave
cada número de nodo y como valores tiene las coordenadas de cada nodo. Se usa este
archivo ya que no existen nodos que se repiten por lo que se crea solo una vez cada nodo.
Luego se crea el nodo con las llaves del diccionario usando el comando G.add_node() .

Imagen 1. Creación de nodos

3. Creación arcos (calles):


→ Ahora se procede a leer los archivos de distancias y tiempos, en donde se leen los nodos
involucrados y se procede a crear cada arco con su origen y destinos los nodos y como el
peso del grafo se usa el valor de tiempo y distancia. Esto se realiza con el comando
G.add_edge(origen, destino, v alor tiempo/distancia).
Imagen 2. Creación de los arcos dirigidos en los nodos creados

Una vez creado el grafo se procedió a aplicar los algoritmos descritos, usando la misma librería
Network, para cada par origen-destino requeridos. Esto se hace para cada algoritmo según la forma:

a) Algoritmo de Dijkstra:
Largo de la ruta mínima → ​nx.dijkstra_path_length(G, origen, destino,
weight=​'weight'​)

Camino de la ruta mínima → ​nx.dijkstra_path(G, origen, destino,


weight=​'weight'​)
En donde recibe los parámetros del grafo G, el valor del nodo de origen, el valor del nodo de
destino y luego se agrega un string en el parámetro weight para que use los pesos determinados
en cada arco.
b) Algoritmo de Dijkstra Bidireccional:
Largo y camino de la ruta mínima → ​nx.bidirectional_dijkstra(G, origen,
destino, weight=​'weight'​)
El código recibe como parámetros el grafo G, el nodo de origen, el nodo de destino y en el
parámetro weight se usa un string para usar los pesos de los arcos existentes. En este caso se
retorna tanto el camino y la distancia de la ruta en un solo código.

c) Algoritmo de Bellman-Ford:
Largo de la ruta mínima → ​nx.bellman_ford_path_length(G, origen, destino,
weight=​'weight'​)

Camino de la ruta mínima → ​nx.bellman_ford_path(G, origen, destino,


weight=​'weight'​)
Al igual que en el algoritmo de Dijkstra, se realiza un código para obtener el largo de la ruta
mínima y otro para obtener el camino los cuales reciben como parámetros el grafo, el nodo de
origen, el nodo de destino y el parámetro weight también se define como un string para que
considere los pesos ya definidos en los arcos.
d) Algoritmo A*:
Largo de la ruta mínima → ​nx.astar_path_length(G, origen, destino,
weight=​'weight'​)

Camino de la ruta mínima → ​nx.astar_path(G1, origen, destino,


weight=​'weight'​)
Este algoritmo también recibe como parámetros el grafo, el nodo de origen, el nodo de destino y
dado que el weight es un string usa los pesos en los arcos definidos previamente.
Al encontrar el camino más corto, es difícil mostrar de manera útil cada nodo en el camino, debido a
la cantidad de nodos, pero usando la librería Folium, dado el camino más corto obtenidos entre las
rutas, se puede generar un mapa interactivo, en donde luego de obtener los nodos que pertenecen a
la ruta, se obtienen las coordenadas de tal nodo y se marca un círculo en el mapa interactivo. Un
pseudocódigo de esto se puede mostrar como:

Imagen 3. Pseudocódigo graficar ruta mínima.


En donde al obtener el camino, obtenemos la ruta que corresponde al segundo elemento del código
de network, en donde para cada nodo perteneciente a la ruta, se agrega un marcador en forma de
círculo con las coordenadas de ese nodo. Así se hace sucesivamente con toda la ruta para finalmente
guardar un mapa en formato HTML que permite ver la ruta en el mapa, así como los alrededores y
poder hacer zoom para entender mejor la ruta. La librería Folium permite diferentes tipos de mapa
en donde se muestran la opción C artoDB (imagen 4) y la opción OpenStreetM ap (imagen 5). En
color azul se muestra la ruta mínima según las distancias y en color rojo se muestran de acuerdos a
los tiempos. Esto se muestra de mejor manera a continuación:

Imagen 4. Ruta mínima entre el nodo 100608 y el nodo 179337 usando C artoDB

Imagen 5. Ruta mínima entre el nodo 100608 y el nodo 30445 usando OpenStreetM ap
Se aprecia que esta herramienta resulta ser sumamente útil ya que se puede ver que en ciertos
trayectos los caminos que siguen la ruta es la misma, pero al llegar a cierto punto estas toman
diferentes caminos, lo que da indicios de intersecciones en donde puede existir un mayor tráfico o
afluencia de automóviles, en donde a pesar de que la distancia mínima es por cierta ruta, existe una
mejor ruta en donde puede que se recorra más distancia pero el tiempo es menor. Esto es la base de
las aplicaciones de transporte hoy en día (Waze, Google Maps, HERE we go, etc) en donde más que
indicar la ruta mínima en distancia, te guía por la ruta que de un menor tiempo de viaje al usuario,
por lo que esta herramienta puede ser muy útil para visualizar estas diferentes rutas y encontrar
puntos críticos.También resulta una herramienta útil graficar 10 rutas de distintos origen y destino
como se ve a continuación:

Imagen 6. Rutas mínima en distancia 10 rutas diferentes origen-destino

Se puede ver que dada las diferentes rutas existentes se pueden detectar diferentes ruta rápidas
como lo son a la salida de Manhattan para llegar a zonas más periféricas son la ruta principal de
varias rutas mínimas, esto se debe a que la isla de Manhattan posee varios túneles expeditos, en
donde cualquier pana de bencina que genere tráfico está penalizado con puntos en la licencia del
conductor, esto para asegurar una ruta expedita para salir de la isla así como rutas que pasan lejos de
la isla también tienen rutas rápidas que se alejan del centro, especialmente autopistas que poseen
más de 3 pistas por cada sentido logrando rutas en distancia pequeñas y expeditas. En el Anexo 4 se
pueden ver las diferentes rutas mínimas desde el primer nodo de origen hacia todos sus destinos.
En el Anexo 1 se encuentran detallados los tiempos mínimos, las distancias mínimas y los tiempos de
ejecución de cada algoritmo, para cada par origen-destino requerido. La ruta seguida para llegar de
cada origen a cada destino, se encuentra detallada en el código computacional, a medida que itera el
algoritmo, ya que las rutas son formadas por un gran número de nodos para los 100 pares origen
destino.

En la Tabla 1 se muestra los promedios de uso de memoria que tuvieron los algoritmos previamente
descritos. Para analizar el uso de memoria se tomaron fotos al administrador de tarea mientras
corría cada algoritmo como se puede ver en el Anexo 3 a modo de ejemplo. Para cada algoritmo se
tomaron fotos en 14 instancias, de las cuales se obtuvieron los resultados expuestos en el Anexo 2.
Los promedios están expuestos en la Tabla 1 donde destaca que el de menor uso de memoria fue el
algoritmo Dijkstra Bidireccional con 1,189 GB. El de mayor uso de memoria es el algoritmo Dijkstra
con 1,391 GB promedio durante las iteraciones.

Algoritmo Uso de memoria

Dijkstra 1,391 GB

Dijkstra- Bidireccional 1,189 GB

Bellman-Ford 1,156 GB

A* 1,136 GB
Tabla 1 : Uso de memoria algoritmos parte 1

En la Tabla 2 se exponen los tiempos promedios de ejecución de cada algoritmo tanto para el caso de
distancia como para tiempo. El algoritmo con mayor tiempo de ejecución es Bellman-Ford con 93,16
segundos para el tiempo y 174,071 segundos para la distancia. El algoritmo de menor tiempo
promedio es el Dijkstra Bidireccional para ambos casos de rutas mínimas con 1,617 segundos y 1,988
segundos (tiempo y distancia respectivamente). Al analizar el uso de memoria y el tiempo de
ejecución se tiene para esta parte 1 que el algoritmo más apropiado es el Dijkstra Bidireccional, ya
que cuenta con el menor uso de memoria y el menor tiempo de ejecución, ya que este algoritmo solo
busca la ruta mínima entre los dos nodos, comenzando desde el origen y retrocediendo desde el
destino hasta que se encuentran en las iteraciones. En cambio el algoritmo Dijkstra comienza desde
el origen y busca la ruta mínima desde el origen al resto de los nodos hasta llegar al destino. Es por
esto que demora más que el bidireccional, pero aun así destaca su uso de memoria.

Algoritmo Tiempo Distancia

Bellman-Ford 93,16 seg 174,071 seg

Dijkstra 3,742 seg 3,698 seg

Dijkstra Bidireccional 1,617 seg 1,988 seg

A* 2,645 seg 2,362 seg


​Tabla 2: Tiempos promedios de ejecución parte 1
Parte 2

En esta parte, se requiere crear una variación del problema de ruta mínima, esta vez sin utilizar la
librería Networkx. Para poder procesar el algoritmo de creación propia, al ser revisados los
algoritmos de la parte 1 se puede descartar el algoritmo de Bellman-Ford, dado que su tiempo de
ejecución que resulta ser muy alto. Por lo que para esta parte se elige un algoritmo Dijkstra, pero a
diferencia del Dijkstra tradicional, se implementa un Heap Binario.

Un Heap se implementa para poseer una estructura de datos que mantenga un ordenamiento parcial
de un vector N de números. En donde el Heap corresponde a un árbol binario dirigido en donde se
cumple que el nodo padre siempre es menor que sus nodos hijos. Al implementar el Heap Binario se
busca el menor elemento en Orden(1), esto ya que el menor corresponde al nodo inicial del árbol
binario.

Los pasos a seguir en el Heap Binario corresponde a:


i. Insertar valor en el nuevo nodo y actualizar el árbol para mantener el Heap → O(log(|N |))
ii. Eliminar nodo y se actualiza el árbol para mantener el Heap → O(log(|N |))
iii. Reducir valor del nodo y se actualiza el árbol para mantener el Heap → O(log(|N |))

Se codifica el mismo algoritmo para poder obtener una variación del algoritmo Dijkstra pero con el
uso de un Heap Binario que usa el siguiente pseudocódigo:

→ defaultdict(list) ​#Se crea el grafo en forma de diccionario de la forma {nodo:


(nodo_destino,peso)}
→ Se crea lista q que va mutando y posee (costo,nodo actual, path)
→ while q: ​ #Mientras existan elementos en q
→ if v1 not in seen: ​#Si el nodo actual no está en los visitados
→ seen.add(v1) ​#Se agrega a los visitados
→ path = (v1,path) #Se actualiza el path
→ if v1 == t: #Si el nodo actual es igual al destino significa que se llego ya
→ return (cost,path) #Y se retorna el costo y el camino
→ for c,v2 in g.get(v1,()): ​#Por cada nodo destino y costo en el grafo asociados con v1
→ if v2 in seen: ​#Si el nodo de destino está en los visitados se continua
→ continue
→ prev = mins.get(v2,None)
→ next = cost + c ​#Ir al siguiente nodo cuesta el costo actual + costo de ir
→ if prev is None o next < prev:
→ mins[v2] = next ​ #Se agrega al diccionario de mínimos el nodo actual
→ heappush(q,(netx,v2,path) ​#Se agrega a q (costo, nodo destino, path)
→ return float(“inf”)

En el código se implementa el algoritmo con la función ​my_main la cual recibe como parámetros las
distancias, los tiempos, coordenadas, orígenes y destino. El nuevo algoritmo se utiliza para calcular
todas las rutas mínimas entre cada origen y cada destino, obteniendo como resultado el tiempo de
ejecución, la distancia o tiempo mínimo de la ruta y la ruta que sigue de acuerdo a los nodos.

Los resultados del algoritmo Heap Binario implementado, se encuentran en el Anexo 1, en las dos
últimas columnas para cada par origen destino. Este algoritmo tuvo un tiempo de ejecución
promedio de 1,465 segundos para ruta mínima de tiempo y tuvo 1,38 segundos promedio de
ejecución para la ruta mínima de distancia. Al comparar estos valores con los obtenidos en la parte 1
(ver Tabla 2), se puede ver que el algoritmo Heap binario tuvo menor tiempo de ejecución que los
cuatro algoritmos implementados en la parte 1, lo cual era esperado debido al ordenamiento que va
realizando el Heap. En cuanto al uso de memoria, la mejora al implementar este algoritmo es aún
más alta, ya que al revisar el uso de memoria en el administrador de tareas mientras se realizaban las
iteraciones, se obtuvo que el uso de memoria permanece entre 339,2 MB y 339,3 MB lo cual al
compararlo con el uso de memoria obtenido en la parte 1 (ver Tabla 1), corresponde casi a la tercera
parte de los algoritmos usados de la librería Networkx. Esto indica que el nuevo algoritmo además de
poseer un menor tiempo de ejecución, ocupa menos memoria y menos recursos del procesador, lo
que se considera muy importante ya que en la mayoría de los casos de ruta, los problemas son muy
gigantes por lo que si se desea hacer un cálculo de una base de datos más grande se recomendaría
utilizar el nuevo algoritmo para que el procesador pueda obtener una solución sin extremar recursos.

Parte 3

1. En este apartado se pide la ruta mínima entre los mismos orígenes y destinos de la parte 1 y 2.
Esta vez se le agrega una restricción, que es que no se puede doblar a la izquierda. Por ejemplo al
revisar la Figura 1, cada punto pi corresponde a una intersección y los arcos corresponden a las
calles. Si empieza en el punto p1 con coordenadas (x1,y1) y se va al punto p3 con coordenadas
(x3,y3), virando en el p2 con coordenadas (x2,y2). Primero se calcula el (x3-x1,y3-y1) y luego se
calcula (x2-x1,y2-y1). Luego se calcula el producto punto entre ambos (x3-x1,y3-y1) x (x2-x1,y2-y1) =
(x3-x1)*(y2-y1) - (y3-y1)*(x2-x1). La operación anterior será definida como Cálculo(p1,p2,p3). Cabe
destacar que los 3 puntos podrían ser una línea en la misma dirección (no necesariamente recta),
también estos 3 puntos podría ser tanto un viraje a la izquierda o a la derecha (no necesariamente en
90 grados). Para determinar si dobló a la izquierda, el resultado de Cálculo(p1,p2,p3) debe ser menor
a cero. Por lo tanto, para que los puntos agregados puedan ser parte de la ruta cumpliendo la
restricción planteada, se debe tener que el resultado de Cálculo(p1,p2,p3) sea mayor o igual a cero,
que equivale a seguir derecho o doblar a la derecha. Esto se debe repetir con cada grupo de 3 puntos
hasta llegar al destino. Luego se de tener Cálculo(p2,p3,p4), luego Cálculo(p3,p4,p5) y así,
cumpliendo con lo anteriormente descrito.

F​ igura 1: Giro a la izquierda


2. En este apartado se agrega al problema de ruta mínima la restricción para que no “culebree”, es
decir, que no gire a la izquierda y luego a la derecha en la siguiente intersección, como se ve en la
Figura 2. Para calcular si la ruta “culebrea”, se debe recurrir a la fórmula Cálculo definida en el
apartado anterior. Esto se obtiene si Cálculo(p1,p2,p3) es menor a cero y Cálculo(p2,p3,p4) mayor a
cero. Estos resultados significan que primero dobló a la izquierda y en la intersección siguiente
inmediatamente a la derecha. Por lo que para agregar dos movimientos seguidos el resultado de la
función debe ser distinto a los mencionados anteriormente. De esta forma, no “culebrea”. Luego se
debe obtener Calculo(p2,p3,p4) y Cálculo(p3,p4,p5), etc hasta llegar al destino.

​ Figura 2: Culebreo
3. En este apartado, se agrega la restricción de un límite de cruzar intersecciones al problema de ruta
mínima entre los orígenes y destinos estudiados en los apartados previos. Para esta parte se hace un
contador de intersecciones. Este contador se restringe para los 3 casos: 500, 650 y 850
intersecciones. Si la ruta excede el contador en el respectivo caso, la ruta se descarta.

Conclusión

A lo largo de este trabajo se estudiaron distintos algoritmos para resolver el problema de ruta
mínima, que fueron aprendidos en el curso. En la primera parte se analizaron los algoritmos Dijkstra,
Dijkstra Bidireccional, Bellman-Ford, A*. De esta parte se obtuvo que el de menor tiempo de
ejecución fue el Dijkstra Bidireccional. Respecto al uso de memoria el algoritmo Bidireccional
también fue el que obtuvo el menor resultado. Por lo que se concluye que de los 4 algoritmos
expuestos de la librería Networkx, el bidireccional es el más favorable para su uso. Luego se realizó la
variación agregando el Heap Binario, sin el uso de la librería Networkx, lo cual significó una mejora
tanto en el tiempo de ejecución como en el uso de memoria. Por lo que se concluye que de los 5
algoritmos, el algoritmo más favorable es el que tiene Heap Binario.

En la actualidad, debido a la situación que se vive debido al Coronavirus, la demanda de servicios de


delivery ha sido más alta que antes, ya que la gente no puede salir de la casa y reciben en el domicilio
todo tipo de servicios y productos. Es por esto que en el contexto actual se torna sumamente
importante el problema de ruta mínima, ya que las empresas deben suplir de la mejor manera
posible la sobredemanda, con los menores tiempos y distancias posible. Además gracias a la
implementación de la herramiento para visualizar en el mapa las rutas mínimas se pueden tomar
decisiones comparando la ruta mínima en distancias y en tiempos, en donde empresas de flota de
transportes pueden ver ambas rutas y según las necesidades específicas elegir la más conveniente.

Las restricciones planteadas en la parte 3 demoran más y recorren distancia mayor que el problema
de ruta mínima normal. Debido a esto pareciera que no se aplicarían en la vida real. Sin embargo,
esto no es cierto, ya que si bien se estudió un caso de una ciudad real con datos reales, no se
consideran las interacciones de tráfico en este trabajo. Es por esto que este tipo de restricciones
como el viraje si se aplican en el mundo real cuando se analiza el ruteo de vehículos, por ejemplo la
empresa repartidora UPS en Estados Unidos no gira a la izquierda en sus recorridos desde el año
2003. Los motivos son principalmente razones de seguridad, especialmente en calzadas de doble
sentido. Sin embargo, eso se tradujo en ganancias de tiempo y dinero, ya que se ahorraba
combustible, ya que al virar a la izquierda debe esperar dependiendo del tráfico que hay en sentido
contrario. La empresa UPS tiene un algoritmo que busca realizar mayoritariamente solo giros a la
derecha y con esto ha logrado ahorrar hasta 47,5 millones de euros anuales, lo cual demuestra la
importancia del estudio de rutas mínimas y sus variaciones.

Como conclusión, en este trabajo se estudiaron distintos algoritmos de rutas mínima de manera
teórica y su aplicación computacional. También se aprendió la importancia que tienen al ser
implementados en la vida real y complementados con el ruteo de vehículo, ya que tiene implicancias
de seguridad y de costo tanto económico como de tiempo. Lo cual demuestra la utilidad del curso y
el contenido aprendido y desarrollado en este informe.

Anexos
Anexo 1: Rutas mínimas y tiempos de ejecución algoritmos parte 1 y 2

Anexo 2: Uso de memoria parte 1

Anexo 3: Foto ejemplo uso de memoria


Ruta Mínima 100608-125487 Ruta mínima 100608-171839

Ruta Mínima 100608-172143 Ruta Mínima 100608-225531

Ruta Mínima 100608-4354 Ruta Mínima 100608-80302

Ruta Mínima 100608-85081 Ruta Mínima 100608-97368


ANEXO 4. Rutas mínimas graficadas para el nodo origen 100608 a todos los destinos.

También podría gustarte