Python

How to Implement Graph Algorithms in Python

Python implementations for graph algorithms represent an essential skill set in problem-solving which are applicable to different practical scenarios. Graphs that comprise of nodes and edges serve as strong data structures and efficiently capture the complex relationships. This guide explores the principles and strategies for Python-based graph algorithm implementation. It provides an example for developers of all fields and gives knowledge of constructing the graphs, mastering the efficient methods, and deploying critical algorithms such as breadth-first search (BFS), depth-first search (DFS), and Dijkstra’s algorithm.

Implementing Graph Algorithms in Python

Graph is recognized as the mathematical data structure representation of objects, e.g., nodes connected by relationships, and edges. Graphs are commonly used to model and craft the real-world scenarios like social networks, recommendation engines, and transportation systems. They consist of two main components, i.e. nodes as vertices and edges as connections.

We must comprehend the many sorts of graphs and their properties in order to deal with them successfully. Depending on our problem, we may work with undirected or directed graphs, weighted or unweighted edges, and cyclic or acyclic structures. Each type of graph has unique properties that affect the choice of algorithms and data structures that are used in our implementation. Before we move to graph algorithms, we need to choose an appropriate representation for our graph. Two common representations for the graph are the adjacency matrix and the list (adjacency).

class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = [[0] * vertices for _ in range(vertices)]

 
In this code, we initialize the graph with a given number of vertices and create an empty adjacency matrix filled with zeros. The total number of vertices, a parameter that is supplied to the “__init__” method, determines the dimension of the matrix.

The alternative approach uses the adjacency lists which are the collections of lists or dictionaries in which each vertex is listed together with its surrounding vertices. For sparse graphs—those with fewer edges—this form uses less memory than the adjacency matrix.

class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = defaultdict(list)

    def add_edge(self, u, v):
        self.graph[u].append(v)

 
Here, we initialize the graph with a given number of vertices and create an empty “defaultdict” where each vertex maps to a list of its neighbors. We can add the edges to the graph by employing the “add_edge” method. Using this representation, we can efficiently store and access the information about the node connections without needing a dense matrix.

After having an insight into the graph representation method, we now need to know the graph traversal algorithms like Depth-First Search (DFS), Breadth-First Search (BFS), and Dijkstra’s Algorithm. DFS is an example of a traversal algorithm graph that explores as much of every branch as feasible before reversing the path. Recursion, as well as a stack data structure, is used in its implementation. Using DFS, it is possible to find every vertex in a graph that can be accessed from a specific source vertex. Here’s the Python code for DFS:

from collections import defaultdict
class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = defaultdict(list)

    def add_edge(self, u, v):
        self.graph[u].append(v)

    def dfs(self, v, visited):
        visited[v] = True
        print(v, end=' ')

        for i in self.graph[v]:
            if not visited[i]:
                self.dfs(i, visited)
g = Graph(5)
g.add_edge(0, 1)
g.add_edge(0, 2)
g.add_edge(1, 3)
g.add_edge(2, 4)
print("DFS starting from vertex 0:")
g.dfs(0, [False] * g.V)

 
In this example, we define a “dfs” function within a “Graph” class. The “dfs” function takes three parameters: “self” which represents the graph instance, “v” which represents the current vertex that we want to explore, and “visited” which is a Boolean list that keeps track of the visited vertices. The DFS algorithm is started by marking the current vertex as visited (`visited[v] = True`) and printing its value.

It then recursively explores all unvisited neighbors of the current vertex by calling the “self.dfs(i, visited)” method for each “”” neighbor. This procedure keeps going until all vertices that can be reached are examined and printed. The “dfs” function is a crucial component of the DFS traversal which helps us explore a graph’s vertices and understand its structure.


Another algorithm for traversing a graph is BFS which analyses every vertex at the current level before proceeding to the next level. It is implemented using a queue data structure and is often used to find the shortest path in an unweighted graph. The Python code for BFS is given as follows:

from collections import deque
from collections import defaultdict

class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = defaultdict(list)

    def add_edge(self, u, v):
        self.graph[u].append(v)

    def bfs(self, start):
        visited = [False] * self.V
        queue = deque()

        visited[start] = True
        queue.append(start)

        while queue:
            node = queue.popleft()
            print(node, end=' ')

            for neighbor in self.graph[node]:
                if not visited[neighbor]:
                    visited[neighbor] = True
                    queue.append(neighbor)

