Shortest Path In DAG Using Graph Class
#include <bits/stdc++.h> // Includes common standard libraries like iostream, unordered_map, list, stack, vector, limits.using namespace std; // Use the standard namespace.
// Define a Graph class to encapsulate graph operations.class Graph {public: // Adjacency list to store the graph: // Key: source node (int) // Value: list of pairs, where each pair is {destination_node, weight} unordered_map<int, list<pair<int, int>>> AdjList;
// Method to add a directed edge to the graph. void addEdge(int u, int v, int weight) { AdjList[u].push_back(make_pair(v, weight)); // Add edge u -> v with given weight. }
// Method to print the adjacency list (for debugging/visualization). void printEdge() { for(auto const& [node, neighbors] : AdjList) { // C++17 structured binding for map iteration. cout << node << " -> "; for(auto const& neighbor_pair : neighbors) { cout << neighbor_pair.first << "[" << neighbor_pair.second << "] "; // Print neighbor and weight. } cout << endl; } }
private: // Helper functions are usually private. // Recursive DFS helper for topological sort. // Pushes nodes to stack 'st' after visiting all their descendants. void solveDFS(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. 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(st, visited, neigh.first); } }
// After all outgoing paths from 'node' are explored and its descendants pushed, // push 'node' itself onto the stack. This is the core of DFS-based topological sort. st.push(node); }
public: // Public methods for graph functionalities. // Performs topological sort of the graph using DFS. // NOTE: Hardcoded `visited` size to 6. For a general solution, 'n' (number of nodes) // should be passed as a parameter to this method or managed by the class. stack<int> topoSort() { stack<int> st; // Stack to store the topological order. vector<int> visited(6, 0); // Visited array (assuming 6 nodes: 0 to 5). // Should ideally be `vector<int> visited(N_NODES, 0);` where N_NODES is determined dynamically.
// Iterate through possible node IDs to ensure all disconnected components are covered. // `AdjList` uses `unordered_map`, so `x.first` gives the node ID. // The outer loop `for(auto x : AdjList)` only iterates over nodes that have at least one outgoing edge. // It's safer to iterate `for(int i = 0; i < N_NODES; ++i)` and check `if(visited[i] == 0)`. // The example data has nodes from 0 to 5. for(int i = 0; i < 6; ++i) { // Iterating from 0 to 5 (assuming N_NODES = 6). if(visited[i] == 0) { // If a node exists in AdjList (i.e., has edges or is a key) // or if it's a valid node index that could be part of a component. if (AdjList.count(i) || !AdjList[i].empty() || i < 6) { // More robust check if node exists in graph. solveDFS(st, visited, i); // Start DFS from unvisited node. } } }
return st; // Return the stack containing the topological order. }
// Calculates the shortest path from a given source node in a DAG. // 'src': The source node. // NOTE: Hardcoded `distance` size to 6. Should be dynamic based on 'n'. vector<int> shortestPath(int src) { vector<int> distance(6, INT_MAX); // Distance array (assuming 6 nodes), initialized to infinity. // Should be `vector<int> distance(N_NODES, INT_MAX);` distance[src] = 0; // Distance from source to itself is 0.
// Get the topological order of the graph nodes. stack<int> st = topoSort();
// Process nodes in topological order. while(!st.empty()) { int currentNode = st.top(); // Get the next node from the topological order. st.pop(); // Remove it from the stack.
// Only process if the current node is reachable (i.e., its distance is not INT_MAX). if(distance[currentNode] != INT_MAX) { // Iterate through all neighbors of the current node. for(auto const& neighbor_pair : AdjList[currentNode]) { int neighbor = neighbor_pair.first; // Neighbor node ID. int weight = neighbor_pair.second; // Weight of edge to neighbor.
// Relaxation step: If a shorter path to 'neighbor' is found. // If current path (distance to currentNode + edge weight) is less than // the existing shortest distance to neighbor. if(distance[currentNode] + weight < distance[neighbor]) { distance[neighbor] = distance[currentNode] + weight; // Update shortest distance. } } } }
return distance; // Return the vector of shortest distances from 'src'. }};
int main() { Graph G; // Create an instance of the Graph class.
// Add edges to the graph. The edges provided in the problem description are for 6 nodes (0-5). G.addEdge(0, 1, 5); G.addEdge(1, 2, 2); G.addEdge(1, 3, 6); G.addEdge(0, 2, 3); G.addEdge(2, 3, 7); G.addEdge(3, 4, -1); G.addEdge(2, 4, 4); G.addEdge(4, 5, -2); G.addEdge(2, 5, 2);
// Optional: Print the graph's adjacency list. // G.printEdge();
int sourceNode = 1; // Set the source node for shortest path calculation. vector<int> shortestDistances = G.shortestPath(sourceNode);
cout << "Shortest distances from source node " << sourceNode << ":" << endl; // Print the calculated shortest distances. for(int i = 0; i < shortestDistances.size(); ++i) { // Iterate through all nodes (0 to 5 here). cout << "Node " << i << ": "; if(shortestDistances[i] == INT_MAX) { cout << "INF"; // Indicate if unreachable. } else { cout << shortestDistances[i]; // Print the distance. } cout << endl; }
return 0; // Indicate successful execution.}
1. Problem Statement
- Goal: Find the shortest path (minimum total weight) from a given
source
node to all other reachable nodes in a Weighted Directed Acyclic Graph (DAG). - Key Constraint: This algorithm works only for DAGs. It handles negative edge weights correctly as long as there are no negative cycles.
- Input: A graph represented by its edges (implicitly built into the
Graph
class),n
nodes (here hardcoded to 6 in some parts, ideally dynamic),src
(source node). - Output: A vector
distance
wheredistance[i]
is the shortest path distance fromsrc
to nodei
.INT_MAX
indicates unreachable.
2. Core Idea
This method leverages two main steps:
- Topological Sort: Since the graph is a DAG, we can find a linear ordering of its vertices such that for every directed edge
u -> v
,u
comes beforev
in the ordering. This is crucial because it ensures that when we process a node, all its direct or indirect prerequisites have already been processed and their shortest paths calculated. - Single Pass Relaxation: Once we have the topological order, we iterate through the nodes in that specific order. For each node, we “relax” all its outgoing edges. Relaxing an edge
u -> v
with weightw
means checking if the path tov
throughu
(distance[u] + w
) is shorter than the currently knowndistance[v]
. If it is, we updatedistance[v]
.
3. Algorithm Steps (within Graph
class)
A. Graph
Class Structure
AdjList
:unordered_map<int, list<pair<int,int>>>
to store the graph. Each key is a nodeu
, and its value is a list of pairs{v, weight}
representing directed edgesu -> v
withweight
.addEdge(u, v, weight)
: Adds a directed edgeu -> v
with the givenweight
to theAdjList
.printEdge()
: Utility function to display the adjacency list.
B. Topological Sort (topoSort
and solveDFS
methods)
-
solveDFS(stack<int> &st, vector<int> &visited, int node)
:- This is a private helper method for DFS.
visited[node] = 1;
(Mark current node as visited).- For each
neighbor
inAdjList[node]
:- If
neighbor
isunvisited
, recursively callsolveDFS
onneighbor
.
- If
- Crucial Step: After all descendants of
node
(and their subgraphs) have been fully explored, pushnode
onto thest
. This ensures that a node is pushed only after all nodes reachable from it have been pushed.
-
topoSort()
:- Initializes an empty
stack<int> st
and avector<int> visited(N_NODES, 0)
(whereN_NODES
is fixed to 6 here, but should ideally be dynamic based on graph input). - Iterates through potential node IDs (0 to N_NODES-1).
- If a node
x.first
(fromAdjList
iteration) is unvisited, it callssolveDFS(st, visited, x.first)
to start DFS for that component. This handles disconnected DAGs. - Returns the populated
st
containing the topological order.
- Initializes an empty
C. Shortest Path Calculation (shortestPath
method)
-
Initialize Distance Array:
vector<int> distance(N_NODES, INT_MAX);
(whereN_NODES
is 6). All distances are initially infinity.distance[src] = 0;
(Distance from source to itself is 0).
-
Get Topological Order:
stack<int> st = topoSort();
(Call thetopoSort
method to get the ordered stack).
-
Process Nodes in Topological Order & Relax Edges:
- While
st
is not empty:val = st.top(); st.pop();
(Get the next node in topological order).- Important Check:
if (distance[val] != INT_MAX)
: This condition ensures we only process nodes that are reachable from the source. Ifval
is unreachable (INT_MAX
), trying to add to it would lead to overflow or incorrect results. - For each
neighbor (x.first)
withweight (x.second)
inAdjList[val]
:- Relaxation Step: If
distance[val] + x.second < distance[x.first]
:distance[x.first] = distance[val] + x.second;
(Update the shortest distance tox.first
).
- Relaxation Step: If
- While
-
Return: The
distance
vector.
4. Time and Space Complexity
- Time Complexity:
O(V + E)
- Building
AdjList
(done externally inmain
then passed or implicitly fromaddEdge
calls):O(E)
. topoSort
(which callssolveDFS
): Each vertex and edge is visited once.O(V + E)
.- Distance array initialization:
O(V)
. - Relaxation loop: Each vertex is processed once, and 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)
.visited
vector:O(V)
.stack
for topological sort:O(V)
.distance
vector:O(V)
.
5. Important Notes/Caveats
- Hardcoded
n
(6) invisited
anddistance
arrays: The code hasvector<int> visited(6,0);
andvector<int> distance(6, INT_MAX);
. This means it assumes exactly 6 nodes (0 to 5). For a general solution,n
should be passed totopoSort
andshortestPath
, and these vectors should be sized dynamicallyvector<int> visited(n,0);
andvector<int> distance(n, INT_MAX);
. - Node Indexing: The current code assumes 0-indexed nodes (0 to N-1).
- Disconnected Components: The
topoSort
function correctly handles disconnected components by iterating through all possible nodes and starting a DFS if unvisited. - Negative Cycles: This algorithm will not work correctly if the graph contains negative weight cycles, as distances would keep decreasing infinitely. This algorithm is specifically for DAGs.