LAB 10 Algoritmos sobre grafos
Contents
!wget --no-cache -O init.py -q https://raw.githubusercontent.com/rramosp/20192.L3/master/init.py
import init; init.init(force_download=False); init.get_weblink()
import init
from local.lib.rlxmoocapi import submit, session
import inspect
student = session.Session(init.endpoint).login( course_id=init.course_id, 
                                                session_id="UDEA", 
                                                lab_id="lab_10" )
LAB 10 Algoritmos sobre grafos¶
Ejercicio 1¶
Implementa al algoritmo de Dijsktra según la descripción más abajo. Las siguientes funciones te serán de ayuda para crear grafos aleatorios y visualizar grafos
import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
def create_random_graph(directed=False, w_size=200, n_cities=7, prob_connected=0.5):
    import itertools
    cities = (np.random.random((n_cities,2))*w_size).astype(int)
    g = nx.DiGraph() if directed else nx.Graph()
    
    for node_id, location in enumerate(cities):
        g.add_node(node_id, pos=location)
    for i,j in itertools.product(range(len(cities)), range(len(cities))):
        if i<j and np.random.random()<prob_connected:
            g.add_edge(i,j,weight=np.round(np.linalg.norm(cities[i]-cities[j]),2))    
            if directed and np.random.random()>.5:
                g.add_edge(j,i,weight=np.round(np.linalg.norm(cities[i]-cities[j]),2))    
                
    return g
def draw_graph(g,node_size=500, font_color="white", 
               show_edge_labels=True, edge_units=" km",
               x_units="km lon", y_units="km lat"):
    positions = {i: g.nodes[i]["pos"] for i in g.nodes} if "pos" in [k for k in g.nodes.values()][0].keys() else None
    nx.drawing.draw(g, with_labels=True, pos=positions,
                    node_alpha=.5, node_color="blue", width=2,
                    node_size=node_size, font_color=font_color)   
    if show_edge_labels and positions is not None:
        nx.draw_networkx_edge_labels(g, pos=positions,
                                     edge_labels={i:"%.1f%s"%(g.get_edge_data(*i)["weight"],edge_units) for i in g.edges});
        plt.axis("on")
        plt.xlabel(x_units)
        plt.ylabel(y_units)
        plt.grid()
    plt.axis("equal");
INPUT:
- nodes: una lista con los nombres de los nodos
- edges: un diccionario con:- key una tupla con los nodos que participan en un edge 
- value el peso (distancia) de ese edge 
 
- start: el nodo desde el que se quiere empezar
- target: el nodo al que se quiere llegar
observa que estamos con un grafo dirigido. Observa el ejemplo más abajo para ver cómo serían edges y nodes
VARIABLES:
puedes usar dos diccionarios para llevar la cuenta del progreso del algoritmo
- mincost: un diccionario con un key por cada nodo cuyo valor asociado va a ser el coste (distancia) mínimo que vamos calculando desde- starthasta el el nodo key
- confirmed: un diccionario con un key por cada nodo cuyo valor asociado será- Trueo- Falseque indique de qué nodos ya sabemos el coste mínimo de llegar desde- start
por ejemplo
- valor de - confirmed:- {0: False, 1: True, 2: True, 3: True, 4: False, 5: False}
- valor de - mincost:- {0: inf, 1: 0, 2: 146.24, 3: 168.91000000000003, 4: inf, 5: 255.36}
indica que:
- ya sabemos que el coste menor de llegar al nodo 3 desde - startes de 168.91
- ya sabemos que el coste menor de llegar al nodo 2 desde - startes de 146.24
- el nodo - startes el nodo 1, ya que el costo de llegar a él es de 0
- todavía no sabemos cómo llegar a los nodos 0 y 4 (o si esto es posible) ya que su valor es - inf
- existe un camino de llegada al nodo 5 con coste 255.36 pero todavía no sabemos si habrá otro camino menos costoso. 
PSEUDOCÓDIGO:
- inicializar el diccionario mincost con el coste del edge ( - start, nodo) para cualquier nodo directamente conectado con- starty con- infpara cualquier otro nodo
- inicializar el diccionario confirmed con todos los nodos a - Falseexcepto el nodo- startque se establece a- True
- repetir tantas veces como número de nodos menos 1: - escoger nodo - wtal que- mincost[w]sea el menor de los elementos de- mincostcuyo- confirmed[w]sea Falso
- confirmed[w]\(\leftarrow\)- True
- para todo nodo - kcon- confirmed[k]=False:- mincost[k]:= min(mincost[k], mincost[k] + edges[(w,k)]) 
 
