
PDSA - Week 4Introduction of GraphBasic Terminology of GraphGraph Representation in data structureFor unweighted directed graphFor unweighted undirected graphGraph Traversing AlgorithmBreadth First Search(BFS) AlgorithmWorking Visualization Implementation BFS for adjacency list of graphImplementation BFS for adjacency matrix of graphDepth First Search(DFS)AlgorithmWorking Visualization Implementation of DFS for adjacency list of graphImplementation of DFS for adjacency matrix of graphApplication of BFS and DFSFind Connected Components in graph using BFSAlgorithmImplementation of Find Connected Components in graph using BFSPre and Post numbering using DFSAlgorithmImplementation of Pre and Post numbering on graph using DFSDirected Acyclic Graph(DAG)Topological SortAlgorithmWorking VisualizationImplementation of Topological Sort for Adjacency listImplementation of Topological Sort for Adjacency matrixLongest Path in DAGAlgorithmWorking VisualizationImplementation for Longest Path on DAGVisualization of graph algorithms using visualgo
Graph:- It is a non-linear data structure. A graph G consist of a non empty set V where members are called the vertices of graph and the set E where member are called the edges.
G = (V, E)
V = set of vertices
E = set of edges
Example:-

G = (V, E)
V = {A,B,C,D,E}
E = {e1,e2,e3,e4,e5} or {(A,B), (A,C), (B,C), (C,D), (C,E)}
Vertex (or node):
A vertex or node is a fundamental unit in a graph. In a simple graph, each vertex can be connected to other vertices by edges. Vertices are often represented by circles or dots in visual representations of graphs.
Edge:
An edge is a line or connection between two vertices in a graph. Edges can be directed or undirected, and weighted or unweighted. In an undirected graph, edges connect vertices without a specified direction, while in a directed graph, edges have a direction, represented by an arrow. In a weighted graph, edges have a numerical weight or value assigned to them.
Degree:
The degree of a vertex is the number of edges that are connected to it. In a directed graph, the degree of a vertex is defined as the sum of the in-degree (number of edges coming into the vertex) and out-degree (number of edges going out of the vertex).
Path:
A path is a sequence of vertices connected by edges. A simple path is a path where no vertex is repeated.
Cycle:
A cycle is a path that starts and ends at the same vertex.
Connectedness:
A graph is said to be connected if there is a path between any two vertices in the graph. A disconnected graph is a graph that is not connected, meaning it can be broken into two or more separate components.
Component:
A component is a connected subgraph of a larger graph. In other words, it is a part of the graph that is connected to other vertices or edges within that part, but not connected to the rest of the graph.
Directed graph:
A directed graph is a graph where edges have a direction, represented by an arrow. In a directed graph, edges are often called arcs.
Undirected graph:
An undirected graph is a graph where edges do not have a direction.
Weighted graph:
A weighted graph is a graph where edges have weights or values assigned to them.
Adjacent vertices:
Two vertices are adjacent if they are connected by an edge.
Incidence:
A vertex is incident on an edge if the vertex is one of the endpoints of the edge.
Subgraph:
A subgraph is a graph that is a subset of another graph, with some edges and vertices removed.
Complete graph:
A complete graph is a graph where every vertex is connected to every other vertex. In other words, there is an edge between every pair of vertices in the graph.
Logical Representation of graph

Adjacency matrix:
An adjacency matrix is a two-dimensional array or list that represents a graph. The matrix has a row and column for each vertex, and the value at position (i, j) indicates whether there is an edge between vertices i and j. If there is an edge, the value is 1, and if there is no edge, the value is 0. This representation is useful for dense graphs, where the number of edges is close to the maximum possible number of edges. In python we can use NumPy array or python list for adjacency matrix.
.
Adjacency list:
An adjacency list is a list of lists where each vertex has a list of its adjacent vertices. Each list contains the vertices that are adjacent to the vertex at that index. This representation is useful for sparse graphs, where the number of edges is much smaller than the maximum possible number of edges. In python we can use dictionary for adjacency list.

