AI Lab5
AI Lab5
Graphs:
Consider a simple (directed) graph (digraph) having six nodes (A-F) and the following arcs
(directed edges):
A -> B
A -> C
B -> C
B -> D
C -> D
C -> F
D -> C
D -> E
E -> F
F -> C
F -> E
This is a dictionary whose keys are the nodes of the graph. For each key, the corresponding
value is a list containing the nodes that are connected by a direct arc from this node. This is
about as simple as it gets (even simpler, the nodes could be represented by numbers instead
of names, but names are more convenient and can easily be made to carry more information,
such as city names).
Let's write a simple function to determine a path between two nodes. It takes a graph and the
start and end nodes as arguments. It will return a list of nodes (including the start and end
nodes) comprising the path. When no path can be found, it returns None. The same node will
not occur more than once on the path returned (i.e. it won't contain cycles). The algorithm
uses an important technique called backtracking: it tries each possibility in turn until it finds a
solution.
Example 2:
class Graph:
def __init__(self, nodes=None, edges=None):
"""Initialize a graph object.
Args:
nodes: Iterator of nodes. Each node is an object.
edges: Iterator of edges. Each edge is a tuple of 2 nodes.
"""
self.nodes, self.adj = [], {}
if nodes != None:
self.add_nodes_from(nodes)
if edges != None:
self.add_edges_from(edges)
def length(self):
"""Returns the number of nodes in the graph.
>>> g = Graph(nodes=[x for x in range(7)])
>>> len(g)
7
"""
return len(self.nodes)
def traverse(self):
return 'V: %s\nE: %s' % (self.nodes, self.adj)
def number_of_nodes(self):
return len(self.nodes)
def number_of_edges(self):
return sum(len(l) for _, l in self.adj.items()) // 2
class DGraph(Graph):
def add_edge(self, u, v):
self.adj[u] = self.adj.get(u, []) + [v]
class WGraph(Graph):
def __init__(self, nodes=None, edges=None):
"""Initialize a graph object.
Args:
nodes: Iterator of nodes. Each node is an object.
edges: Iterator of edges. Each edge is a tuple of 2 nodes and
a weight.
"""
self.nodes, self.adj, self.weight = [], {}, {}
if nodes != None:
self.add_nodes_from(nodes)
if edges != None:
self.add_edges_from(edges)
class DWGraph(WGraph):
def add_edge(self, u, v, w):
self.adj[u] = self.adj.get(u, []) + [v]
self.weight[(u,v)] = w
Lab Task:
1. Change the function find path to return shortest path.
def find_shortest_path(graph, start, end, path=None):
if path is None:
path = [start]
if start == end:
return path
return shortest_path
# Example graph
graph = {
"A": ["B", "C"],
"B": ["C", "D"],
"C": ["D", "F"],
"D": ["C", "E"],
"E": ["F"],
"F": ["C", "E"],
}
2.
C
onsider a simple (directed) graph (digraph) having six nodes (A-F) and the following arcs
(directed edges) with respective cost of edge given in parentheses:
A -> B (2)
A -> C (1)
B -> C (2)
B -> D (5)
C -> D (1)
C -> F (3)
D -> C (1)
D -> E (4)
E -> F (3)
F -> C (1)
F -> E (2)
Using the code for a directed weighted graph in Example 2, instantiate an object of DWGraph
in __main__, add the nodes and edges of the graph using the relevant functions, and
implement a function find_path() that takes starting and ending nodes as arguments and
returns at least one path (if one exists) between those two nodes. The function should also
keep track of the cost of the path and return the total cost as well as the path. Print the path
and its cost in __main__.
# Directed Weighted Graph class
class DWGraph:
def __init__(self):
self.nodes = [] # List of nodes
self.adj = {} # Adjacency list for storing neighbors
self.weight = {} # Weight for storing edges' weights
while stack:
node, path, cost = stack.pop()
if node == end:
return (cost, path)
# Test example
if __name__ == "__main__":
graph = DWGraph()
# Adding nodes
for node in ["A", "B", "C", "D", "E", "F"]:
graph.add_node(node)
for u, v, w in edges:
graph.add_edge(u, v, w)
if result:
cost, path = result
print(f"Path from {start_node} to {end_node}: {path} with total
cost {cost}")
else:
print(f"No path found from {start_node} to {end_node}")