Skip to main content
โšก Calmops

Graph Algorithms: Traversals, Shortest Paths, and Applications

Introduction

From finding the shortest route on maps to detecting social network connections, graphs are incredibly versatile. Understanding graph algorithms is essential for solving many real-world problems efficiently.

This article covers fundamental graph algorithms, their implementations, and practical applications.

Graph Representations

How you represent a graph affects algorithm performance:

Adjacency List:

graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D'],
    'C': ['A', 'D'],
    'D': ['B', 'C']
}
  • Memory efficient for sparse graphs: O(V + E)
  • Good for traversals
  • Better cache for large graphs

Adjacency Matrix:

matrix = [
    [0, 1, 1, 0],  # A connects to B, C
    [1, 0, 0, 1],  # B connects to A, D
    [1, 0, 0, 1],  # C connects to A, D
    [0, 1, 1, 0]   # D connects to B, C
]
  • O(1) edge lookup
  • O(Vยฒ) space
  • Good for dense graphs

Breadth-First Search (BFS)

Explores level by levelโ€”visit all neighbors before going deeper.

from collections import deque

def bfs(graph, start):
    visited = {start}
    queue = deque([start])
    result = []
    
    while queue:
        node = queue.popleft()
        result.append(node)
        
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    
    return result

Properties:

  • Time: O(V + E)
  • Space: O(V)
  • Finds shortest path in unweighted graphs
  • Uses queue data structure

Applications:

  • Shortest path in unweighted graphs
  • Level-order tree traversal
  • Finding connected components
  • Social network “degrees of separation”

Depth-First Search (DFS)

Explores as deep as possible before backtracking.

def dfs(graph, start, visited=None):
    if visited is None:
        visited = set()
    
    visited.add(start)
    result = [start]
    
    for neighbor in graph[start]:
        if neighbor not in visited:
            result.extend(dfs(graph, neighbor, visited))
    
    return result

# Iterative version
def dfs_iterative(graph, start):
    visited = set()
    stack = [start]
    result = []
    
    while stack:
        node = stack.pop()
        if node not in visited:
            visited.add(node)
            result.append(node)
            stack.extend(reversed(graph[node]))
    
    return result

Properties:

  • Time: O(V + E)
  • Space: O(V) for recursion stack
  • Uses stack (or recursion)

Applications:

  • Detecting cycles
  • Topological sorting
  • Finding strongly connected components
  • Maze solving
  • Path finding

Topological Sort

Linear ordering of vertices where all edges point from earlier to later.

Kahn’s Algorithm (BFS-based):

from collections import deque, defaultdict

def topological_sort(num_courses, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * num_courses
    
    for dest, src in prerequisites:
        graph[src].append(dest)
        in_degree[dest] += 1
    
    queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
    result = []
    
    while queue:
        node = queue.popleft()
        result.append(node)
        
        for neighbor in graph[node]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
    
    return result if len(result) == num_courses else []

Applications:

  • Course scheduling
  • Build systems (dependency resolution)
  • Task scheduling
  • Instruction ordering

Shortest Path Algorithms

Dijkstra’s Algorithm

Finds shortest path from single source to all other vertices in weighted graphs with non-negative weights.

import heapq

def dijkstra(graph, start):
    dist = {vertex: float('inf') for vertex in graph}
    dist[start] = 0
    pq = [(0, start)]
    visited = set()
    
    while pq:
        current_dist, current = heapq.heappop(pq)
        
        if current in visited:
            continue
        visited.add(current)
        
        for neighbor, weight in graph[current]:
            if current_dist + weight < dist[neighbor]:
                dist[neighbor] = current_dist + weight
                heapq.heappush(pq, (dist[neighbor], neighbor))
    
    return dist

Properties:

  • Time: O((V + E) log V) with binary heap
  • Space: O(V)
  • Does NOT work with negative weights

Bellman-Ford

Handles negative weights, detects negative cycles.

def bellman_ford(num_vertices, edges, source):
    dist = [float('inf')] * num_vertices
    dist[source] = 0
    
    # Relax edges V-1 times
    for _ in range(num_vertices - 1):
        for u, v, w in edges:
            if dist[u] != float('inf') and dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
    
    # Check for negative cycles
    for u, v, w in edges:
        if dist[u] != float('inf') and dist[u] + w < dist[v]:
            return None  # Negative cycle detected
    
    return dist

Minimum Spanning Tree (MST)

Connects all vertices with minimum total edge weight, no cycles.

Prim’s Algorithm

Like Dijkstra but for MST.

def prim(graph, start):
    visited = {start}
    edges = [(weight, start, neighbor) 
             for neighbor, weight in graph[start]]
    heapq.heapify(edges)
    total_weight = 0
    
    while edges:
        weight, u, v = heapq.heappop(edges)
        
        if v in visited:
            continue
        
        visited.add(v)
        total_weight += weight
        
        for neighbor, w in graph[v]:
            if neighbor not in visited:
                heapq.heappush(edges, (w, v, neighbor))
    
    return total_weight

Kruskal’s Algorithm

Uses Union-Find (Disjoint Set) for efficiency.

def kruskal(num_vertices, edges):
    # Sort edges by weight
    edges = sorted(edges, key=lambda x: x[2])
    
    parent = list(range(num_vertices))
    rank = [0] * num_vertices
    
    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]
    
    def union(x, y):
        px, py = find(x), find(y)
        if px == py:
            return False
        if rank[px] < rank[py]:
            px, py = py, px
        parent[py] = px
        if rank[px] == rank[py]:
            rank[px] += 1
        return True
    
    mst_weight = 0
    edges_used = 0
    
    for u, v, w in edges:
        if union(u, v):
            mst_weight += w
            edges_used += 1
            if edges_used == num_vertices - 1:
                break
    
    return mst_weight

Real-World Applications

  • Navigation: Shortest path in road networks
  • Social networks: Friend recommendations, degrees of separation
  • Web page ranking: PageRank algorithm
  • Package delivery: Vehicle routing problems
  • Network design: Minimum cost to connect cities

External Resources

Conclusion

Graph algorithms are fundamental tools in a programmer’s toolkit. BFS and DFS provide basic traversal, while Dijkstra and MST algorithms solve optimization problems. Understanding when and how to apply these algorithms opens solutions to countless practical problems.

Comments