G = (V, E)
V = [0,1,2,3,4]
E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)]
Adjacency matrix representation:
Using NumPy 2d array
1V = [0,1,2,3,4]2E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)] # each tuple(u,v) represent edge from u to v3size = len(V)4import numpy as np5AMat = np.zeros(shape=(size,size))6for (i,j) in E:7 AMat[i,j] = 1 # mark 1 if edge present in graph from i to j , otherwise 08print(AMat)Output adjacency matrix (AMat)
xxxxxxxxxx61[[0. 1. 1. 0. 0.]2 [0. 0. 0. 1. 1.]3 [0. 0. 0. 1. 1.]4 [0. 0. 0. 0. 1.]5 [0. 0. 0. 0. 0.]]6# AMat[i,j] == 1 represent edge from i to j
Using Python nested list
xxxxxxxxxx121V = [0,1,2,3,4]2E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)]3size = len(V)4AMat = []5for i in range(size):6 row = []7 for j in range(size):8 row.append(0)9 AMat.append(row.copy()) 10for (i,j) in E:11 AMat[i][j] = 1 # mark 1 if edge present in graph from i to j , otherwise 012print(AMat)Output adjacency matrix (AMat)
xxxxxxxxxx61[[0, 1, 1, 0, 0],2[0, 0, 0, 1, 1],3[0, 0, 0, 1, 1],4[0, 0, 0, 0, 1],5[0, 0, 0, 0, 0]]6# AMat[i][j] == 1 represent edge from i to j
Adjacency list representation:
Using python dictionary
xxxxxxxxxx101V = [0,1,2,3,4]2E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)]3size = len(V)4AList = {}5# In dictionay AList, for example, AList[i] = [j,k] represent two edge from i to j and i to k6for i in range(size):7 AList[i] = []8for (i,j) in E:9 AList[i].append(j)10print(AList)Output adjacency list (AList)
xxxxxxxxxx21{0: [1, 2], 1: [3, 4], 2: [4, 3], 3: [4], 4: []}2# for example, AList[i] = [j,k] represent two edge from i to j and i to k

