Module 2, Practical 9¶
In this practical we will keep working with graphs.
Graphs recap¶
Graphs are mathematical structures made of two key elements: nodes (or vertices) and edges. Nodes are things that we want to represent and edges are relationships among the objects. Mathematically, a graph is an entity \(G = (N,E)\) where \(N\) is a set of nodes and \(E = N \times N\) is the set of edges.
Nodes are normally represented as circles, while edges (relationships) are represented by lines/arrows connecting nodes. An example follows:
Relations represented by edges can be transitive (e.g. sibling_of: if \(X\) is sibling of \(Y\) then \(Y\) is sibling of \(X\)) and in this case the edges are just lines rather than arrows. In this case the graph is directed. In case relationships are not transitive (i.e. \(X \rightarrow Y\) does not imply \(Y \rightarrow X\)) we put an arrow to indicate the direction of the relationship among the nodes and in this case we say the graph is undirected.
Some terminology (from the lecture):
The degree of a node is the number of connection it has with other nodes. In directed graphs the in-degree is the number of incoming edges, while the out-degree is the number of outgoing edges.
A path in the graph is a sequence of nodes connected by edges.
Up to now, we have seen two different strategies for implementing graphs: adjacency matrices and linked lists. In the remainder, we will work with the DiGraphLL and DiGraphLLAnalyzer class implemented in the previous practical.
Visits¶
Visiting graphs means traversing through its edges and nodes following the connections that make up the graph. Graphs can have cycles and this makes it quite tricky to visit the graph. Differently from what seen in the case of trees, we need to keep a data structure of visited nodes to avoid getting stuck in loops like the following (or you can pretty much end up in an infinite while loop):
Traversing a graph \(G = (V,E)\) in mathematical terms can be described as, given a node \(r\), visit all the nodes reachable from \(r\) going through the nodes exactly once.
As in the case of Trees, two ways exist to perform a visit of a graph: depth-first and breadth-first search.
As said above, the base class that we will extend is DiGraphLL
, whose code is reported below as a reminder:
[1]:
class DiGraphLL:
def __init__(self):
"""Every node is an element in the dictionary.
The key is the node id and the value is a dictionary
with second node as key and the weight as value
"""
self.__nodes = dict()
def insertNode(self, node):
test = self.__nodes.get(node, None)
if test == None:
self.__nodes[node] = {}
#print("Node {} added".format(node))
def insertEdge(self, node1, node2, weight):
test = self.__nodes.get(node1, None)
test1 = self.__nodes.get(node2, None)
if test != None and test1 != None:
#if both nodes exist othewise don't do anything
test = self.__nodes[node1].get(node2, None)
if test != None:
exStr = "Edge {} --> {} already existing.".format(node1,node2)
raise Exception(exStr)
else:
#print("Inserted {}-->{} ({})".format(node1,node2,weight))
self.__nodes[node1][node2] = weight
def deleteNode(self, node):
test = self.__nodes.get(node, None)
if test != None:
self.__nodes.pop(node)
# need to loop through all the nodes!!!
for n in self.__nodes:
test = self.__nodes[n].get(node, None)
if test != None:
self.__nodes[n].pop(node)
def deleteEdge(self, node1,node2):
test = self.__nodes.get(node1, None)
if test != None:
test = self.__nodes[node1].get(node2, None)
if test != None:
self.__nodes[node1].pop(node2)
def __len__(self):
return len(self.__nodes)
def nodes(self):
return list(self.__nodes.keys())
def graph(self):
return self.__nodes
def __str__(self):
ret = ""
for n in self.__nodes:
for edge in self.__nodes[n]:
ret += "{} -- {} --> {}\n".format(str(n),
str(self.__nodes[n][edge]),
str(edge))
return ret
def adjacent(self, node):
"""returns a list of nodes connected to node"""
ret = []
test = self.__nodes.get(node, None)
if test != None:
for n in self.__nodes:
if n == node:
#all outgoing edges
for edge in self.__nodes[node]:
ret.append(edge)
else:
#all incoming edges
for edge in self.__nodes[n]:
if edge == node:
ret.append(n)
return ret
def adjacentEdge(self, node, incoming = True):
"""
If incoming == False
we look at the edges of the node
else we need to loop through all the nodes.
An edge is present if there is a
corresponding entry in the dictionary.
If no such nodes exist returns None
"""
ret = []
if incoming == False:
edges = self.__nodes.get(node,None)
if edges != None:
for e in edges:
w = self.__nodes[node][e]
ret.append((node, e, w))
return ret
else:
for n in self.__nodes:
edge = self.__nodes[n].get(node,None)
if edge != None:
ret.append((n,node, edge))
return ret
def edges(self):
"""Returns all the edges in a list of triplets"""
ret = []
for node in self.__nodes:
for edge in self.__nodes[node]:
w = self.__nodes[node][edge]
ret.append((node,edge, w))
return ret
def edgeIn(self,node1,node2):
"""Checks if edge node1 --> node2 is present"""
n1 = self.__nodes.get(node1, None)
if n1 != None:
n2 = self.__nodes[node1].get(node2, None)
if n2 != None:
return True
else:
return False
else:
return False
Download the complete source file: DiGraphLL2.py
Depth First Search (DFS)¶
Example: Let’s implement DFS in the DiGraphLLAnalyzer
class we implemented in the previous lab.
[3]:
from collections import deque
class DiGraphLLAnalyzer(DiGraphLL):
"""Every node is an element in the dictionary.
The key is the node id and the value is a dictionary
with key second node and value the weight
"""
def getTopConnected_incoming(self):
topN = ""
#accumulator to count connections
conn = [0]*len(self.nodes())
for node in self.nodes():
for el in self.graph()[node]:
elInd = self.nodes().index(el)
conn[elInd] +=1
M = max(conn)
ind = [x for x in range(len(conn)) if conn[x] == M]
return [self.nodes()[x] for x in ind]
def getTopConnected_outgoing(self):
"""Returns the node(s)"""
topN = []
conn = -1
for node in self.nodes():
n = len(self.graph()[node])
if n > conn:
topN = [node]
conn = n
else:
if n == conn:
topN.append(node)
return topN
def hasPathAux(self, node1,node2):
if node1 not in self.nodes() or node2 not in self.nodes():
return False
else:
Q = deque()
Q.append(node1)
visited = set()
while len(Q) > 0:
curN = Q.popleft()
#do not travel on already visited nodes
if curN not in visited:
visited.add(curN)
#get all outgoing nodes of Q
for edge in self.graph()[curN]:
if edge == node2:
return True
else:
Q.append(edge)
return False
def hasPath(self, node1, node2):
#checks both paths and returns True or false
res = self.hasPathAux(node1,node2)
if res:
return True
else:
return self.hasPathAux(node2,node1)
def DFSvisit(self, root, preorder = True, visited = set()):
visited.add(root)
if preorder:
print("{}".format(root))
#remember that the self.adjacentEdge method returns:
#[('node1','node2', weight1), ...('node1', 'nodeX', weightX)]
outGoingEdges = self.adjacentEdge(root, incoming = False)
nextNodes = []
if len(outGoingEdges) > 0:
nextNodes = [x[1] for x in outGoingEdges]
#print(nextNodes)
for nextN in nextNodes:
if nextN not in visited:
print("\tfrom {} --> {}".format(root, nextN))
self.DFSvisit(nextN, preorder, visited)
if not preorder:
print("{}".format(root))
if __name__ == "__main__":
G = DiGraphLLAnalyzer()
for i in range(1,10):
G.insertNode("Node_" + str(i))
G.insertEdge("Node_1", "Node_2",1)
G.insertEdge("Node_2", "Node_1",1)
G.insertEdge("Node_1", "Node_3",1)
G.insertEdge("Node_1", "Node_5",1)
G.insertEdge("Node_2", "Node_3",1)
G.insertEdge("Node_2", "Node_5",1)
G.insertEdge("Node_3", "Node_4",1)
G.insertEdge("Node_3", "Node_6",1)
G.insertEdge("Node_5", "Node_3",1)
G.insertEdge("Node_5", "Node_5",1)
G.insertEdge("Node_6", "Node_4",1)
G.insertEdge("Node_6", "Node_6",1)
G.insertEdge("Node_7", "Node_5",1)
G.insertEdge("Node_5", "Node_8",1)
G.insertEdge("Node_8", "Node_7",1)
G.insertEdge("Node_9", "Node_2",1)
print("Preorder:")
G.DFSvisit("Node_1", preorder = True, visited = set())
print("\nPostorder:")
G.DFSvisit("Node_1", preorder = False, visited = set())
print("\nPreorder from Node_9")
G.DFSvisit("Node_9", preorder = True, visited = set())
Preorder:
Node_1
from Node_1 --> Node_2
Node_2
from Node_2 --> Node_3
Node_3
from Node_3 --> Node_4
Node_4
from Node_3 --> Node_6
Node_6
from Node_2 --> Node_5
Node_5
from Node_5 --> Node_8
Node_8
from Node_8 --> Node_7
Node_7
Postorder:
from Node_1 --> Node_2
from Node_2 --> Node_3
from Node_3 --> Node_4
Node_4
from Node_3 --> Node_6
Node_6
Node_3
from Node_2 --> Node_5
from Node_5 --> Node_8
from Node_8 --> Node_7
Node_7
Node_8
Node_5
Node_2
Node_1
Preorder from Node_9
Node_9
from Node_9 --> Node_2
Node_2
from Node_2 --> Node_1
Node_1
from Node_1 --> Node_3
Node_3
from Node_3 --> Node_4
Node_4
from Node_3 --> Node_6
Node_6
from Node_1 --> Node_5
Node_5
from Node_5 --> Node_8
Node_8
from Node_8 --> Node_7
Node_7
Download the complete source file: DiGraphLLAnalyzer2.py
If you look at the output of preorder visit from Node_1 you’ll notice that were are not visiting Node_9.
To make sure we visit all nodes we need to modify the code in the following way:
[6]:
from collections import deque
class DiGraphLLAnalyzer(DiGraphLL):
# ... missing code ... same as above
def DFSvisit(self, root, preorder = True, visited = set()):
visited.add(root)
if preorder:
print("{}".format(root))
#remember that the self.adjacentEdge method returns:
#[('node1','node2', weight1), ...('node1', 'nodeX', weightX)]
outGoingEdges = self.adjacentEdge(root, incoming = False)
nextNodes = []
if len(outGoingEdges) > 0:
nextNodes = [x[1] for x in outGoingEdges]
#print(nextNodes)
for nextN in nextNodes:
if nextN not in visited:
print("\tfrom {} --> {}".format(root, nextN))
self.DFSvisit(nextN, preorder, visited)
if not preorder:
print("{}".format(root))
def DFS(self, root, preorder = True):
visited = set()
#first visit from specified node then check all other nodes
#set is mutable so the set is going to change!
print("Starting from {}".format(root))
self.DFSvisit(root, preorder, visited)
for node in self.nodes():
if node not in visited:
print("Starting from {}".format(node))
self.DFSvisit(node, preorder, visited)
if __name__ == "__main__":
G = DiGraphLLAnalyzer()
for i in range(1,10):
G.insertNode("Node_" + str(i))
G.insertEdge("Node_1", "Node_2",1)
G.insertEdge("Node_2", "Node_1",1)
G.insertEdge("Node_1", "Node_3",1)
G.insertEdge("Node_1", "Node_5",1)
G.insertEdge("Node_2", "Node_3",1)
G.insertEdge("Node_2", "Node_5",1)
G.insertEdge("Node_3", "Node_4",1)
G.insertEdge("Node_3", "Node_6",1)
G.insertEdge("Node_5", "Node_3",1)
G.insertEdge("Node_5", "Node_5",1)
G.insertEdge("Node_6", "Node_4",1)
G.insertEdge("Node_6", "Node_6",1)
G.insertEdge("Node_7", "Node_5",1)
G.insertEdge("Node_5", "Node_8",1)
G.insertEdge("Node_8", "Node_7",1)
G.insertEdge("Node_9", "Node_2",1)
print("Preorder:")
G.DFS("Node_1", preorder = True)
print("\nPostorder:")
G.DFS("Node_1", preorder = False)
print("\nPostorder:")
G.DFS("Node_5", preorder = False)
Preorder:
Starting from Node_1
Node_1
from Node_1 --> Node_2
Node_2
from Node_2 --> Node_3
Node_3
from Node_3 --> Node_4
Node_4
from Node_3 --> Node_6
Node_6
from Node_2 --> Node_5
Node_5
from Node_5 --> Node_8
Node_8
from Node_8 --> Node_7
Node_7
Starting from Node_9
Node_9
Postorder:
Starting from Node_1
from Node_1 --> Node_2
from Node_2 --> Node_3
from Node_3 --> Node_4
Node_4
from Node_3 --> Node_6
Node_6
Node_3
from Node_2 --> Node_5
from Node_5 --> Node_8
from Node_8 --> Node_7
Node_7
Node_8
Node_5
Node_2
Node_1
Starting from Node_9
Node_9
Postorder:
Starting from Node_5
from Node_5 --> Node_3
from Node_3 --> Node_4
Node_4
from Node_3 --> Node_6
Node_6
Node_3
from Node_5 --> Node_8
from Node_8 --> Node_7
Node_7
Node_8
Node_5
Starting from Node_1
from Node_1 --> Node_2
Node_2
Node_1
Starting from Node_9
Node_9
Download the complete source file: DiGraphLLAnalyzer3.py
The graph created above (except for the weights of the edges) is:
Exercises¶
Implement depth first search for the class
DiGraphLLAnalyzer
using an explicit stack and a pre-order visit of the nodes (to keep things simple). Remember that you can simulate a stack (FILO – first in last out queue) by using a list withappend(elem)
andpop(-1)
(orcollections.deque
and the methodsappend(elem)
andpop()
. Use the pseudocode below (I suggest to use a set rather than a boolean list for the visited structure):
You can test your methods with the following code:
G = DiGraphLLAnalyzer()
G1 = DiGraphLLAnalyzer()
for i in range(1,10):
G.insertNode("Node_" + str(i))
G1.insertNode("Node_" + str(i))
G.insertEdge("Node_1", "Node_2",1)
G.insertEdge("Node_2", "Node_1",1)
G.insertEdge("Node_1", "Node_3",1)
G.insertEdge("Node_1", "Node_5",1)
G.insertEdge("Node_2", "Node_3",1)
G.insertEdge("Node_2", "Node_5",1)
G.insertEdge("Node_3", "Node_4",1)
G.insertEdge("Node_3", "Node_6",1)
G.insertEdge("Node_5", "Node_3",1)
G.insertEdge("Node_5", "Node_5",1)
G.insertEdge("Node_6", "Node_4",1)
G.insertEdge("Node_6", "Node_6",1)
G.insertEdge("Node_7", "Node_5",1)
G.insertEdge("Node_5", "Node_8",1)
G.insertEdge("Node_8", "Node_7",1)
G.insertEdge("Node_9", "Node_2",1)
#Graph G1
G1.insertEdge("Node_1" ,"Node_2", 1)
G1.insertEdge("Node_1" ,"Node_4", 1)
G1.insertEdge("Node_1" ,"Node_6", 1)
G1.insertEdge("Node_1" ,"Node_7", 1)
G1.insertEdge("Node_2" ,"Node_7", 1)
G1.insertEdge("Node_3" ,"Node_4", 1)
G1.insertEdge("Node_5" ,"Node_4", 1)
G1.insertEdge("Node_6" ,"Node_5", 1)
G1.insertEdge("Node_6" ,"Node_8", 1)
G1.insertEdge("Node_7" ,"Node_9", 1)
G1.insertEdge("Node_8" ,"Node_9", 1)
G.DFS("Node_1")
G.DFS("Node_5")
G1.DFS("Node_1")
G1.DFS("Node_4")
G1.DFS("Node_6")
Show/Hide Solution
Write a method
getDFStime(self,start_sort = True)
that for each node of aDiGraphLLAnalyzer
, returns its start and end time in a DFS visit. Time is just a counter that counts the visiting operations. Start time is when a node has first been encountered, and end time is when all its subgraph has been visited and the algorithm moved to another part of the graph. Sort the results by start time or by end time depending on the value ofstart_sort
that can be True or False (Hint: use lambda functions with sort!).
Hint: implement another function (dfsTime(self, root, visited, times, time)
) that performs a dfs of the graph updating a structure of start and end times (times
) and keeping a counter (time
) that increases step by step by one.
Test the code with the following two graphs G and G1 (checking them with the pictures below):
that can be generated by the following code:
G = DiGraphLLAnalyzer()
G1 = DiGraphLLAnalyzer()
for i in range(1,10):
G.insertNode("Node_" + str(i))
G1.insertNode("Node_" + str(i))
G.insertEdge("Node_1", "Node_2",1)
G.insertEdge("Node_2", "Node_1",1)
G.insertEdge("Node_1", "Node_3",1)
G.insertEdge("Node_1", "Node_5",1)
G.insertEdge("Node_2", "Node_3",1)
G.insertEdge("Node_2", "Node_5",1)
G.insertEdge("Node_3", "Node_4",1)
G.insertEdge("Node_3", "Node_6",1)
G.insertEdge("Node_5", "Node_3",1)
G.insertEdge("Node_5", "Node_5",1)
G.insertEdge("Node_6", "Node_4",1)
G.insertEdge("Node_6", "Node_6",1)
G.insertEdge("Node_7", "Node_5",1)
G.insertEdge("Node_5", "Node_8",1)
G.insertEdge("Node_8", "Node_7",1)
G.insertEdge("Node_9", "Node_2",1)
#Graph G1
G1.insertEdge("Node_1" ,"Node_2", 1)
G1.insertEdge("Node_1" ,"Node_4", 1)
G1.insertEdge("Node_1" ,"Node_6", 1)
G1.insertEdge("Node_1" ,"Node_7", 1)
G1.insertEdge("Node_2" ,"Node_7", 1)
G1.insertEdge("Node_3" ,"Node_4", 1)
G1.insertEdge("Node_5" ,"Node_4", 1)
G1.insertEdge("Node_6" ,"Node_5", 1)
G1.insertEdge("Node_6" ,"Node_8", 1)
G1.insertEdge("Node_7" ,"Node_9", 1)
G1.insertEdge("Node_8" ,"Node_9", 1)
Show/Hide Solution
Breadth First Search (BFS)¶
This algorithm is quite useful to find the shortest path between two nodes.
Example: Let’s implement BFS in the DiGraphLLAnalyzer
class.
[17]:
from collections import deque
class DiGraphLLAnalyzer(DiGraphLL):
# ... missing code ...
def BFSvisit(self, root):
if root in self.graph():
Q = deque()
Q.append(root)
#visited is a set of visited nodes
visited = set()
visited.add(root)
while len(Q) > 0:
curNode = Q.popleft()
outGoingEdges = self.adjacentEdge(curNode, incoming = False)
nextNodes = []
if outGoingEdges != None:
#remember that self.adjacentEdge returns:
#[('node1','node2', weight1), ...('node1', 'nodeX', weightX)]
nextNodes = [x[1] for x in outGoingEdges]
print("From {}:".format(curNode))
for nextNode in nextNodes:
if nextNode not in visited:
Q.append(nextNode)
visited.add(nextNode)
print("\t --> {}".format(nextNode ))
if __name__ == "__main__":
G = DiGraphLLAnalyzer()
for i in range(1,10):
G.insertNode("Node_" + str(i))
G.insertEdge("Node_1", "Node_2",1)
G.insertEdge("Node_2", "Node_1",1)
G.insertEdge("Node_1", "Node_3",1)
G.insertEdge("Node_1", "Node_5",1)
G.insertEdge("Node_2", "Node_3",1)
G.insertEdge("Node_2", "Node_5",1)
G.insertEdge("Node_3", "Node_4",1)
G.insertEdge("Node_3", "Node_6",1)
G.insertEdge("Node_5", "Node_3",1)
G.insertEdge("Node_5", "Node_5",1)
G.insertEdge("Node_6", "Node_4",1)
G.insertEdge("Node_6", "Node_6",1)
G.insertEdge("Node_7", "Node_5",1)
G.insertEdge("Node_5", "Node_8",1)
G.insertEdge("Node_8", "Node_7",1)
G.insertEdge("Node_9", "Node_2",1)
print("BFS(Node_1):")
G.BFSvisit("Node_1")
print("\n------------")
print("\nNow BFS(Node_5):")
G.BFSvisit("Node_5")
print("\n------------")
print("\nNow BFS(Node_9):")
G.BFSvisit("Node_9")
print("\n------------")
BFS(Node_1):
From Node_1:
--> Node_2
--> Node_3
--> Node_5
From Node_2:
From Node_3:
--> Node_4
--> Node_6
From Node_5:
--> Node_8
From Node_4:
From Node_6:
From Node_8:
--> Node_7
From Node_7:
------------
Now BFS(Node_5):
From Node_5:
--> Node_3
--> Node_8
From Node_3:
--> Node_4
--> Node_6
From Node_8:
--> Node_7
From Node_4:
From Node_6:
From Node_7:
------------
Now BFS(Node_9):
From Node_9:
--> Node_2
From Node_2:
--> Node_1
--> Node_3
--> Node_5
From Node_1:
From Node_3:
--> Node_4
--> Node_6
From Node_5:
--> Node_8
From Node_4:
From Node_6:
From Node_8:
--> Node_7
From Node_7:
------------
The created graph is (do not look at the weights):
Download the complete source file: DiGraphLLAnalyzer4.py
Also in this case Node_9 is not visited! (if you start from Node_1)
To visit all the nodes of the tree and get a forest of BFS trees we need to modify the code as follows:
[18]:
from collections import deque
class DiGraphLLAnalyzer(DiGraphLL):
# ... missing code ...
def BFSvisit(self, root, target):
len_path = 0
if root in self.graph():
Q = deque()
Q.append(root)
#visited is a set of visited nodes
visited = set()
if root == target:
return len_path
visited.add(root)
while len(Q) > 0:
curNode = Q.popleft()
len_path = len_path + 1
outGoingEdges = self.adjacentEdge(curNode, incoming = False)
nextNodes = []
if outGoingEdges != None:
#remember that self.adjacentEdge returns:
#[('node1','node2', weight1), ...('node1', 'nodeX', weightX)]
nextNodes = [x[1] for x in outGoingEdges]
if target in nextNodes:
return len_path + 1
print("From {}:".format(curNode))
for nextNode in nextNodes:
if nextNode not in visited:
Q.append(nextNode)
visited.add(nextNode)
print("\t --> {}".format(nextNode ))
return None
def BFS(self, root):
print("Starting from {}".format(root))
visited = set()
visited.add(root)
self.BFSvisit(root,visited)
for node in self.nodes():
if node not in visited:
print("Starting from {}".format(node))
self.BFSvisit(node,visited)
if __name__ == "__main__":
G = DiGraphLLAnalyzer()
for i in range(1,10):
G.insertNode("Node_" + str(i))
G.insertEdge("Node_1", "Node_2",1)
G.insertEdge("Node_2", "Node_1",1)
G.insertEdge("Node_1", "Node_3",1)
G.insertEdge("Node_1", "Node_5",1)
G.insertEdge("Node_2", "Node_3",1)
G.insertEdge("Node_2", "Node_5",1)
G.insertEdge("Node_3", "Node_4",1)
G.insertEdge("Node_3", "Node_6",1)
G.insertEdge("Node_5", "Node_3",1)
G.insertEdge("Node_5", "Node_5",1)
G.insertEdge("Node_6", "Node_4",1)
G.insertEdge("Node_6", "Node_6",1)
G.insertEdge("Node_7", "Node_5",1)
G.insertEdge("Node_5", "Node_8",1)
G.insertEdge("Node_8", "Node_7",1)
G.insertEdge("Node_9", "Node_2",1)
print("BFS(Node_1):")
G.BFS("Node_1")
print("\n------------")
print("\nNow BFS(Node_5):")
#G.BFS("Node_5")
print("\n------------")
print("\nNow BFS(Node_9):")
#G.BFS("Node_9")
print("\n------------")
BFS(Node_1):
Starting from Node_1
From Node_1:
--> Node_2
--> Node_3
--> Node_5
From Node_2:
From Node_3:
--> Node_4
--> Node_6
From Node_5:
--> Node_8
From Node_4:
From Node_6:
From Node_8:
--> Node_7
From Node_7:
Starting from Node_2
From Node_2:
--> Node_1
--> Node_3
--> Node_5
From Node_1:
From Node_3:
--> Node_4
--> Node_6
From Node_5:
--> Node_8
From Node_4:
From Node_6:
From Node_8:
--> Node_7
From Node_7:
Starting from Node_3
From Node_3:
--> Node_4
--> Node_6
From Node_4:
From Node_6:
Starting from Node_4
From Node_4:
Starting from Node_5
From Node_5:
--> Node_3
--> Node_8
From Node_3:
--> Node_4
--> Node_6
From Node_8:
--> Node_7
From Node_4:
From Node_6:
From Node_7:
Starting from Node_6
From Node_6:
--> Node_4
From Node_4:
Starting from Node_7
From Node_7:
--> Node_5
From Node_5:
--> Node_3
--> Node_8
From Node_3:
--> Node_4
--> Node_6
From Node_8:
From Node_4:
From Node_6:
Starting from Node_8
From Node_8:
--> Node_7
From Node_7:
--> Node_5
From Node_5:
--> Node_3
From Node_3:
--> Node_4
--> Node_6
From Node_4:
From Node_6:
Starting from Node_9
From Node_9:
--> Node_2
From Node_2:
--> Node_1
--> Node_3
--> Node_5
From Node_1:
From Node_3:
--> Node_4
--> Node_6
From Node_5:
--> Node_8
From Node_4:
From Node_6:
From Node_8:
--> Node_7
From Node_7:
------------
Now BFS(Node_5):
------------
Now BFS(Node_9):
------------
Download the complete source file: DiGraphLLAnalyzer5.py
Exercise¶
Write a method
shortestPath(self, node1, node2)
that finds the shortest path between two nodesnode1
andnode2
(if it exists) ignoring any weight on the edges.
Hint: modify the BFS in such a way at it stops when the second node is reached (or when no more nodes can be explored). Apply this code twice, that is once from node1–>node2 and the other from node2–>node1 to find the shortest of the two. You need to change the code in order to obtain the path.
Structuring the code (hints):
Implement a method
shortestPath(self,node1,node2)
that finds and prints the shortest path between node1 and node2 (testing both node1 –> node2 and node2 –> node1 and returning the shortest of the two) calling the other two functions;Implement a method
path = findShortestPath(self,node1,node2)
that returns the path going from node1 to node2 if this exist. Note: in my implementation, path is a list starting with the terminal node [node2, nodex, nodexx, node1] and ending with the first node of the sought path. The idea behind the implementation of this method is to store somewhere (like in a dictionary) the parent of each visited node and then produce the list above going backwards from node2 up to node1.Implement a method
printPath(self,path)
that gets a path as specified above and prints it out like:
Shortest path between: Node_1 and Node_6
Node_1 --> Node_3
Node_3 --> Node_6
Test the code with the following two graphs G and G1 (checking them with the pictures below):
G = DiGraphLLAnalyzer()
G1 = DiGraphLLAnalyzer()
for i in range(1,10):
G.insertNode("Node_" + str(i))
G1.insertNode("Node_" + str(i))
G.insertEdge("Node_1", "Node_2",1)
G.insertEdge("Node_2", "Node_1",1)
G.insertEdge("Node_1", "Node_3",1)
G.insertEdge("Node_1", "Node_5",1)
G.insertEdge("Node_2", "Node_3",1)
G.insertEdge("Node_2", "Node_5",1)
G.insertEdge("Node_3", "Node_4",1)
G.insertEdge("Node_3", "Node_6",1)
G.insertEdge("Node_5", "Node_3",1)
G.insertEdge("Node_5", "Node_5",1)
G.insertEdge("Node_6", "Node_4",1)
G.insertEdge("Node_6", "Node_6",1)
G.insertEdge("Node_7", "Node_5",1)
G.insertEdge("Node_5", "Node_8",1)
G.insertEdge("Node_8", "Node_7",1)
G.insertEdge("Node_9", "Node_2",1)
#Graph G1
G1.insertEdge("Node_1" ,"Node_2", 1)
G1.insertEdge("Node_1" ,"Node_4", 1)
G1.insertEdge("Node_1" ,"Node_6", 1)
G1.insertEdge("Node_1" ,"Node_7", 1)
G1.insertEdge("Node_2" ,"Node_7", 1)
G1.insertEdge("Node_3" ,"Node_4", 1)
G1.insertEdge("Node_5" ,"Node_4", 1)
G1.insertEdge("Node_6" ,"Node_5", 1)
G1.insertEdge("Node_6" ,"Node_8", 1)
G1.insertEdge("Node_7" ,"Node_9", 1)
G1.insertEdge("Node_8" ,"Node_9", 1)
G.shortestPath("Node_1","Node_6")
G.shortestPath("Node_8","Node_9")
G.shortestPath("Node_8","Node_4")
print("\n\nG1:")
G1.shortestPath("Node_8","Node_4")
G1.shortestPath("Node_9","Node_1")
G1.shortestPath("Node_4","Node_1")
Show/Hide Solution
Exercise¶
Write a method
isCyclic(self)
that identifies if a direct graph (implemented as aDiGraphLLAnalyzer
) is cyclic or not (i.e. returns True for a graph with cycles and False for a graph without cycles).
Hint: you can perform a recursive DFS and check if any edge that is being explored is actually a back-edge (i.e. an edge that points back to one of the nodes present in the recursion-stack of the nodes found so far). To do that you have to store a set of nodes that are along the path that is traversed recursively and check if the nodes that you are adding to the path are already present in this set of nodes).
Test the code with the following three graphs:
G = DiGraphLLAnalyzer()
G1 = DiGraphLLAnalyzer()
G2 = DiGraphLLAnalyzer()
for i in range(1,10):
G.insertNode("Node_" + str(i))
G1.insertNode("Node_" + str(i))
if i<9:
G2.insertNode("Node_" + str(i))
G.insertEdge("Node_1", "Node_2",1)
G.insertEdge("Node_2", "Node_1",1)
G.insertEdge("Node_1", "Node_3",1)
G.insertEdge("Node_1", "Node_5",1)
G.insertEdge("Node_2", "Node_3",1)
G.insertEdge("Node_2", "Node_5",1)
G.insertEdge("Node_3", "Node_4",1)
G.insertEdge("Node_3", "Node_6",1)
G.insertEdge("Node_5", "Node_3",1)
G.insertEdge("Node_5", "Node_5",1)
G.insertEdge("Node_6", "Node_4",1)
G.insertEdge("Node_6", "Node_6",1)
G.insertEdge("Node_7", "Node_5",1)
G.insertEdge("Node_5", "Node_8",1)
G.insertEdge("Node_8", "Node_7",1)
G.insertEdge("Node_9", "Node_2",1)
#Graph G1
G1.insertEdge("Node_1" ,"Node_2", 1)
G1.insertEdge("Node_1" ,"Node_4", 1)
G1.insertEdge("Node_1" ,"Node_6", 1)
G1.insertEdge("Node_1" ,"Node_7", 1)
G1.insertEdge("Node_2" ,"Node_7", 1)
G1.insertEdge("Node_3" ,"Node_4", 1)
G1.insertEdge("Node_5" ,"Node_4", 1)
G1.insertEdge("Node_6" ,"Node_5", 1)
G1.insertEdge("Node_6" ,"Node_8", 1)
G1.insertEdge("Node_7" ,"Node_9", 1)
G1.insertEdge("Node_8" ,"Node_9", 1)
#Graph G2
G2.insertEdge("Node_1" ,"Node_2", 1)
G2.insertEdge("Node_2" ,"Node_3", 1)
G2.insertEdge("Node_2" ,"Node_4", 1)
G2.insertEdge("Node_2" ,"Node_5", 1)
G2.insertEdge("Node_6" ,"Node_2", 1)
G2.insertEdge("Node_6" ,"Node_7",1)
G2.insertEdge("Node_8" ,"Node_7", 1)
G2.insertEdge("Node_8" ,"Node_4", 1)
G2.insertEdge("Node_7" ,"Node_5", 1)
print("Is G cyclic? {}".format(G.isCyclic()))
print("\nG1:")
print("Is G1 cyclic? {}".format(G1.isCyclic()))
print("\nG2:")
print("Is G2 cyclic? {}".format(G2.isCyclic()))
Here are the graphs:
Show/Hide Solution