I decided to try out some named tuples by implementing Dijkstra's algorithm to find the cheapest routes in a file like this (where each line represents node_a
is connected with node_b
with a cost of n
):
1 6 14 1 2 7 1 3 9 2 3 1 2 4 15 3 6 2 3 4 11 4 5 6 5 6 9
However, something that caught my attention is that some of the lines got really long:
import sys
from collections import namedtuple
INFINITY = 999999
class UndirectedGraph:
def __init__(self, node_list):
self.Node = namedtuple('Node', ['coming_from', 'cost'])
self.node_dict = self.get_nodes(node_list)
self.create_connections(node_list)
self.size = len(self.node_dict)
def get_nodes(self, node_list):
'''
Gets a list of tuples (node1, node2, weight_of_connection) and
distribute theses nodes through a dictionaire
'''
node_dict = {}
for line in node_list:
a_node, another_node = line[0], line[1]
node_dict[a_node] = []
node_dict[another_node] = []
return node_dict
def create_connections(self, raw_node_list):
'''
Creates the connection between the nodes in the nodes dict
'''
try:
for n in raw_node_list:
current_node, neighbor, cost = n
self.node_dict[current_node].append(self.get_new_node(neighbor, cost))
self.node_dict[neighbor].append(self.get_new_node(current_node, cost))
except:
print("General error: {}".format(sys.exc_info()[0]))
raise
def dijkstra(self, source):
'''
Applies the dijkstra algorithm for finding the shortest path to every
other node coming from source.
Returns a list containing the label of the early node and the cost of the
total path to the given node
'''
if source not in self.node_dict:
raise ValueError('Node informed does not exist')
# first slot of costs_array is a sentinel
costs_array = [self.get_new_node(INFINITY, INFINITY)] * (self.size + 1)
visited_nodes = set()
current_label = source
costs_array[source] = self.get_new_node(source, 0)
# transverse through all nodes
for i in range(self.size):
visited_nodes.add(current_label)
for neighbor in self.node_dict[current_label]:
if neighbor.coming_from not in visited_nodes and \
self.has_lower_cost(current_label, neighbor, costs_array):
costs_array[neighbor.coming_from] = self.get_minimum_cost(current_label, \
neighbor, costs_array)
current_label = self.get_cheapest_neighbor(current_label, costs_array, visited_nodes)
return costs_array[1:]
def get_cheapest_neighbor(self, current, costs_array, visited_nodes):
'''
Returns the index in costs_array for the cheapest available node
'''
lowest_value = lowest_index = INFINITY
for node in self.node_dict[current]:
if node.coming_from not in visited_nodes and \
node.cost < lowest_value:
lowest_value = node.cost
lowest_index = node.coming_from
return lowest_index
def has_lower_cost(self, current, neighbor, costs_array):
'''
Returns True if the new cost calculation is less than previous value
'''
if costs_array[neighbor.coming_from].cost is INFINITY:
return True
return (costs_array[current].cost + neighbor.cost) < costs_array[neighbor.coming_from].cost
def get_minimum_cost(self, current, neighbor, costs_array):
'''
Returns a new node with an appropriate new cost
'''
return self.get_new_node(current, costs_array[current].cost + neighbor.cost)
def get_new_node(self, node_label, node_cost):
'''
Creates and returns a Node named tuple
'''
return self.Node(coming_from=node_label, cost=node_cost)
def main():
# read nodes from file
with open('dij.txt', 'r') as f:
nodes_list = [[int(item) for item in line.split()] for line in f]
my_graph = UndirectedGraph(node_list=nodes_list)
start_node = 1
minimum_paths = my_graph.dijkstra(start_node)
print('Minimum path to every node starting from {} is:'.format(start_node))
for i, n in enumerate(minimum_paths, start=1):
print('{}: coming from {} with cost of {}'.format(i, n.coming_from, n.cost))
if __name__ == '__main__':
main()
I'm open to all suggestions on how to make this more pythonic, readable, efficient, or less 'cumbersome'.
The output with this code as the given example is:
Minimum path to every node starting from {} is: 1: coming from 1 with cost of 0 2: coming from 1 with cost of 7 3: coming from 2 with cost of 8 4: coming from 3 with cost of 19 5: coming from 6 with cost of 19 6: coming from 3 with cost of 10