G = (V, E)
V = [0,1,2,3,4]
E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)]
Adjacency matrix representation:
Using NumPy 2d array
xxxxxxxxxx91V = [0,1,2,3,4]2E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)]3UE = E + [ (j,i) for (i,j) in E] # each edge represented by two tuple (u,v) and (v,u)4size = len(V)5import numpy as np6AMat = np.zeros(shape=(size,size))7for (i,j) in UE:8 AMat[i,j] = 1 # mark 1 if edge present in graph from i to j , otherwise 09print(AMat)Output adjacency matrix (AMat)
xxxxxxxxxx61[[0. 1. 1. 0. 0.]2 [1. 0. 0. 1. 1.]3 [1. 0. 0. 1. 1.]4 [0. 1. 1. 0. 1.]5 [0. 1. 1. 1. 0.]]6 # AMat[i,j] == 1 represent edge from i to j
Using Python nested list
xxxxxxxxxx131V = [0,1,2,3,4]2E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)]3UE = E + [ (j,i) for (i,j) in E] # each edge represented by two tuple (u,v) and (v,u)4size = len(V)5AMat = []6for i in range(size):7 row = []8 for j in range(size):9 row.append(0)10 AMat.append(row.copy()) 11for (i,j) in UE:12 AMat[i][j] = 1 # mark 1 if edge present in graph from i to j , otherwise 013print(AMat)Output adjacency matrix (AMat)
xxxxxxxxxx61[[0, 1, 1, 0, 0], 2[1, 0, 0, 1, 1], 3[1, 0, 0, 1, 1], 4[0, 1, 1, 0, 1], 5[0, 1, 1, 1, 0]]6# AMat[i][j] == 1 represent edge from i to j
Adjacency list representation:
Using python dictionary
xxxxxxxxxx111V = [0,1,2,3,4]2E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)]3UE = E + [ (j,i) for (i,j) in E] # each edge represented by two tuple (u,v) and (v,u)4size = len(V)5AList = {}6# In dictionay AList, for example, AList[i] = [j,k] represent two edge from i to j and i to k7for i in range(size):8 AList[i] = []9for (i,j) in UE:10 AList[i].append(j)11print(AList)Output adjacency list (AList)
xxxxxxxxxx21{0: [1, 2], 1: [3, 4, 0], 2: [4, 3, 0], 3: [4, 1, 2], 4: [1, 2, 3]}2# for example, AList[i] = [j,k] represent two edge from i to j and i to k
Graph traversal is the process of visiting all the vertices (nodes) of a graph. There are two commonly used algorithms for graph traversal:
Breadth-First Search (BFS)
Depth-First Search (DFS)
The Breadth First Search (BFS) algorithm is used to traverse a graph or a tree in a breadth-first manner. BFS starts at the root node and explores all the neighboring nodes at the current depth before moving on to the nodes at the next depth. This is done by using a queue data structure. The algorithm marks each visited node to avoid revisiting it.
Here are the steps for the BFS algorithm:
Choose a starting node and enqueue it to a queue.
Mark the starting node as visited.
While the queue is not empty, dequeue a node from the front of the queue.
For each of the dequeued node's neighbors that are not visited, mark them as visited and enqueue them to the queue.
Repeat steps 3-4 until the queue is empty.
To keep track of the traversal order, you can add each visited node to a list as it is dequeued from the queue.
Complexity is
x1# Queue Implementation2class Queue:3 def __init__(self):4 self.queue = []5 def enqueue(self,v):6 self.queue.append(v)7 def isempty(self):8 return(self.queue == []) 9 def dequeue(self):10 v = None11 if not self.isempty():12 v = self.queue[0]13 self.queue = self.queue[1:]14 return(v) 15 def __str__(self):16 return(str(self.queue))17
18# BFS Implementation For Adjacency list19def BFSList(AList,start_vertex):20 # Initialization21 visited = {}22 for each_vertex in AList.keys():23 visited[each_vertex] = False 24 25 # Create Queue object q26 q = Queue()27 28 # Mark the start_vertex visited and insert it into the queue 29 visited[start_vertex] = True30 q.enqueue(start_vertex)31 32 # Repeat the following until the queue is empty 33 while(not q.isempty()):34 # Remove the one vertex from queue35 curr_vertex = q.dequeue()36 # Visit each adjacent of the removed vertex (if not visited), mark that visited, and insert it into the queue 37 for adj_vertex in AList[curr_vertex]:38 if (not visited[adj_vertex]):39 visited[adj_vertex] = True40 q.enqueue(adj_vertex) 41 return(visited)42
43AList = {0: [1, 2], 1: [3, 4], 2: [4, 3], 3: [4], 4: []}44print(BFSList(AList,0))Output
xxxxxxxxxx11{0: True, 1: True, 2: True, 3: True, 4: True}
Code Execution Flow
xxxxxxxxxx621# Queue Implementation2class Queue:3 def __init__(self):4 self.queue = []5 def enqueue(self,v):6 self.queue.append(v)7 def isempty(self):8 return(self.queue == [])9 def dequeue(self):10 v = None11 if not self.isempty():12 v = self.queue[0]13 self.queue = self.queue[1:]14 return(v) 15 def __str__(self):16 return(str(self.queue))17
18# Function to return list of neighbours or adjacent vertex of vertex i19def neighbours(AMat,i):20 nbrs = []21 (rows,cols) = AMat.shape22 for j in range(cols):23 if AMat[i,j] == 1:24 nbrs.append(j)25 return(nbrs)26
27# BFS Implementation For Adjacency matrix28def BFS(AMat,start_vertex):29 # Initialization30 (rows,cols) = AMat.shape31 visited = {}32 for each_vertex in range(rows):33 visited[each_vertex] = False 34 35 # Create Queue object q36 q = Queue()37 38 # Mark the start_vertex visited and insert it into the queue 39 visited[start_vertex] = True40 q.enqueue(start_vertex)41 42 # Repeat the following until the queue is empty 43 while(not q.isempty()):44 # Remove the one vertex from queue45 curr_vertex = q.dequeue()46 # Visit the each adjacent of removed vertex(if not visited) and insert into the queue47 for adj_vertex in neighbours(AMat,curr_vertex):48 if (not visited[adj_vertex]):49 visited[adj_vertex] = True50 q.enqueue(adj_vertex)51 52 return(visited)53
54
55V = [0,1,2,3,4]56E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)] 57size = len(V)58import numpy as np59AMat = np.zeros(shape=(size,size))60for (i,j) in E:61 AMat[i,j] = 162print(BFS(AMat,0))Output
xxxxxxxxxx11{0: True, 1: True, 2: True, 3: True, 4: True}
Code Execution Flow
Find parent of each vertex using BFS
Parent information is useful to determine shortest path from vertex v in term of number of edges or vertex in path.
xxxxxxxxxx501# Queue Implementation2class Queue:3 def __init__(self):4 self.queue = []5 def enqueue(self,v):6 self.queue.append(v)7 def isempty(self):8 return(self.queue == [])9 def dequeue(self):10 v = None11 if not self.isempty():12 v = self.queue[0]13 self.queue = self.queue[1:]14 return(v) 15 def __str__(self):16 return(str(self.queue))17
18# Using BFS approch For Adjacency list, for path, maintaining the parent of each vertex19# Path can be found by backtracking from destination to source using parent information20def BFSListPath(AList,start_vertex):21 # Initialization22 (visited,parent) = ({},{})23 for each_vertex in AList.keys():24 visited[each_vertex] = False25 parent[each_vertex] = -1 26 27 # Create Queue object q28 q = Queue()29 30 # Mark the start_vertex visited and insert it into the queue 31 visited[start_vertex] = True32 q.enqueue(start_vertex)33 34 # Repeat the following until the queue is empty35 while(not q.isempty()):36 # Remove the one vertex from queue37 curr_vertex = q.dequeue()38 # Visit the each adjacent of removed vertex(if not visited) and insert into the queue39 for adj_vertex in AList[curr_vertex]:40 if (not visited[adj_vertex]):41 visited[adj_vertex] = True42 # Assigne the curr_vertex as parent of each unvisited adjacent of curr_vertex43 parent[adj_vertex] = curr_vertex44 q.enqueue(adj_vertex)45 46 return(visited,parent)47
48
49AList ={0: [1, 2], 1: [3, 4], 2: [4, 3], 3: [4], 4: []}50print(BFSListPath(AList,0))Output
xxxxxxxxxx11({0: True, 1: True, 2: True, 3: True, 4: True}, {0: -1, 1: 0, 2: 0, 3: 1, 4: 1})
Find level number of vertices using BFS
Maintain level information to record length of the shortest path, in terms of number of edges or vertex.
xxxxxxxxxx511# Queue Implementation2class Queue:3 def __init__(self):4 self.queue = []5 def enqueue(self,v):6 self.queue.append(v)7 def isempty(self):8 return(self.queue == [])9 def dequeue(self):10 v = None11 if not self.isempty():12 v = self.queue[0]13 self.queue = self.queue[1:]14 return(v) 15 def __str__(self):16 return(str(self.queue))17
18# Using BFS approch For Adjacency list, for path, maintaining the parent of each vertex19# Using BFS approch maintaing the adjacent level number from source vertrex20def BFSListPathLevel(AList,v):21 # Initialization22 (level,parent) = ({},{})23 for each_vertex in AList.keys():24 level[each_vertex] = -125 parent[each_vertex] = -126 27 # Create Queue object q28 q = Queue()29 30 # Assigning the level 0 for start_vertex and insert it into the queue31 level[v] = 032 q.enqueue(v)33 34 # Repeat the following until the queue is empty35 while(not q.isempty()):36 # Remove the one vertex from queue37 curr_vertex = q.dequeue()38 # Visit the each adjacent of curr_vertex(if level value is -1) and insert into the queue39 for adj_vertex in AList[curr_vertex]:40 if (level[adj_vertex] == -1):41 # Assign the level value on each adjacent one more than the curr_vertex level42 level[adj_vertex] = level[curr_vertex] + 143 # Assigne the curr_vertex as parent of adjacent vertex of curr_vertex44 parent[adj_vertex] = curr_vertex45 q.enqueue(adj_vertex)46 47 return(level,parent)48
49
50AList ={0: [1, 2], 1: [3, 4], 2: [4, 3], 3: [4], 4: []}51print(BFSListPathLevel(AList,0))Output
xxxxxxxxxx11({0: 0, 1: 1, 2: 1, 3: 2, 4: 2}, {0: -1, 1: 0, 2: 0, 3: 1, 4: 1})
The Depth First Search (DFS) algorithm is used to traverse a graph or a tree in a depth-first manner, exploring as far as possible along each branch before backtracking. This is done by using a stack data structure.
Here are the steps for the Recursive DFS algorithm:
Choose a starting node and mark it as visited.
For each unvisited neighbor of the starting node, recursively call the DFS algorithm starting from that neighbor.
Repeat step 2 for all unvisited neighbors.
Here are the steps for DFS using a stack:
Create a stack S and a set visited to keep track of visited nodes.
Push the starting node onto the stack S.
While S is not empty, pop the top element u from S.
If u is not visited, mark it as visited and do the following:
Perform any processing on the node u.
Get all unvisited neighbors v of u and push them onto S.
Repeat steps 3-4 until S is empty.
To keep track of the traversal order, you can add each visited node to a list as it is visited.
Complexity is
DFS using Stack for adjacency list of graph
xxxxxxxxxx451# Stack Implementation2class Stack:3 def __init__(self):4 self.stack = []5 def Push(self,v):6 self.stack.append(v)7 def isempty(self):8 return(self.stack == [])9 def Pop(self):10 v = None11 if not self.isempty():12 v = self.stack.pop()13 return(v) 14 def __str__(self):15 return(str(self.stack))16
17# DFS Implementation for Adjacency list18def DFSList(AList,start_vertex):19 # Initializaion20 visited = {}21 for each_vertex in AList.keys():22 visited[each_vertex] = False 23 24 # Create stack object st25 st = Stack()26 27 # Push start_vertex in to the stack as first vertex28 st.Push(start_vertex) 29 30 # Repeat the following until the stack is empty31 while(not st.isempty()):32 # Pop one vertex from stack 33 current_vertex = st.Pop()34 # If popped vertex is not visited, marked visited35 if visited[current_vertex] == False:36 visited[current_vertex] = True37 # Push all unvisited adjacent of popped vertex into the stack38 for adj_veretx in AList[current_vertex]:39 if(not visited[adj_veretx]):40 st.Push(adj_veretx) 41 return(visited)42
43
44AList ={0: [1, 2], 1: [3, 4], 2: [4, 3], 3: [4], 4: []}45print(DFSList(AList,0))Output
xxxxxxxxxx11{0: True, 1: True, 2: True, 3: True, 4: True}
Code Execution Flow
DFS Recursive (without using external stack)
xxxxxxxxxx271# Initialization Function2def DFSInitList(AList):3 (visited,parent) = ({},{})4 for each_vertex in AList.keys():5 visited[each_vertex] = False6 parent[each_vertex] = -17 return(visited,parent)8
9# DFS Recursive Implementation for Adjacency list10def DFSList(AList,visited,parent,v):11 # Mark vertex v as visited vertex12 visited[v] = True13 # Repeat following for each unvisited adjacent of vertex v14 for adj_vertex in AList[v]:15 if (not visited[adj_vertex]):16 # Assign vertex v as parent of each unvisited adjacent of v 17 parent[adj_vertex] = v18 19 # Recursively call the DFS on unvisited adjacent of v20 (visited,parent) = DFSList(AList,visited,parent,adj_vertex)21 22 return(visited,parent)23
24
25AList ={0: [1, 2], 1: [3, 4], 2: [4, 3], 3: [4], 4: []}26v,p = DFSInitList(AList)27print(DFSList(AList,v,p,0))Output
xxxxxxxxxx11({0: True, 1: True, 2: True, 3: True, 4: True}, {0: -1, 1: 0, 2: 0, 3: 1, 4: 3})
Code Execution Flow
DFS global for adjacency list of graph
xxxxxxxxxx281# Global variable2(visited,parent) = ({},{})3
4# Initialization function5def DFSInitListGlobal(AList): 6 for each_vertex in AList.keys():7 visited[each_vertex] = False8 parent[each_vertex] = -19 return10
11# DFS Recursive Implementation for Adjacency list12def DFSListGlobal(AList,v):13 # Mark vertex v as visited vertex14 visited[v] = True15 # Repeat following for each unvisited adjacent of vertex v16 for adj_vertex in AList[v]:17 if (not visited[adj_vertex]):18 # Assign vertex v as parent of each unvisited adjacent of v 19 parent[adj_vertex] = v20 # Recursively call the DFS on unvisited adjacent of v21 DFSListGlobal(AList,adj_vertex) 22 return23
24
25AList ={0: [1, 2], 1: [3, 4], 2: [4, 3], 3: [4], 4: []}26DFSInitListGlobal(AList)27DFSListGlobal(AList,0)28print(visited,parent)Output
xxxxxxxxxx11{0: True, 1: True, 2: True, 3: True, 4: True} {0: -1, 1: 0, 2: 0, 3: 1, 4: 3}
xxxxxxxxxx421# Function to return list of neighbours or adjacent vertex of vertex i2def neighbours(AMat,i):3 nbrs = []4 (rows,cols) = AMat.shape5 for j in range(cols):6 if AMat[i,j] == 1:7 nbrs.append(j)8 return(nbrs)9
10# Initialization Function11def DFSInit(AMat):12 (rows,cols) = AMat.shape13 (visited,parent) = ({},{})14 for each_vertex in range(rows):15 visited[each_vertex] = False16 parent[each_vertex] = -117 return(visited,parent)18
19# DFS Recursive Implementation for Adjacency matrix20def DFS(AMat,visited,parent,v):21 # Mark vertex v as visited vertex22 visited[v] = True23 # Repeat following for each unvisited adjacent of vertex v24 for adj_vertex in neighbours(AMat,v):25 if (not visited[adj_vertex]):26 # Assign vertex v as parent of each unvisited adjacent of v 27 parent[adj_vertex] = v28 # Recursively call the DFS on unvisited adjacent of v29 (visited,parent) = DFS(AMat,visited,parent,adj_vertex)30 31 return(visited,parent)32
33
34V = [0,1,2,3,4]35E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)] 36size = len(V)37import numpy as np38AMat = np.zeros(shape=(size,size))39for (i,j) in E:40 AMat[i,j] = 141v,p=DFSInit(AMat)42print(DFS(AMat,v,p,0))Output
xxxxxxxxxx11({0: True, 1: True, 2: True, 3: True, 4: True}, {0: -1, 1: 0, 2: 0, 3: 1, 4: 3})
Code Execution Flow
DFS global for adjacency matrix of graph
xxxxxxxxxx441# Global variables2(visited,parent) = ({},{})3
4# Function to return list of neighbours or adjacent vertex of vertex i5def neighbours(AMat,i):6 nbrs = []7 (rows,cols) = AMat.shape8 for j in range(cols):9 if AMat[i,j] == 1:10 nbrs.append(j)11 return(nbrs)12
13# Initialization Function14def DFSInitGlobal(AMat):15 (rows,cols) = AMat.shape 16 for each_vertex in range(rows):17 visited[each_vertex] = False18 parent[each_vertex] = -119 return20
21# DFS Recursive Implementation for Adjacency matrix22def DFSGlobal(AMat,v):23 # Mark vertex v as visited vertex24 visited[v] = True25 # Repeat following for each unvisited adjacent of vertex v26 for adj_vertex in neighbours(AMat,v):27 if (not visited[adj_vertex]):28 # Assign vertex v as parent of each unvisited adjacent of v29 parent[adj_vertex] = v30 # Recursively call the DFS on unvisited adjacent of v31 DFSGlobal(AMat,adj_vertex) 32 return33
34
35V = [0,1,2,3,4]36E = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (2, 3), (3, 4)] 37size = len(V)38import numpy as np39AMat = np.zeros(shape=(size,size))40for (i,j) in E:41 AMat[i,j] = 142DFSInitGlobal(AMat)43DFSGlobal(AMat,0)44print(visited,parent)Output
xxxxxxxxxx11{0: True, 1: True, 2: True, 3: True, 4: True} {0: -1, 1: 0, 2: 0, 3: 1, 4: 3}
The Connected Components in a Graph using BFS algorithm is used to find all the connected components in an undirected graph.
Here are the steps for the Connected Components in Graph using BFS algorithm:
Create a queue and an array visited to keep track of the visited nodes in the graph.
Initialize dictionary component[v] = -1 for each vertex v in the graph
For each unvisited node u in the graph, perform a BFS traversal starting from u and assign new component id to all the visited nodes as a single connected component in component dictionary.
Repeat steps 3 until all nodes in the graph are visited.
Return component dictionary.
For given graph