donde tienes que tener en cuenta que edges[(w,k)] no estará definido para los casos en los que no hay un edge entre w y k (que es equivalente a que su coste fuera infinito)
OUTPUT:
el coste \(\in \mathbb{R}\) más pequeño de viajar desde start hasta target, este coste se encuentra en mincost[target]
Fíjate cómo verías en edges y nodes el siguiente grafo.
g = create_random_graph(n_cities=6, prob_connected=.7, directed=True)
g = nx.DiGraph()
g.add_node("S", pos=(0,5))
g.add_node("A", pos=(10,10))
g.add_node("B", pos=(10,0))
g.add_node("C", pos=(20,15))
g.add_node("D", pos=(20,5))
g.add_edge("S","A", weight=5)
g.add_edge("S","B", weight=10)
g.add_edge("A","B", weight=3)
g.add_edge("A","C", weight=7)
g.add_edge("A","D", weight=12)
g.add_edge("B","D", weight=1)
plt.figure(figsize=(3,3))
draw_graph(g, edge_units="")
plt.axis("off")
nodes = [i for i in g.nodes.keys()]
edges = {k:v["weight"] for k,v in g.edges.items()}
print("\n**** ESTA SERIA LA ENTRADA A TU FUNCION ****\n")
print("nodes", nodes)
print("edges", edges)
def dijkstra_shortest_path(nodes, edges, start, target):
    import numpy as np
    # inicializacion
    mincost   = #... TU CODIGO AQUI ...    
    confirmed = #... TU CODIGO AQUI ...
    
    for _ in range(len(nodes)-1):
        
        # .. TU CODIGO AQUI ...
        # PASOS 3.A y 3.B del pseudocodigo
        
        for k in nodes:
            
            # ... TU CODIGO AQUI ...
            # PASO 3.C del pseudocodigo
            
    return mincost[target]
comprueba tu código. Observa cómo creamos un grafo aleatorio y seleccionamos también aleatoriamente nodos a los que medirle la distancia más corta. Comparando esa métrica con lo devuelto por networx, debería de dar el mismo valor. Incluyendo los infititos.
g = create_random_graph(n_cities=10, prob_connected=.4, directed=True)
draw_graph(g, edge_units="")
plt.axis("off")
print ("type of g",type(g))
print ("Content of g: ", g)
print ("type of g.nodes", type(g.nodes))
print ("g.nodes= ", g.nodes)
print (g.nodes.keys())
print ("correct  networkx  tu_implementacion")
for _ in range(10):
    source, target = 5, 2
    source, target = np.random.permutation(g.nodes)[:2]
    try:
        nd = nx.dijkstra_path_length(g, source, target)
    except nx.NetworkXNoPath:
        nd = np.inf
    nodes = [i for i in g.nodes.keys()]
    edges = {k:v["weight"] for k,v in g.edges.items()}
    md = dijkstra_shortest_path(nodes, edges, source, target)
    print (nd==md, "%12.2f"%nd,"%12.2f"%md)