g = Graph(5)
g.add_edge(0, 1)
g.add_edge(0, 2)
g.add_edge(1, 3)
g.add_edge(2, 4)
print("BFS starting from vertex 0:")
g.bfs(0)

 
In this scenario, the BFS algorithm is implemented in the “Graph” class using the “deque” data structure that is obtained from the “collections” module. The “bfs” function always starts by initializing two important data structures: “visited” which is a Boolean list that tracks the visited vertices and “queue” which is a double-ended queue deque that stores the vertices that we want to explore.

We begin with the BFS traversal from a specified starting vertex which is “start”. We mark it as visited (visited[start] = True) and enclose it in the “queue”. We also utilize the main loop which
individually processes the vertices in the “queue”.

For each vertex, we dequeue it with the “node = queue.popleft()” method, print its value, and then enqueue its unvisited neighbors by setting the “visited[neighbor] = True” and the queue.append(neighbor) methods. Every level’s vertices are explored by the BFS algorithm which also makes sure that we examine every vertex at a level before proceeding to the next level. This property makes BFS suitable for finding the shortest path in unweighted graphs and other applications that require a breadth-first exploration.


The final shortest path algorithm, Dijkstra’s algorithm, is now presented. It is used to determine the smallest path among any two nodes in a weighted graph. This operates by keeping a priority queue of vertices and repeatedly choosing the vertex that is closest to the source vertex.

import heapq
class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = [[0] * vertices for _ in range(vertices)]

    def add_edge(self, u, v, w):
        self.graph[u][v] = w  # Set the weight for the edge (u, v)
        self.graph[v][u] = w  # Set the weight for the reverse edge (v, u)

    def dijkstra(self, src):
        distances = [float('inf')] * self.V
        distances[src] = 0

        priority_queue = [(0, src)]

        while priority_queue:
            dist, node = heapq.heappop(priority_queue)

            for neighbor, weight in enumerate(self.graph[node]):
                if weight > 0 and dist + weight < distances[neighbor]:
                    distances[neighbor] = dist + weight
                    heapq.heappush(priority_queue, (distances[neighbor], neighbor))
   
        return distances

g = Graph(5)
g.add_edge(0, 1, 4)  # Edge from 0 to 1 with weight 4
g.add_edge(0, 2, 1)  # Edge from 0 to 2 with weight 1
g.add_edge(1, 3, 1)  # Edge from 1 to 3 with weight 1
g.add_edge(2, 4, 3)  # Edge from 2 to 4 with weight 3

print("Shortest paths from vertex 0 using Dijkstra's algorithm:")
distances = g.dijkstra(0)
print(distances)

 
In the “Graph” class, we define a “dijkstra” function for this example. The “dijkstra” function also takes in two input parameters. The first parameter is “self” which represent the graph instance, and the second parameter is “src” which specifies the source vertex from which we want to find the shortest paths to all other vertices. Dijkstra’s algorithm works by initializing two important data structures: “distances” which is a list that stores the minimum distance from the source vertex to every vertex and “priority_queue” which is a priority queue that keeps track of vertices that we need to explore.

Then, we set all distances to infinity “float(‘inf’)” except for the source vertex which would be equal  to 0 “distances[src] = 0”. We also use the main loop here that continues as long as there are vertices in the “priority_queue”.

In every iteration, we extract the vertex with the smallest distance by calling the “node = heapq.heappop(priority_queue)” method and analyze its neighbors. For each neighbor, if the path through the current vertex is shorter than the previously known distance, we update the distance and put the neighbor in the “priority_queue”.


The “distances” list at the end of Dijkstra’s methods is guaranteed to contain the shortest paths between the source vertex and all other vertices in the graph.

Conclusion

To conclude, the provided explanation provides a thorough understanding of graph algorithms in Python including the concepts, data structures, and step-by-step descriptions of each function. We can utilize these skills to implement and apply the graph algorithms to various real-world problems, enhancing our ability to tackle the complex computer science and data science challenges.

About the author

Kalsoom Bibi

Hello, I am a freelance writer and usually write for Linux and other technology related content