x
1# Queue Implementation2class Queue:3 def __init__(self):4 self.queue = []5 def enqueue(self,v):6 self.queue.append(v)7 def isempty(self):8 return(self.queue == [])9 def dequeue(self):10 v = None11 if not self.isempty():12 v = self.queue[0]13 self.queue = self.queue[1:]14 return(v) 15 def __str__(self):16 return(str(self.queue))17
18# BFS Implementation for Adjacency list19def BFSList(AList,start_vertex):20 visited = {}21 for each_vertex in AList.keys():22 visited[each_vertex] = False23 24 q = Queue() 25 visited[start_vertex] = True26 q.enqueue(start_vertex)27 28 while(not q.isempty()):29 curr_vertex = q.dequeue()30 for adj_vertex in AList[curr_vertex]:31 if (not visited[adj_vertex]):32 visited[adj_vertex] = True33 q.enqueue(adj_vertex) 34 return(visited)35
36# Implementation to find connected components in graph37def Components(AList):38 # Initialization of component value -1 for each vertex39 component = {}40 for each_vertex in AList.keys():41 component[each_vertex] = -1 42 43 # Initialize compid(represent conntected component id)44 # Initialize seen(represent number of visited/checked vertices)45 (compid,seen) = (0,0)46 47 # Repeat the following untill seen value is not equal to number of vertex48 while seen < max(AList.keys()):49 # Find the min level value vertex among all vertices which are not checked or visited50 startv = min([i for i in AList.keys() if component[i] == -1])51 # Call the BFS to check the reachability from startv52 visited = BFSList(AList,startv) 53 # Assign compid value to each reachable vertex from startv and increment seen value54 for vertex in visited.keys():55 if visited[vertex]:56 seen = seen + 157 component[vertex] = compid58 59 # Increment compid by one to check again if any vertex are remaing to visisted 60 compid = compid + 1 61 62 return(component)63
64
65AList = {0: [1], 1: [2], 2: [0], 3: [4, 6], 4: [3, 7], 5: [3, 7], 6: [5], 7: [4, 8], 8: [5, 9], 9: [8]}66print(Components(AList))Output
xxxxxxxxxx11{0: 0, 1: 0, 2: 0, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 1}
The Pre and Post numbering using DFS algorithm is used to assign a unique number to each node in a graph during a Depth-First Search (DFS) traversal.
Here are the steps for the Pre and Post numbering using DFS algorithm:
Initialize a counter count to 0 and two dictionary pre and post to keep track of the pre and post numbers of the nodes.
For each unvisited node u in the graph, perform a DFS traversal starting from u.
During the DFS traversal, visit each unvisited neighbor v of the current node u and recursively perform a DFS traversal starting from v.
Increment the count and assign it as the pre number of the current node u.
After all neighbors of the current node u have been visited, increment the count again and assign it as the post number of the current node u.
Repeat steps 3-5 until all nodes in the graph are visited.
Return the pre and post arrays.
For given graph