registra tu solución en línea¶
student.submit_task(globals(), task_id="task_01");
Ejercicio 2¶
Implementa el algoritmo de Dijkstra que además de calcular la longitud del camino más corto, obtenga los nodos por los que ese camino pasa.
ESTRATEGIA SUGERIDA
Observa que cada vez que modificamos mincost[k] en el paso 3.C del pseudocódigo anterior, es que hemos encontrado un camino más corto hasta el nodo k desde mincost[w] (respecto a lo que conocemos en este punto del algoritmo). Es decir, que el camino más corto para llegar a k, pasa por w justo antes de llegar a k.
Por tanto, si guardamos el valor de w cada vez que actualizamos mincost tendremos, para cada nodo, cual es el nodo inmediatamente anterior en el camino más corto.
Mantendremos entonces una variable wayto que contenga un diccionario con un key por cada nodo cuyo valor asociado va a ser el nodo por el que se pasó inmediatamente antes de llegar al nodo key  por el camino más corto.
En el paso 1 del pseudocódigo inicializamos wayto de modo que wayto[i]=start si hay un camino directo de start a i. En otro caso, establecemos wayto[i]=None
En el paso 3.C, cuando actualizemos mincost[k] para algún nodo k, actualizamos también wayto[k]=w, en cualquier otro caso, no tocamos wayto.
Con esto, tendremos al final del proceso por ejemplo un wayto con el siguiente contenido, con start=6 y target=5:
{0: 9, 1: 7, 2: 7, 3: 0, 4: 7, 5: 4, 6: None, 7: 6, 8: 3, 9: 6}
esto indica que:
- el camino más corto para llegar al 5 pasa inmediatamente antes por el 4 
- el camino más corto para llegar al 4 pasa inmediatamente antes por el 7 
- el camino más corto para llegar al 7 pasa inmediatemente antes por el 6, que es nuestro - start
por tanto, el camino más corto para llegar desde el 6 al 5 es [6,7,4,5]
Como último paso tendrás que reconstruir el path desde wayto antes de acabar tu algoritmo y adjuntarlo al valor de retorno.
def dijkstra_shortest_path(nodes, edges, start, target):
    
    import numpy as np
    # inicializacion
    mincost   = #... TU CODIGO AQUI ...    
    confirmed = #... TU CODIGO AQUI ...
    wayto     = #... TU CODIGO AQUI ...
    
    for _ in range(len(nodes)-1):
        
        # .. TU CODIGO AQUI ...
        # PASOS 3.A y 3.B del pseudocodigo
        
        for k in nodes:
            
            # ... TU CODIGO AQUI ...
            # PASO 3.C del pseudocodigo .. acuerdate de actualizar wayto
    path = # ... RECONSTRUYE EL PATH DESDE wayto ...
    return mincost[target], path
prueba tu código. observa el grafo generado y prueba con distintos start y target
#g = create_random_graph(n_cities=10, prob_connected=.4, directed=True)
nodes = [i for i in g.nodes.keys()]
edges = {k:v["weight"] for k,v in g.edges.items()}
draw_graph(g, edge_units="")
source, target = 6, 5
#source, target = 4,3
print ("STUDENT", dijkstra_shortest_path(nodes, edges, source, target))
print ("NETWORKX", (nx.dijkstra_path_length(g, source, target), nx.dijkstra_path(g,source,target)))
prueba tu código de manera más exhausitva. Obseva cómo generamos grafos y start, target aleatorios y se compara tu resultado con el de networkx. Tu columna correct ha de estar siempre a True. Si no, observa el caso en el que no sea así y revisa tu implementación.
g = create_random_graph(n_cities=15, prob_connected=.2, directed=True)
plt.figure(figsize=(7,7))
draw_graph(g, edge_units="", show_edge_labels=False)
plt.axis("off")
print("src tgt correct      networkx                        tu_implementacion")
for _ in range(20):
    source, target = np.random.permutation(g.nodes)[:2]
    try:
        ndist = nx.dijkstra_path_length(g, source, target)
        npath = nx.dijkstra_path(g, source, target)
    except nx.NetworkXNoPath:
        npath = []
        ndist = np.inf
    
    nodes = [i for i in g.nodes.keys()]
    edges = {k:v["weight"] for k,v in list(g.edges.items())}
    mdist,mpath = dijkstra_shortest_path(nodes, edges, source, target)
    ok = (mdist==ndist) and (len(npath)==len(mpath)) and np.allclose(mpath, npath)
    print("%3d"%source, "%3d"%target, ok, "%12.2f %20s"%(ndist,npath),"%12.2f %20s"%(mdist, mpath))
