Introduction

There are two outstanding algorithms for computing the shortest paths from a source node in a graph. These two algorithms are the Dijkstra’s algorihtm and the Bellman-Ford algorithm. The two algorithms converges to an identical sink tree rooted at the source node.

Dijkstra’s Algorithm

Below is a Python implementation of the Dijkstra’s algorithm. This implementation requires that the graph is connected otherwise the algorithm will be in an infinite loop.

#
# dijkstra.py
#
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt

def dijkstra(graph, srcid):
    node_list = []            # list of nodes to work on
    dist_to_src = dict()      # distance to source on shortest paths
    prec_node_to_dst = dict() # preceding node to destination on shortest paths
    for node in graph.nodes:
        dist_to_src[node] = float('inf')
        prec_node_to_dst[node] = None
        node_list.append(node)
    dist_to_src[list(graph.nodes)[srcid]] = 0

    while node_list:
        (curr_node,path_cost) = \
            min([(node,dist_to_src[node]) \
                for node in node_list], key = lambda t: t[1])
        print('(current node,path cost to source) = ', (curr_node,path_cost))
        node_list.remove(curr_node)
        print('node_list = ', node_list)
        
        for neighbor in graph.neighbors(curr_node):
            link_cost = graph.get_edge_data(curr_node, neighbor)[0]['cost']
            alt_cost = dist_to_src[curr_node] + link_cost
            if alt_cost < dist_to_src[neighbor]: 
                # path to the neighbor with shorter dist_to_srcance found
                dist_to_src[neighbor] = alt_cost 
                prec_node_to_dst[neighbor] = curr_node
                print('dist_to_src = ', dist_to_src)
                print('prec_node_to_dst = ', prec_node_to_dst)
    return dist_to_src, prec_node_to_dst

def plot_graph(graph):
    plt.subplot(121)
    pos = nx.spring_layout(graph)
    nx.draw(graph, pos, with_labels=True)
    edge_labels=dict([((u,v,),d['cost']) for u,v,d in graph.edges(data=True)])
    nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels, \
            label_pos=0.3, font_size=7)

if __name__=="__main__":
    edgesDf = pd.read_csv('graph_edges.csv')
    nodesDf = pd.read_csv('graph_nodes.csv')
    
    graph = nx.MultiDiGraph()
    
    for i, node in nodesDf.iterrows():
        graph.add_node(node['id'])
    
    for i,edge in edgesDf.iterrows():
        graph.add_edge(edge['src'], edge['dst'], cost=edge['cost'])
    
    nodeName = 'v1'
    srcid = [i for i,name in enumerate(graph.nodes()) if name == nodeName]
    dist_to_src,prec_node_to_dst = dijkstra(graph, srcid[0])
    print('dist_to_src = ', dist_to_src)
    print('prec_node_to_dst = ', prec_node_to_dst)
    
    plot_graph(graph)

    # show graph. this is blocking
    plt.show()

The program takes a graph edge file and a graph node file, both of them are in the CSV format (i.e., the comma-separated-value format). Below is an example of a graph edge file called graph_edges.csv.

src,dst,cost
v1,v2,2
v2,v1,3
v1,v4,1
v4,v1,7
v2,v3,3
v3,v2,6
v4,v2,2
v2,v4,2
v4,v3,3
v3,v4,3
v4,v5,1
v5,v4,1
v3,v6,5
v6,v3,8
v3,v5,1
v5,v3,1
v5,v6,2
v6,v5,4

The following is an example of a graph node file called graph_nodes.csv.

id,x,y
v1,0,0
v2,10,10
v3,20,10
v4,10,-10
v5,20,-10
v6,0,30

Notice that columns x and y are unused in the program above and can be safely removed. The intention of these two columns is to control the layout of the graph when we plot it.

Exercise and Exploration

The dijkstra.py program finds the shortest paths from a source node to all nodes in a graph. What are the challenges to apply the algorithm like this to solve the routing problem?

Programming

The dijkstra.py returns two list, one is the list of the costs of the shortest paths to all node in the graph from the source node (i.e., the dist_to_src in the program), the other the list of the preceding node to the desination node along a shortest path (i.e., the prec_node_to_dst. Following the Bellman’s optimality we can obtain the shortest path to all destination nodes from the source. Here are two exercises,

  1. Add a function to the dijkstra.py program to build the graph of the sink tree from the source node, e.g., the function with the following interface,
    build_shortest_paths_tree(graph, prec_node_to_dst)
    
  2. What minor modification can you apply to the dijkstra.py program so that the program also works for graphs that are not connected.

Bellman-Ford Algorithm

Below is a Python implementation of the Bellman-Ford algorithm.

import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt

def bellmanford(graph, srcid):
    node_dict = dict()        # list of nodes to work on
    dist_to_src = dict()      # distance to source on shortest paths
    prec_node_to_dst = dict() # preceding node to destination on shortest paths
    for node in graph.nodes:
        dist_to_src[node] = float('inf')
        prec_node_to_dst[node] = None
        node_dict[str(node)] = node
    dist_to_src[list(graph.nodes)[srcid]] = 0

    for node_key in node_dict:
        node_u = node_dict[node_key]
        edges_v = graph.out_edges(node_u, data=True)
        for edge in edges_v:
            edge_cost = edge[2]['cost']
            node_v = node_dict[str(edge[1])] 
            distance = dist_to_src[node_u] + edge_cost
            if distance < dist_to_src[node_v]:
                dist_to_src[node_v] = distance
                prec_node_to_dst[node_v] = node_u

    # logic to detect negative cycle ignored
    
    return dist_to_src, prec_node_to_dst

def plot_graph(graph):
    plt.subplot(121)
    pos = nx.spring_layout(graph)
    nx.draw(graph, pos, with_labels=True)
    edge_labels=dict([((u,v,),d['cost']) for u,v,d in graph.edges(data=True)])
    nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels, \
            label_pos=0.3, font_size=7)

if __name__=="__main__":
    edgesDf = pd.read_csv('graph_edges.csv')
    nodesDf = pd.read_csv('graph_nodes.csv')
    
    graph = nx.MultiDiGraph()
    
    for i, node in nodesDf.iterrows():
        graph.add_node(node['id'])
    
    for i,edge in edgesDf.iterrows():
        graph.add_edge(edge['src'], edge['dst'], cost=edge['cost'])
    
    nodeName = 'v1'
    srcid = [i for i,name in enumerate(graph.nodes()) if name == nodeName]
    dist_to_src,prec_node_to_dst = bellmanford(graph, srcid[0])
    print('dist_to_src = ', dist_to_src)
    print('prec_node_to_dst = ', prec_node_to_dst)
    
    plot_graph(graph)

    # show graph. this is blocking
    plt.show()

Execise and Exploration

The Bellman-Ford algorithms permits a graph to have negative weight. It can detect whether there is a negative cycle reachable from a source node. The logic is negelected in the above program.

  1. What is a negative cycle? Can you give a graph that contains a negative cycle?
  2. Can you add logic to the graph to detect the existence of a negative cycle?

Program Files and Sample Graph Files

For your convenience, you may download these files below (using your favorite Web browser or wget or some other command line tools),