xxxxxxxxxx371# Global variables2(visited,pre,post) = ({},{},{})3
4# Initialization5def DFSInitPrePost(AList): 6 for each_vertex in AList.keys():7 visited[each_vertex] = False8 (pre[each_vertex],post[each_vertex]) = (-1,-1)9 return10
11# Implementation of Pre and Post numbering on graph using DFS approch12def DFSListPrePost(AList,v,count):13 # Mark veretx v as visited14 visited[v] = True15 # Assigned the pre numbering for v when we are traversing the vertex(entering the vertex)16 pre[v] = count17 # Increment the count18 count = count + 1 19 # Repeat for each unvisited adjacent of vertex v20 for adj_vertex in AList[v]:21 if (not visited[adj_vertex]):22 # Recursively call the DFSListPrePost on unvisited adjacent of v23 count = DFSListPrePost(AList,adj_vertex,count)24 # Assigned the post numbering for v when return back from recusrive call(leaving the vertex)25 post[v] = count26 # Increment the count27 count = count + 1 28 29 return(count)30
31
32AList = {0: [1, 4],1: [0],2: [],3: [],4: [0, 8, 9],5: [],6: [],7: [],8: [4, 9],9: [8, 4]}33DFSInitPrePost(AList)34print(DFSListPrePost(AList,0,0))35print(visited)36print(pre)37print(post)Output
xxxxxxxxxx41102{0: True, 1: True, 2: False, 3: False, 4: True, 5: False, 6: False, 7: False, 8: True, 9: True}3{0: 0, 1: 1, 2: -1, 3: -1, 4: 3, 5: -1, 6: -1, 7: -1, 8: 4, 9: 5}4{0: 9, 1: 2, 2: -1, 3: -1, 4: 8, 5: -1, 6: -1, 7: -1, 8: 7, 9: 6}
Classifying tree edges in directed graphs using pre and post numbering
Tree edge/forward edge:
Edge (u, v) is tree/forward edge, if Interval [pre(u), post(u)] contains [pre(v), post(v)]
Back edge:
Edge (u, v) is back edge, if Interval [pre(v), post(v)] contains [pre(u), post(u)]
Back edges generate cycles
Cross edge:
Edge (u, v) is cross edge, if Intervals [pre(u), post(u)] and [pre(v), post(v)] are disjoint
A Directed Acyclic Graph(DAG) is a directed graph with no directed cycles. That is, it consists of vertices and edges, with each edge directed from one vertex to another, such that following those directions will never form a closed loop.
Directed acyclic graphs are a natural way to represent dependencies
Arise in many contexts
Pre-requisites between courses for completing a degree
Recipe for cooking
Construction project
Problems to be solved on DAG
Topological sorting
Longest paths
The Topological Sort algorithm is used to sort the nodes of a directed acyclic graph (DAG) in such a way that for every directed edge from node u to node v, node u comes before node v in the sorting order.
Here are the steps for the Topological Sort algorithm:
Compute the in-degree of each node in the graph.
Enqueue all nodes with in-degree 0 to a queue.
While the queue is not empty, dequeue a node from the front of the queue and add it to the sorted list.
For each of the dequeued node's neighbors, decrement their in-degree by 1.
If any of the dequeued node's neighbors now have in-degree 0, enqueue them to the queue.
Repeat steps 3-5 until the queue is empty.
If the graph contains a cycle, the Topological Sort algorithm cannot produce a valid sorting order because it is impossible to order the nodes such that all edges point forward. In this case, the algorithm will terminate with an error or produce an incomplete sorting order.
To keep track of the sorting order, you can add each dequeued node to a list as it is dequeued from the queue.
Complexity is
For given graph(DAG)

