Shortest Path in Directed Acyclic Graph (DAG) šŸŽÆ

beginner
12 min

Shortest Path in Directed Acyclic Graph (DAG) šŸŽÆ

Welcome to this comprehensive guide on finding the shortest path in a Directed Acyclic Graph (DAG)! In this lesson, we'll explore the intricacies of DAGs, understand why they're essential, and learn how to find the shortest path using algorithms tailored for this graph structure. Let's dive right in!

What is a Directed Acyclic Graph (DAG)? šŸ“

A Directed Acyclic Graph is a graph where there are no directed cycles, meaning you cannot travel from a node back to itself or to any of its ancestors along the edges. In simpler terms, if there's a path from node A to node B, you won't find a path from node B back to node A in a DAG.

Importance of DAGs in Real-world Scenarios šŸ’”

DAGs are crucial in many real-world applications, such as:

  1. Task Scheduling in Operating Systems
  2. Dependency Management in Software Development
  3. Website Link Analysis
  4. Data Pipeline Design

The Shortest Path Problem in DAGs šŸŽÆ

The shortest path problem in DAGs is to find the path with the minimum total edge weight between any two nodes. We'll learn about an efficient algorithm to solve this problem: Topological Sort and Depth-First Search (DFS).

Topological Sort šŸ“

Topological sort is a method for linearizing a DAG by ordering its vertices such that for every directed edge (u, v), vertex u comes before vertex v in the ordering.

Why Topological Sort? šŸ’”

Topological sort helps us determine if a DAG has a cycle and provides a linear ordering of the vertices, which is essential for the next step: Depth-First Search.

Depth-First Search (DFS) šŸ“

Depth-First Search is an algorithm for traversing or searching tree or graph structures. The algorithm starts at the root (or some arbitrary node) and explores as far as possible along each branch before backtracking.

Why DFS? šŸ’”

DFS, when combined with Topological Sort, allows us to find the shortest path in a DAG by building a min-priority queue (or heap) of nodes and popping the minimum-weight node at each step.

Algorithm for Finding Shortest Path in DAG šŸŽÆ

  1. Perform Topological Sort on the given DAG.
  2. Initialize a min-priority queue (or heap) with the sorted nodes.
  3. While the queue is not empty:
    • Pop the minimum-weight node from the queue.
    • If the node is the destination, return the path from the root to the destination.
    • For each adjacent node v of the popped node:
      1. Decrease the weight of v by the weight of the edge connecting the popped node and the adjacent node.
      • If the new weight of v is less than the current minimum weight, update the minimum weight and parent pointer for v.
      • If the new weight of v is 0, it means we've found an alternate shorter path to v, so mark v as visited to prevent cycles.

Code Examples šŸ’»

Here are two complete, working examples in Python:

Example 1 - Simple DAG

python
class Graph: def __init__(self): self.graph = defaultdict(list) def add_edge(self, u, v, w=1): self.graph[u].append((v, w)) def topological_sort_util(self, v, visited, stack): visited[v] = True for neighbour, weight in self.graph[v]: if visited[neighbour] is False: self.topological_sort_util(neighbour, visited, stack) stack.insert(0, v) def topological_sort(self): n = len(self.graph) visited = [False] * n stack = [] for node in range(n): if visited[node] is False: self.topological_sort_util(node, visited, stack) return stack def shortest_path(self, src, dest): stack = self.topological_sort() shortest_path = [] parents = dict() for node in self.graph: parents[node] = None queue = [(0, src)] while queue: distance, current = queue[0] if current == dest: shortest_path = [dest] + shortest_path break for neighbour, weight in self.graph[current]: if parents[neighbour] is None: parents[neighbour] = current new_distance = distance + weight queue.append((new_distance, neighbour)) return shortest_path[::-1] g = Graph() g.add_edge('A', 'B', 4) g.add_edge('A', 'C', 2) g.add_edge('B', 'D', 1) g.add_edge('C', 'D', 2) g.add_edge('C', 'E', 5) g.add_edge('D', 'E', 1) print(g.shortest_path('A', 'E'))

Example 2 - More Complex DAG

python
class Graph: def __init__(self): self.graph = defaultdict(list) def add_edge(self, u, v, w=1): self.graph[u].append((v, w)) def topological_sort_util(self, v, visited, stack): visited[v] = True for neighbour, weight in self.graph[v]: if visited[neighbour] is False: self.topological_sort_util(neighbour, visited, stack) stack.insert(0, v) def topological_sort(self): n = len(self.graph) visited = [False] * n stack = [] for node in range(n): if visited[node] is False: self.topological_sort_util(node, visited, stack) return stack def shortest_path(self, src, dest): stack = self.topological_sort() shortest_path = [] parents = dict() for node in self.graph: parents[node] = None queue = [(0, src)] while queue: distance, current = heapq.heappop(queue) if current == dest: shortest_path = [dest] + shortest_path break for neighbour, weight in self.graph[current]: if parents[neighbour] is None: parents[neighbour] = current new_distance = distance + weight heapq.heappush(queue, (new_distance, neighbour)) return shortest_path[::-1] g = Graph() g.add_edge('A', 'B', 4) g.add_edge('A', 'C', 2) g.add_edge('B', 'D', 1) g.add_edge('C', 'D', 2) g.add_edge('C', 'E', 5) g.add_edge('D', 'E', 1) g.add_edge('F', 'E', 3) g.add_edge('F', 'I', 6) g.add_edge('G', 'F', 2) g.add_edge('G', 'H', 5) g.add_edge('H', 'I', 1) print(g.shortest_path('A', 'I'))

Quiz šŸŽ“

Quick Quiz
Question 1 of 1

What does Topological Sort do in the context of Directed Acyclic Graphs (DAG)?

That's all for this lesson on finding the shortest path in a Directed Acyclic Graph (DAG)! With a solid understanding of DAGs, Topological Sort, and Depth-First Search, you're well on your way to mastering algorithms and data structures. Keep up the great learning! šŸŽ‰