Graphs & Algorithms
Graphs & Algorithms
Graph Data Structure is a collection of nodes connected by edges. It’s used to represent
relationships between different entities. Graph algorithms are methods used to manipulate and
analyze graphs, solving various problems like finding the shortest path or detecting cycles.
Components of a Graph:
Vertices: Vertices are the fundamental units of the graph. Sometimes, vertices are also known as
vertex or nodes. Every node/vertex can be labeled or unlabeled.
Edges: Edges are drawn or used to connect two nodes of the graph. It can be ordered pair of
nodes in a directed graph. Edges can connect any two nodes in any possible way. There are no
rules. Sometimes, edges are also known as arcs. Every edge can be labelled/unlabelled.
Operations on Graphs:
Basic Operations:
Insertion of Nodes/Edges in the graph – Insert a node into the graph.
Deletion of Nodes/Edges in the graph – Delete a node from the graph.
Searching on Graphs – Search an entity in the graph.
Traversal of Graphs – Traversing all the nodes in the graph.
More Operations:
Shortest Paths : From a source to a destination, a source to all other nodes and between all pairs.
Minimum Spanning Tree : In a weighted, connected undirected graph, finding the minimum weight
edges to connect all.
1. Graph Representation
Graphs can be represented in various ways, with the two most common being adjacency matrices and
adjacency lists.
a. Adjacency Matrix
Structure: A 2D array where cell (i,j)(i, j)(i,j) indicates the presence (1) or absence (0) of an edge
between vertex iii and vertex jjj.
Space Complexity: O(V2)O(V^2)O(V2), where VVV is the number of vertices.
Usage: Efficient for dense graphs.
Figure:
A B C D
A 0 1 1 0
B 1 0 0 1
C 1 0 0 1
D 0 1 1 0
b. Adjacency List
Structure: An array (or list) where each index represents a vertex and contains a list of adjacent
vertices.
Space Complexity: O(V+E)O(V + E)O(V+E), where EEE is the number of edges.
Usage: Efficient for sparse graphs.
Figure:
A: [B, C]
B: [A, D]
C: [A, D]
D: [B, C]
Algorithms:
a. Kruskal’s Algorithm
In Kruskal’s algorithm, sort all edges of the given graph in increasing order. Then it keeps on adding
new edges and nodes in the MST if the newly added edge does not form a cycle. It picks the
minimum weighted edge at first and the maximum weighted edge at last. Thus we can say that it
makes a locally optimal choice in each step in order to find the optimal solution. Hence this is
a Greedy Algorithm.
How to find MST using Kruskal’s algorithm?
Below are the steps for finding MST using Kruskal’s algorithm:
1. Sort all the edges in non-decreasing order of their weight.
2. Pick the smallest edge. Check if it forms a cycle with the spanning tree formed so far. If the cycle
is not formed, include this edge. Else, discard it.
3. Repeat step#2 until there are (V-1) edges in the spanning tree.
b. Prim’s Algorithm
Prim's Algorithm is a greedy algorithm that is used to find the minimum spanning tree from a graph. Prim's algorithm finds
the subset of edges that includes every vertex of the graph such that the sum of the weights of the edges can be minimized.
Prim's algorithm starts with the single node and explores all the adjacent nodes with all the connecting edges at every step.
The edges with the minimal weights causing no cycles in the graph got selected.
Prim's algorithm is a greedy algorithm that starts from one vertex and continue to add the edges with the smallest weight
until the goal is reached. The steps to implement the prim's algorithm are given as follows -
Now, let's see the working of prim's algorithm using an example. It will be easier to understand the prim's algorithm using an
example.
Cost of MST = 4 + 2 + 1 + 3 = 10 units.
Algorithm
class Graph:
def __init__(self, vertices):
self.V = vertices
self.graph = [[0] * vertices for _ in range(vertices)]
for v in range(self.V):
if key[v] < min_value and not mst_set[v]:
min_value = key[v]
min_index = v
return min_index
def prim_mst(self):
key = [sys.maxsize] * self.V
parent = [-1] * self.V
key[0] = 0
mst_set = [False] * self.V
for _ in range(self.V):
u = self.min_key(key, mst_set)
mst_set[u] = True
for v in range(self.V):
if (0 < self.graph[u][v] < key[v]) and not mst_set[v]:
key[v] = self.graph[u][v]
parent[v] = u
# Example usage
if __name__ == "__main__":
g = Graph(5)
g.add_edge(0, 1, 2)
g.add_edge(0, 3, 6)
g.add_edge(1, 2, 3)
g.add_edge(1, 3, 8)
g.add_edge(1, 4, 5)
g.add_edge(2, 4, 7)
g.add_edge(3, 4, 9)
a. Dijkstra’s Algorithm
Find Shortest Paths from Source to all Vertices using Dijkstra’s Algorithm.
Given a weighted graph and a source vertex in the graph, find the shortest paths from the source to
all the other vertices in the given graph.
Note: The given graph does not contain any negative edge.
Examples:
Output: 0 4 12 19 21 11 9 8 14
Explanation: The distance from 0 to 1 = 4.
The minimum distance from 0 to 2 = 12. 0->1->2
The minimum distance from 0 to 3 = 19. 0->1->2->3
The minimum distance from 0 to 4 = 21. 0->7->6->5->4
The minimum distance from 0 to 5 = 11. 0->7->6->5
The minimum distance from 0 to 6 = 9. 0->7->6
The minimum distance from 0 to 7 = 8. 0->7
The minimum distance from 0 to 8 = 14. 0->1->2->8
b. Bellman-Ford Algorithm
Bellman ford algorithm is a single-source shortest path algorithm. This algorithm is used to find the shortest distance from
the single vertex to all the other vertices of a weighted graph. There are various other algorithms used to find the shortest
path like Dijkstra algorithm, etc.
Imagine you have a map with different cities connected by roads, each road having a certain
distance. The Bellman–Ford algorithm is like a guide that helps you find the shortest path
from one city to all other cities, even if some roads have negative lengths. It’s like a GPS for
computers, useful for figuring out the quickest way to get from one point to another in a
network.
As we can observe in the above graph that some of the weights are negative. The above graph contains 6
vertices so we will go on relaxing till the 5 vertices. Here, we will relax all the edges 5 times. The loop will
iterate 5 times to get the correct answer. If the loop is iterated more than 5 times then also the answer will be
the same, i.e., there would be no change in the distance between the vertices.
How it works:
1. Set initial distance to zero for the source vertex, and set initial distances to infinity for all other
vertices.
2. For each edge, check if a shorter distance can be calculated, and update the distance if the
calculated distance is shorter.
3. Check all edges (step 2) V−1�−1 times. This is as many times as there are vertices (V�), minus
one.
4. Optional: Check for negative cycles.
Manual Run Through
The Bellman-Ford algorithm is actually quite straight forward, because it checks all edges, using the
adjacency matrix. Each check is to see if a shorter distance can be made by going from the vertex on one
side of the edge, via the edge, to the vertex on the other side of the edge.
And this check of all edges is done V−1�−1 times, with V� being the number of vertices in the graph.
This is how the Bellman-Ford algorithm checks all the edges in the adjacency matrix in our graph 5-1=4
times:
The first four edges that are checked in our graph are A->C, A->E, B->C, and C->A. These first four edge
checks do not lead to any updates of the shortest distances because the starting vertex of all these edges
has an infinite distance.
After the edges from vertices A, B, and C are checked, the edges from D are checked. Since the starting
point (vertex D) has distance 0, the updated distances for A, B, and C are the edge weights going out from
vertex D.
The next edges to be checked are the edges going out from vertex E, which leads to updated distances for
vertices B and C.
The Bellman-Ford algorithm have now checked all edges 1 time. The algorithm will check all edges 3 more
times before it is finished, because Bellman-Ford will check all edges as many times as there are vertices in
the graph, minus 1.
The algorithm starts checking all edges a second time, starting with checking the edges going out from
vertex A. Checking the edges A->C and A->E do not lead to updated distances.
The next edge to be checked is B->C, going out from vertex B. This leads to an updated distance from vertex
D to C of 5-4=1.
Checking the next edge C->A, leads to an updated distance 1-3=-2 for vertex A.
The check of edge C->A in round 2 of the Bellman-Ford algorithm is actually the last check that leads to an
updated distance for this specific graph. The algorithm will continue to check all edges 2 more times without
updating any distances.
Checking all edges V−1�−1 times in the Bellman-Ford algorithm may seem like a lot, but it is done this
many times to make sure that the shortest distances will always be found.
Sample Program (Dijkstra’s Algorithm)
python
Copy code
import heapq
class Graph:
def __init__(self, vertices):
self.V = vertices
self.graph = {i: [] for i in range(vertices)}
while pq:
current_distance, current_vertex = heapq.heappop(pq)
# Example usage
if __name__ == "__main__":
g = Graph(5)
g.add_edge(0, 1, 10)
g.add_edge(0, 2, 3)
g.add_edge(1, 2, 1)
g.add_edge(1, 3, 2)
g.add_edge(2, 1, 4)
g.add_edge(2, 3, 8)
g.add_edge(2, 4, 2)
g.add_edge(3, 4, 7)
g.add_edge(4, 3, 9)
g.dijkstra(0)
a. Floyd-Warshall Algorithm
Figure:
Use a matrix to show the distances between all pairs of vertices before and after applying the
algorithm.
def floyd_warshall(self):
dist = self.graph
for k in range(self.V):
for i in range(self.V):
for j in range(self.V):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
for i in range(self.V):
for j in range(self.V):
if dist[i][j] == float('inf'):
print(f"Distance from {i} to {j}: INF")
else:
print(f"Distance from {i} to {j}: {dist[i][j]}")
# Example usage
if __name__ == "__main__":
g = Graph(4)
g.add_edge(0, 1, 5)
g.add_edge(0, 3, 10)
g.add_edge(1, 2, 3)
g.add_edge(2, 3, 1)
a. Ford-Fulkerson Method
Figure:
Create a directed graph and illustrate flow values on the edges, showing how the flow increases with
each augmenting path.
class Graph:
def __init__(self, vertices):
self.V = vertices
self.graph = [[0] * vertices for _ in range(vertices)]
while queue:
u = queue.popleft()
for v