xxxxxxxxxx551# Implementaion of Queue2class Queue:3 def __init__(self):4 self.queue = []5 def enqueue(self,v):6 self.queue.append(v)7 def isempty(self):8 return(self.queue == [])9 def dequeue(self):10 v = None11 if not self.isempty():12 v = self.queue[0]13 self.queue = self.queue[1:]14 return(v) 15 def __str__(self):16 return(str(self.queue))17
18# Implementation of Topological sort for Adjacency list19def toposortlist(AList):20 # Initialization21 (indegree,toposortlist) = ({},[])22 zerodegreeq = Queue()23 for u in AList.keys():24 indegree[u] = 025 26 # Compute indegree for each vertex27 for u in AList.keys():28 for v in AList[u]:29 indegree[v] = indegree[v] + 130 31 # Find the vertex with indegree 0 and added into the queue32 for u in AList.keys():33 if indegree[u] == 0:34 zerodegreeq.enqueue(u)35 36 # Topological sort Computing process37 while (not zerodegreeq.isempty()):38 # Remove one vertex from queue which have zero degree vertices39 curr_vertex = zerodegreeq.dequeue() 40 # Store the removed vertex in toposortlist and reduce the indegree by one 41 toposortlist.append(curr_vertex)42 indegree[curr_vertex] = indegree[curr_vertex]-143 44 # Repeat for each adjacent of the removed vertex45 for adj_vertex in AList[curr_vertex]:46 # Reduce the indegree of each adjacent of the removed vertex by 147 indegree[adj_vertex] = indegree[adj_vertex] - 148 # If after reducing the degree of adjacent, it becomes zero then insert it into the queue49 if indegree[adj_vertex] == 0:50 zerodegreeq.enqueue(adj_vertex) 51 52 return(toposortlist)53
54AList={0: [2, 3, 4], 1: [2, 7], 2: [5], 3: [5, 7], 4: [7], 5: [6], 6: [7], 7: []}55print(toposortlist(AList))Output
xxxxxxxxxx11[0, 1, 3, 4, 2, 5, 6, 7]
Code Execution Flow
xxxxxxxxxx371# Implementation of Topological sort for Adjacency matrix2def toposort(AMat):3 #Initialization4 (rows,cols) = AMat.shape5 indegree = {}6 toposortlist = []7 8 #Compute indegree for each vertex9 for c in range(cols):10 indegree[c] = 011 for r in range(rows):12 if AMat[r,c] == 1:13 indegree[c] = indegree[c] + 114 15 # Topological sort Computing process16 for i in range(rows):17 # Select the min level vertex for removing the graph which has indegree 018 j = min([k for k in range(cols) if indegree[k] == 0])19 # Store the removed vertex j in toposortlist and reduce the indegree by one 20 toposortlist.append(j)21 indegree[j] = indegree[j] - 122 23 # Reduce the indegree of each adjacent of the removed vertex j by 124 for k in range(cols):25 if AMat[j,k] == 1:26 indegree[k] = indegree[k] - 127 28 return(toposortlist)29
30
31edges=[(0,2),(0,3),(0,4),(1,2),(1,7),(2,5),(3,5),(3,7),(4,7),(5,6),(6,7)]32size = 833import numpy as np34AMat = np.zeros(shape=(size,size))35for (i,j) in edges:36 AMat[i,j] = 137print(toposort(AMat))Output
xxxxxxxxxx11[0, 1, 2, 3, 4, 5, 6, 7]
The Longest Path in an unweighted Directed Acyclic Graph (DAG) algorithm is used to find the maximum length path in a DAG.
Here are the steps for the Longest Path in an Unweighted DAG algorithm:
Topologically sort the nodes in the graph using the Topological Sort algorithm.
Initialize a distance array with all nodes set to 0 except the source node, which is set to 0.
For each node u in the topologically sorted order, iterate through its incoming edges (v, u) and update the distance of node u as max(dist[u], dist[v] + 1).
The longest path length will be the maximum value in the distance array.
Complexity is
For given graph(DAG)

