Shortest Path In Directed Acyclic Graph
#include <bits/stdc++.h> // Includes common standard libraries like iostream, unordered_map, list, stack, vector, limits.using namespace std; // Use the standard namespace.
// Recursive DFS helper function for Topological Sort.// This generates a topological ordering of nodes, pushed onto a stack.// 'adjList': The adjacency list of the graph (nodes point to {neighbor, weight}).// 'st': A stack where nodes are pushed after all their descendants are visited.// 'visited': A vector to keep track of visited nodes globally.// 'node': The current node being visited.void solveDFS(unordered_map<int, list<pair<int, int>>> &adjList, stack<int> &st, vector<int> &visited, int node) { visited[node] = 1; // Mark the current node as visited.
// Iterate through all neighbors of the current node. // 'neigh.first' is the neighbor node, 'neigh.second' is the weight to that neighbor. for(pair<int, int> neigh : adjList[node]) { // If the neighbor has not been visited, recursively call DFS on it. if(visited[neigh.first] == 0) { solveDFS(adjList, st, visited, neigh.first); } }
// After all descendants of 'node' have been visited and pushed to the stack, // push 'node' itself onto the stack. This ensures topological order when popped. st.push(node);}
// Function to perform Topological Sort using DFS.// 'adjList': The adjacency list of the graph.// 'n': Total number of nodes.// Returns a stack containing nodes in topological order.stack<int> topoSort(unordered_map<int, list<pair<int, int>>> adjList, int n) { stack<int> st; // Stack to store the topological order. vector<int> visited(n, 0); // Visited array, initialized to 0 (not visited).
// Iterate through all nodes to handle disconnected components. // Assuming nodes are 0-indexed. for(int i = 0; i < n; i++) { // Iterate through node IDs (0 to n-1). // Check if the node is present in adjList and is unvisited. // The condition `adjList.count(i)` ensures we only try to process nodes that actually exist in the graph. // If node IDs are guaranteed 0 to n-1 without gaps, `adjList.count(i)` isn't strictly necessary. if (adjList.count(i) || !adjList[i].empty() || i < n) { // Simplified check for graph nodes. if(visited[i] == 0) { solveDFS(adjList, st, visited, i); // Start DFS from unvisited node. } } } // Note: If nodes can have gaps or start from 1, the iteration should be more robust. // The current `adjList` uses `unordered_map`, which handles arbitrary integer keys. // The `visited` vector of size `n` implies 0 to n-1 indexing. // If node IDs can be sparse, `visited` should also be `unordered_map<int, bool>`.
return st; // Return the stack with topological order.}
// Main function to find shortest paths from a source in a DAG.// 'edges': A vector of vectors, where each inner vector {u, v, w} represents a directed edge u -> v with weight w.// 'n': Total number of nodes.// 'm': Total number of edges.// 'src': The source node.// Returns a vector containing the shortest distances from 'src' to all other nodes.vector<int> shortestPath(vector<vector<int>> &edges, int n, int m, int src) { // 1. Create Adjacency List from the weighted directed edges. unordered_map<int, list<pair<int, int>>> adjList; for(int i = 0; i < m; i++) { int u = edges[i][0]; // Source node. int v = edges[i][1]; // Destination node. int w = edges[i][2]; // Weight of the edge. adjList[u].push_back({v, w}); // Directed edge u -> v with weight w. }
// 2. Perform Topological Sort to get the processing order. // The topoSort function iterates all nodes from 0 to n-1. stack<int> st = topoSort(adjList, n);
// Optional: Print topological sort (for debugging/understanding). stack<int> temp = st; // Create a copy to print without modifying original stack. cout << "Topological Sort Order: "; vector<int> topo_order_vec; while(!temp.empty()) { topo_order_vec.push_back(temp.top()); temp.pop(); } reverse(topo_order_vec.begin(), topo_order_vec.end()); // Reverse to print in correct topo order for(int node_val : topo_order_vec) { cout << node_val << " "; } cout << endl; // Original code printed from stack directly, which is reverse of topological order. // `while(!temp.empty()) { cout << temp.top() << " "; temp.pop(); } cout << endl;`
// 3. Initialize distance vector. // `INT_MAX` is used to represent infinity (unreachable nodes). vector<int> distance(n, INT_MAX); distance[src] = 0; // Distance from source to itself is 0.
// 4. Process nodes in topological order and relax edges. while(!st.empty()) { int node = st.top(); // Get the next node from the topological sort. st.pop(); // Pop it from the stack.
// Only process if the current node is reachable from the source. if(distance[node] != INT_MAX) { // Iterate through all neighbors of the current node. for(auto neighbor_pair : adjList[node]) { int neighbor = neighbor_pair.first; int weight = neighbor_pair.second;
// Relaxation step: If a shorter path to 'neighbor' is found. // distance[node] + weight: Distance from source to 'node' + weight of edge 'node -> neighbor'. // distance[neighbor]: Current shortest distance from source to 'neighbor'. if(distance[node] + weight < distance[neighbor]) { distance[neighbor] = distance[node] + weight; // Update shortest distance. } } } }
return distance; // Return the vector of shortest distances.}
int main() { vector<vector<int>> edges; // Vector to store input edges {u, v, w}. int n, m, src; // 'n' for nodes, 'm' for edges, 'src' for source node.
cout << "Enter the number of nodes : "; cin >> n;
cout << "Enter the number of edges : "; cin >> m;
cout << "Enter the edges (u v w, meaning u -> v with weight w): " << endl; for(int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; // Read source, destination, and weight. edges.push_back({u, v, w}); // Add edge to vector. }
cout << "Enter the source : "; cin >> src;
// Call shortestPath function. vector<int> answer_distances = shortestPath(edges, n, m, src);
cout << "Shortest distances from source " << src << ":" << endl; for(int i = 0; i < n; i++) { cout << "Node " << i << ": "; if(answer_distances[i] == INT_MAX) { cout << "Unreachable" << endl; } else { cout << answer_distances[i] << endl; } }
return 0; // Indicate successful execution.}1. Problem Statement
- Goal: Find the shortest path (minimum total weight) from a given
sourcenode to all other reachable nodes in a Weighted Directed Acyclic Graph (DAG). - Key Constraint: This algorithm works only for DAGs. It does not work for graphs with cycles, especially those with negative weight cycles.
- Input: List of weighted directed
edges (u, v, w), totalnnodes, totalmedges,src(source node). - Output: A vector
distancewheredistance[i]is the shortest path distance fromsrcto nodei.INT_MAXindicates unreachable.
2. Core Idea
The shortest path in a DAG can be found efficiently by combining:
- Topological Sort: First, compute a topological ordering of the graph’s nodes. This ensures that when we process a node, all its prerequisites (nodes that must come before it in any path) have already been processed.
- Single Pass Relaxation: Iterate through the nodes in topological order. For each node, relax all its outgoing edges. “Relaxing an edge”
u -> vwith weightwmeans updatingdistance[v]ifdistance[u] + wis less than the currentdistance[v].
3. Algorithm Steps
A. Topological Sort (DFS-based)
solveDFSfunction:- Parameters:
adjList,st(a stack),visitedarray,node. - Mark
visited[node] = true. - Recursively call
solveDFSfor all unvisited neighbors. - Crucial: After all neighbors and their descendants are processed, push
nodeontost.
- Parameters:
topoSortfunction:- Builds the
adjListfrom input (if not already built). - Initializes
visitedarray and an emptystack st. - Iterates through all nodes (
0ton-1). If!visited[node], callssolveDFSto start DFS for that component. - Returns the populated
stack st.
- Builds the
B. Shortest Path Calculation (shortestPath function)
- Build Adjacency List: Create
adjListfrom the inputedges. For weighted directed edgesu -> vwith weightw, storevandw(e.g.,adjList[u].push_back({v, w})). - Perform Topological Sort: Call
topoSort(adjList, n)to get the topological order in a stack. - Initialize Distance Array:
- Create
vector<int> distance(n, INT_MAX). - Set
distance[src] = 0. All other nodes are initially unreachable (INT_MAX).
- Create
- Process Nodes in Topological Order:
- While the topological sort stack
stis not empty:node = st.top(); st.pop();(Get the next node in topological order).- Important Check: If
distance[node] != INT_MAX(meaningnodeis reachable fromsrc):- For each
neighbor (x.first)and itsweight (x.second)fromadjList[node]:- Relaxation Step: If
distance[node] + x.second < distance[x.first]:distance[x.first] = distance[node] + x.second;(Update the shortest distance tox.first).
- Relaxation Step: If
- For each
- While the topological sort stack
- Return: The
distancevector.
4. Time and Space Complexity
- Time Complexity:
O(V + E)- Building
adjList:O(E). - Topological Sort (DFS): Each vertex and edge visited once.
O(V + E). - Distance initialization:
O(V). - Relaxation step: Each vertex is popped once. For each vertex, its outgoing edges are iterated. Each edge is relaxed exactly once.
O(V + E). - Total:
O(V + E).
- Building
- Space Complexity:
O(V + E)adjList:O(V + E).visitedvector:O(V).stackfor topological sort:O(V).distancevector:O(V).
5. Why it works for DAGs and not general graphs
- In a DAG, the topological sort guarantees that all predecessors of a node are processed before the node itself. This means by the time we process
node,distance[node]already holds the correct shortest path fromsrc(assumingsrcitself is processed before or is the first node). - In graphs with cycles (especially negative cycles), this single-pass relaxation won’t work, as distances might continuously decrease, and there’s no fixed order to guarantee correct processing. For general graphs, Dijkstra’s (non-negative weights) or Bellman-Ford (can handle negative weights, detects negative cycles) are used.