registra tu solución en linea¶
student.submit_task(globals(), task_id="task_02");
Ejercicio 3¶
Realiza la implementación del algoritmo de Floyd-Warshal que nos devuelve una matriz con el camino más corto entre todos los nodos. Si \(n\) es el número de nodos del grafo, tu solución ha de devolver un pandas.DataFrame  de \(n\times n\) en el que las columnas y los índices son los nodos.
Lee la descripción del algoritmo en Wikipedia para adquirir una intuición de cómo se implementa.
SUGERENCIA DE IMPLEMENTACIÓN¶
- usa - collections.defaultdictpara crear un diccionario llamado- mincostcon el mismo contenido de- edgespero con un valor por defecto- np.infpara cualquier par de nodos que que inicialmente no esté en edges.
- implementa el siguiente pseudocódigo: - mincost es un defaultdict inicializado vacío para cada edge entre nodos i,j: mincost[(i,j)] = edges[(i,j)] para cada nodo k: para cada nodo i: para cada nodo j: costo_ijk = mincost[(i,k)]+mincost[(k,j)] if costo_ijk < mincost[(i,j)]: mincost[(i,j)] = cost_ijk establece el valor 0 para mincost[(t,t)] para cada nodo t construye una matriz de n x n con los contenidos de mincost. construye un dataframe de pandas según indicado.
def floyd_warshall(nodes, edges):
    
    import numpy as np
    import pandas as pd
    from collections import defaultdict
    
    n_edges = len(edges)
    mincost = defaultdict(lambda: np.inf)
    # 1. construye el mincost inicial
    # 2. realiza el triple bucle para actualizar el mincost de cada nodo
    # 3. establece mincost[(t,t)]=0 para todos los nodos
    # 4. constriuye un pandas dataframe segun indicado
    result = pd.DataFrame ( # ... TU CODIGO AQUI .... )
    return rr
prueba tu código con algún grafo sencillo
g = create_random_graph(n_cities=10, prob_connected=.4, directed=True)
nodes = [i for i in g.nodes.keys()]
edges = {k:v["weight"] for k,v in g.edges.items()}
draw_graph(g, edge_units="")
nodes = [i for i in g.nodes.keys()]
edges = {k:v["weight"] for k,v in g.edges.items()}
stfw = floyd_warshall(nodes, edges)
nxfw = np.array(nx.floyd_warshall_numpy(g))
print("TU SOLUCION")
print(stfw.values)
print("\nNETWORKX")
print(nxfw)
print("\nCOMPARATIVA:", np.allclose(stfw.values, nxfw))
prueba tu código de manera más exhaustiva
for _ in range(20):
    g = create_random_graph(n_cities=np.random.randint(20)+5, prob_connected=.3, directed=True)
    
    nxfw = np.array(nx.floyd_warshall_numpy(g))
    
    nodes = [i for i in g.nodes.keys()]
    edges = {k:v["weight"] for k,v in g.edges.items()}
    stfw = floyd_warshall(nodes, edges)
    
    ok = np.allclose(nxfw, stfw.values)
    if not ok:
        print("*** INCORRECT RESULT ON GRAPH ***")
        print("graph")
        print(nodes)
        print(edges)
        print("TU SOLUCION")
        print(stfw)
        print("\nNETWORKX")
        print(nxfw)
        print("\nCOMPARATIVA:", np.allclose(stfw, nxfw))
    else:
        print("ok", end=' ')
submit your answer (you must be connected to internet)¶
student.submit_task(globals(), task_id="task_03");