xxxxxxxxxx561# Implementation of Queue2class Queue:3 def __init__(self):4 self.queue = []5 def enqueue(self,v):6 self.queue.append(v)7 def isempty(self):8 return(self.queue == [])9 def dequeue(self):10 v = None11 if not self.isempty():12 v = self.queue[0]13 self.queue = self.queue[1:]14 return(v) 15 def __str__(self):16 return(str(self.queue))17
18# Implementation Longest path on DAG for Adjacency list19def longestpathlist(AList):20 # Initialization21 (indegree,lpath) = ({},{})22 zerodegreeq = Queue()23 for u in AList.keys():24 (indegree[u],lpath[u]) = (0,0)25 26 # Compute indegree for each vertex27 for u in AList.keys():28 for v in AList[u]:29 indegree[v] = indegree[v] + 130
31 # Find the vertex with indegree 0 and added into the queue32 for u in AList.keys():33 if indegree[u] == 0:34 zerodegreeq.enqueue(u)35 36 # Longest path computing process 37 while (not zerodegreeq.isempty()):38 # Remove one vertex from queue which have zero degree vertices and reduce the indegree by 139 curr_vertex = zerodegreeq.dequeue()40 indegree[curr_vertex] = indegree[curr_vertex] - 141 42 # Repeat for each adjacent of the removed vertex 43 for adj_vertex in AList[curr_vertex]:44 # Reduce the indegree of each adjacent of the removed vertex by 145 indegree[adj_vertex] = indegree[adj_vertex] - 146 # Assign the longest path value for each adjacent of the removed vertex47 lpath[adj_vertex] = max(lpath[adj_vertex],lpath[curr_vertex]+1)48 # If after reducing the degree of adjacent, it becomes zero then insert it into the queue49 if indegree[adj_vertex] == 0:50 zerodegreeq.enqueue(adj_vertex)51 52 return(lpath)53
54
55AList={0: [2, 3, 4], 1: [2, 7], 2: [5], 3: [5, 7], 4: [7], 5: [6], 6: [7], 7: []}56print(longestpathlist(AList))Output
xxxxxxxxxx11{0: 0, 1: 0, 2: 1, 3: 1, 4: 1, 5: 2, 6: 3, 7: 4}Code Execution Flow
Select the BFS/DFS/Topological sort algorithm from left menu to visualize the working.
Source - https://visualgo.net/en/dfsbfs