Chapter 37: Graphs (Part 3)

Welcome to Chapter 37 of our comprehensive guide on Graphs, where we'll dive deeper into some advanced graph algorithms. In this chapter, we will cover important topics like Topological Sorting using BFS (Kahn's Algorithm), finding all paths from a source to a target node, and understanding Dijkstra's Algorithm for finding the shortest path. We'll not only explore these concepts theoretically but also provide code implementations and practice questions to solidify your understanding.

Let’s begin our journey through the following topics:

  1. Topological Sorting using BFS (Kahn's Algorithm)

  2. Topological Sort using BFS (Code Implementation)

  3. All Paths from Source to Target

  4. Dijkstra’s Algorithm

  5. Dijkstra’s Algorithm (Code Implementation)


1. Topological Sorting using BFS (Kahn's Algorithm)

Topological sorting is used to order the vertices of a Directed Acyclic Graph (DAG) in such a way that for every directed edge uv, vertex u comes before v. This is particularly useful in tasks like task scheduling, dependency resolution, and compilation.

Kahn's Algorithm is one of the most efficient ways to perform topological sorting using Breadth-First Search (BFS). The algorithm works by:

  • Calculating the in-degree (number of incoming edges) for each vertex.

  • Finding all vertices with an in-degree of zero (no dependencies) and adding them to a queue.

  • Iteratively removing vertices from the queue, reducing the in-degree of adjacent vertices, and adding any new vertices with zero in-degree to the queue.

If the graph is a DAG, this process will provide a valid topological order. If not, it indicates the presence of a cycle.

2. Topological Sort using BFS (Code Implementation)

Here's how you can implement Kahn's Algorithm in Java:

import java.util.*;

public class TopologicalSortBFS {
    public static List<Integer> topologicalSort(int n, List<List<Integer>> adjList) {
        int[] inDegree = new int[n];
        List<Integer> result = new ArrayList<>();
        Queue<Integer> queue = new LinkedList<>();

        // Calculate in-degrees
        for (int i = 0; i < n; i++) {
            for (int neighbor : adjList.get(i)) {
                inDegree[neighbor]++;
            }
        }

        // Add vertices with 0 in-degree to queue
        for (int i = 0; i < n; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        // BFS to determine topological order
        while (!queue.isEmpty()) {
            int node = queue.poll();
            result.add(node);

            for (int neighbor : adjList.get(node)) {
                inDegree[neighbor]--;
                if (inDegree[neighbor] == 0) {
                    queue.offer(neighbor);
                }
            }
        }

        // Check if topological sorting is possible
        if (result.size() != n) {
            System.out.println("The graph contains a cycle.");
            return new ArrayList<>();
        }

        return result;
    }

    public static void main(String[] args) {
        int n = 6; // Number of nodes
        List<List<Integer>> adjList = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            adjList.add(new ArrayList<>());
        }

        // Sample edges for the graph
        adjList.get(5).add(0);
        adjList.get(5).add(2);
        adjList.get(4).add(0);
        adjList.get(4).add(1);
        adjList.get(2).add(3);
        adjList.get(3).add(1);

        List<Integer> topologicalOrder = topologicalSort(n, adjList);
        System.out.println("Topological Order: " + topologicalOrder);
    }
}

3. All Paths from Source to Target

Finding all paths from a source node to a target node is essential in scenarios like navigating from one point to another while considering all possible routes. This problem can be solved using Depth-First Search (DFS) or Backtracking to explore all possible paths from the source node to the target node.

The algorithm involves recursively visiting each node and maintaining a path list that stores the nodes in the current path. If the target node is reached, the path is added to the result list. If not, the algorithm backtracks and continues exploring other routes.

4. Dijkstra’s Algorithm

Dijkstra's Algorithm is used to find the shortest paths from a single source node to all other nodes in a graph with non-negative edge weights. It is a greedy algorithm that picks the node with the smallest tentative distance and explores all its neighbors, updating their distances if a shorter path is found.

The steps involved in Dijkstra’s Algorithm are:

  • Initialize distances from the source to all nodes as infinity, except for the source node itself, which is set to zero.

  • Use a priority queue to explore the node with the smallest distance.

  • Update the distances of neighboring nodes and add them to the queue if a shorter path is found.

5. Dijkstra’s Algorithm (Code Implementation)

Below is a code implementation of Dijkstra's Algorithm in Java:

import java.util.*;

public class DijkstraAlgorithm {
    static class Pair {
        int node;
        int weight;

        Pair(int node, int weight) {
            this.node = node;
            this.weight = weight;
        }
    }

    public static int[] dijkstra(int n, List<List<Pair>> adjList, int source) {
        int[] distances = new int[n];
        Arrays.fill(distances, Integer.MAX_VALUE);
        distances[source] = 0;

        PriorityQueue<Pair> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a.weight));
        pq.offer(new Pair(source, 0));

        while (!pq.isEmpty()) {
            Pair current = pq.poll();
            int currentNode = current.node;

            for (Pair neighbor : adjList.get(currentNode)) {
                int newDist = distances[currentNode] + neighbor.weight;
                if (newDist < distances[neighbor.node]) {
                    distances[neighbor.node] = newDist;
                    pq.offer(new Pair(neighbor.node, newDist));
                }
            }
        }

        return distances;
    }

    public static void main(String[] args) {
        int n = 5; // Number of nodes
        List<List<Pair>> adjList = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            adjList.add(new ArrayList<>());
        }

        // Sample graph edges
        adjList.get(0).add(new Pair(1, 2));
        adjList.get(0).add(new Pair(2, 4));
        adjList.get(1).add(new Pair(2, 1));
        adjList.get(1).add(new Pair(3, 7));
        adjList.get(2).add(new Pair(4, 3));
        adjList.get(3).add(new Pair(4, 1));

        int source = 0;
        int[] shortestPaths = dijkstra(n, adjList, source);

        System.out.println("Shortest paths from node " + source + ": " + Arrays.toString(shortestPaths));
    }
}

Practice Questions

  1. Topological Sorting: Given a Directed Acyclic Graph (DAG), find the topological order using Kahn's Algorithm. Validate whether your solution handles cycles correctly.

  2. Shortest Path Calculation: Implement Dijkstra's Algorithm for a graph with 100 nodes and test with different edge weights and structures.

  3. All Paths Problem: Write a function that finds all paths from a source to a target node in a DAG and print them.


By mastering these topics, you'll gain deeper insights into graph algorithms and their practical applications. Stay tuned for the next chapter as we continue to explore the fascinating world of graphs! Happy coding!