2858. Minimum Edge Reversals So Every Node Is Reachable
Problem Description
You are given a directed graph with n
nodes (labeled from 0
to n - 1
) that would form a tree if all edges were bidirectional. The graph is represented by a 2D array edges
, where edges[i] = [ui, vi]
indicates a directed edge from node ui
to node vi
.
An edge reversal means changing the direction of an edge - turning an edge from ui β vi
into vi β ui
.
Your task is to find, for each node i
(where 0 β€ i β€ n - 1
), the minimum number of edge reversals needed so that starting from node i
, you can reach all other nodes in the graph by following directed edges.
Return an array answer
where answer[i]
represents the minimum number of edge reversals required when starting from node i
.
Key Points:
- The graph forms a tree structure (connected and acyclic) if edges were bidirectional
- Each edge is initially directed (one-way)
- You need to calculate the answer independently for each starting node
- The goal is to make all nodes reachable from a given starting node with minimum edge reversals
Example Understanding:
If you have edges [[0,1], [2,0], [3,2]]
, this creates a directed path: 3 β 2 β 0 β 1
.
- Starting from node 0: You can reach node 1 directly, but to reach nodes 2 and 3, you'd need to reverse edges
2β0
and3β2
, requiring 2 reversals. - Starting from node 3: You can already reach all nodes following the existing directions, requiring 0 reversals.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly states we have a directed graph with
n
nodes and edges connecting them.
Is it a tree?
- Yes: The problem states "The graph would form a tree if its edges were bi-directional." This means the underlying structure is a tree (connected and acyclic), just with directed edges.
DFS
- Yes: We arrive at DFS as our algorithm choice.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this problem.
Why DFS is Appropriate
The DFS pattern fits perfectly for this problem because:
-
Tree Structure: We're dealing with a tree structure where we need to traverse from each potential root to all other nodes.
-
Two-Phase DFS Approach: The solution uses DFS twice:
- First DFS: Calculate the minimum reversals needed when starting from node 0. This traverses the tree and counts how many edges point in the "wrong" direction (away from root 0).
- Second DFS: Use dynamic programming to calculate answers for all other nodes based on the result from node 0. When moving the "root" from node
i
to its childj
, we adjust the reversal count based on the edge direction.
-
Parent-Child Relationships: DFS naturally maintains parent-child relationships during traversal, which is crucial for:
- Avoiding cycles (checking
if j != fa
) - Determining edge direction relative to the chosen root
- Propagating results from parent to children in the second phase
- Avoiding cycles (checking
The tree property guarantees that DFS will visit each node exactly once, making it an efficient O(n) solution for calculating the answer for all nodes.
Intuition
The key insight is that if we know the answer for one node, we can efficiently compute the answer for all other nodes without starting from scratch.
Let's think about what happens when we pick node 0 as the root. We need to count how many edges need to be reversed to reach all nodes from node 0. An edge needs to be reversed if it points "away" from our root (going against the direction we want to traverse).
Now here's the clever part: what happens when we shift our perspective from node 0 to one of its neighbors, say node 1?
- If the original edge was
0 β 1
, we didn't need to reverse it when starting from 0. But now starting from 1, we'd need to reverse it to reach 0. So the reversal count increases by 1. - If the original edge was
1 β 0
, we had to reverse it when starting from 0. But now starting from 1, it's already pointing the right way. So the reversal count decreases by 1.
This observation leads to a beautiful relationship: when moving the "root" from node i
to its adjacent node j
, the answer changes by exactly Β±1 depending on the original edge direction.
The solution brilliantly encodes this by creating a bidirectional representation where:
- Forward edges (
x β y
) are marked with weight1
- Reverse edges (
y β x
) are marked with weight-1
During the first DFS from node 0, we count edges with negative weight (these are the edges pointing away from 0 that need reversal).
In the second DFS, we propagate the answer using the formula: ans[j] = ans[i] + k
, where k
is the edge weight. This elegantly captures the Β±1 adjustment:
- If
k = 1
: the edge originally pointed fromi
toj
, so we need one more reversal whenj
is the root - If
k = -1
: the edge originally pointed fromj
toi
, so we need one fewer reversal whenj
is the root
This two-pass approach transforms an O(nΒ²) naive solution (running DFS from each node) into an elegant O(n) solution.
Learn more about Depth-First Search, Breadth-First Search, Graph and Dynamic Programming patterns.
Solution Approach
The implementation uses a two-pass DFS strategy with a clever edge weight encoding system.
Step 1: Graph Construction
First, we build an adjacency list where each edge is stored bidirectionally with different weights:
g = [[] for _ in range(n)]
for x, y in edges:
g[x].append((y, 1)) # Original direction: weight = 1
g[y].append((x, -1)) # Reverse direction: weight = -1
This encoding allows us to track edge directions:
- Weight
1
: Edge needs to be reversed if we traverse it in this direction - Weight
-1
: Edge is already in the correct direction
Step 2: First DFS - Calculate reversals from node 0
def dfs(i: int, fa: int):
for j, k in g[i]:
if j != fa:
ans[0] += int(k < 0) # Count edges that need reversal
dfs(j, i)
Starting from node 0, we traverse the tree and count how many edges point away from the root (have negative weight). These are the edges we'd need to reverse to reach all nodes from node 0.
i
: Current nodefa
: Parent node (to avoid revisiting)k < 0
: Indicates the edge originally pointed fromj
toi
, so it needs reversal when starting from node 0
Step 3: Second DFS - Propagate results to all nodes
def dfs2(i: int, fa: int):
for j, k in g[i]:
if j != fa:
ans[j] = ans[i] + k
dfs2(j, i)
This phase uses dynamic programming to calculate answers for all other nodes based on the result from node 0:
- When moving from node
i
to nodej
:- If
k = 1
: The edge originally goesi β j
, so starting fromj
requires one additional reversal (to reachi
) - If
k = -1
: The edge originally goesj β i
, so starting fromj
requires one fewer reversal
- If
The formula ans[j] = ans[i] + k
elegantly captures this adjustment.
Time Complexity: O(n) - Each node is visited exactly twice Space Complexity: O(n) - For the adjacency list and recursion stack
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with 4 nodes and edges: [[0,1], [2,0], [3,2]]
This creates the directed graph: 3 β 2 β 0 β 1
Step 1: Build the Graph We create a bidirectional adjacency list with weights:
Node 0: [(1, 1), (2, -1)] // 0β1 original, 2β0 reversed Node 1: [(0, -1)] // 0β1 reversed view Node 2: [(0, 1), (3, -1)] // 2β0 original, 3β2 reversed Node 3: [(2, 1)] // 3β2 original
Step 2: First DFS from Node 0 Starting at node 0, we traverse and count negative weights (edges needing reversal):
- Visit node 0
- Check edge to node 1: weight = 1 (positive, no reversal needed)
- Visit node 1 (no more edges except back to parent)
- Check edge to node 2: weight = -1 (negative, needs reversal! Count = 1)
- Visit node 2
- Check edge to node 3: weight = -1 (negative, needs reversal! Count = 2)
- Visit node 3 (no more edges except back to parent)
- Check edge to node 3: weight = -1 (negative, needs reversal! Count = 2)
- Visit node 2
- Check edge to node 1: weight = 1 (positive, no reversal needed)
Result: ans[0] = 2
(need to reverse edges 2β0 and 3β2)
Step 3: Second DFS to Propagate Results Starting from node 0 with ans[0] = 2:
- At node 0, process children:
- To node 1:
ans[1] = ans[0] + 1 = 2 + 1 = 3
- Edge 0β1 exists, so from node 1 we need +1 reversal to reach node 0
- To node 2:
ans[2] = ans[0] + (-1) = 2 - 1 = 1
- Edge 2β0 exists, so from node 2 we need -1 reversal (one less)
- From node 2, process its child:
- To node 3:
ans[3] = ans[2] + (-1) = 1 - 1 = 0
- Edge 3β2 exists, so from node 3 we need -1 reversal
- To node 3:
- To node 1:
Final Answer: [2, 3, 1, 0]
Verification:
- From node 0: Reverse 2β0 and 3β2 to reach all nodes (2 reversals) β
- From node 1: Reverse 0β1, 2β0, and 3β2 to reach all nodes (3 reversals) β
- From node 2: Reverse 3β2 to reach node 3, already can reach 0β1 (1 reversal) β
- From node 3: Can already reach all nodes via 3β2β0β1 (0 reversals) β
Solution Implementation
1class Solution:
2 def minEdgeReversals(self, n: int, edges: List[List[int]]) -> List[int]:
3 # Initialize result array to store minimum reversals for each node as root
4 min_reversals = [0] * n
5
6 # Build adjacency list with weighted edges
7 # Weight 1: original direction (no reversal needed)
8 # Weight -1: reversed direction (reversal needed)
9 adjacency_list = [[] for _ in range(n)]
10 for source, destination in edges:
11 adjacency_list[source].append((destination, 1)) # Original edge direction
12 adjacency_list[destination].append((source, -1)) # Reversed edge direction
13
14 def calculate_reversals_from_root(node: int, parent: int) -> None:
15 """
16 First DFS: Calculate the number of edge reversals needed
17 when node 0 is the root to reach all other nodes.
18 """
19 for neighbor, edge_weight in adjacency_list[node]:
20 if neighbor != parent:
21 # If edge_weight < 0, we need to reverse this edge
22 min_reversals[0] += int(edge_weight < 0)
23 calculate_reversals_from_root(neighbor, node)
24
25 # Calculate reversals needed when node 0 is the root
26 calculate_reversals_from_root(0, -1)
27
28 def propagate_reversals_to_children(node: int, parent: int) -> None:
29 """
30 Second DFS: Use the result from node 0 to calculate
31 the minimum reversals for all other nodes as roots.
32
33 The key insight: when moving root from parent to child,
34 the reversal count changes by the edge weight between them.
35 """
36 for neighbor, edge_weight in adjacency_list[node]:
37 if neighbor != parent:
38 # Update reversal count for neighbor based on current node
39 # If edge_weight = 1: moving root from node to neighbor requires one less reversal
40 # If edge_weight = -1: moving root from node to neighbor requires one more reversal
41 min_reversals[neighbor] = min_reversals[node] + edge_weight
42 propagate_reversals_to_children(neighbor, node)
43
44 # Propagate the reversal counts to all nodes
45 propagate_reversals_to_children(0, -1)
46
47 return min_reversals
48
1class Solution {
2 // Adjacency list where each edge stores [destination, direction]
3 // direction: 1 means original direction, -1 means reversed direction
4 private List<int[]>[] adjacencyList;
5
6 // Result array storing minimum reversals needed from each node
7 private int[] minReversals;
8
9 public int[] minEdgeReversals(int n, int[][] edges) {
10 // Initialize result array
11 minReversals = new int[n];
12
13 // Initialize adjacency list
14 adjacencyList = new List[n];
15 Arrays.setAll(adjacencyList, i -> new ArrayList<>());
16
17 // Build bidirectional graph with edge directions
18 // For edge (x -> y): x to y has weight 1 (no reversal needed)
19 // y to x has weight -1 (reversal needed)
20 for (int[] edge : edges) {
21 int from = edge[0];
22 int to = edge[1];
23 adjacencyList[from].add(new int[] {to, 1}); // Original direction
24 adjacencyList[to].add(new int[] {from, -1}); // Reversed direction
25 }
26
27 // First DFS: Calculate reversals needed from node 0
28 dfs(0, -1);
29
30 // Second DFS: Use rerooting to calculate reversals for all other nodes
31 dfs2(0, -1);
32
33 return minReversals;
34 }
35
36 /**
37 * First DFS to calculate the number of edge reversals needed
38 * to reach all nodes from node 0
39 * @param currentNode - current node being visited
40 * @param parent - parent node to avoid revisiting
41 */
42 private void dfs(int currentNode, int parent) {
43 // Traverse all neighbors
44 for (int[] neighbor : adjacencyList[currentNode]) {
45 int nextNode = neighbor[0];
46 int direction = neighbor[1];
47
48 // Skip parent to avoid cycles
49 if (nextNode != parent) {
50 // If direction is -1, we need to reverse this edge
51 // to go from node 0 to nextNode
52 if (direction < 0) {
53 minReversals[0] += 1;
54 }
55
56 // Continue DFS traversal
57 dfs(nextNode, currentNode);
58 }
59 }
60 }
61
62 /**
63 * Second DFS using rerooting technique to calculate reversals
64 * for all nodes based on the result from node 0
65 * @param currentNode - current node being visited
66 * @param parent - parent node to avoid revisiting
67 */
68 private void dfs2(int currentNode, int parent) {
69 // Traverse all neighbors
70 for (int[] neighbor : adjacencyList[currentNode]) {
71 int nextNode = neighbor[0];
72 int direction = neighbor[1];
73
74 // Skip parent to avoid cycles
75 if (nextNode != parent) {
76 // Calculate reversals for nextNode based on currentNode
77 // If direction is 1: moving root from currentNode to nextNode
78 // doesn't need this edge reversed
79 // If direction is -1: moving root from currentNode to nextNode
80 // needs this edge reversed
81 minReversals[nextNode] = minReversals[currentNode] + direction;
82
83 // Continue rerooting for subtree rooted at nextNode
84 dfs2(nextNode, currentNode);
85 }
86 }
87 }
88}
89
1class Solution {
2public:
3 vector<int> minEdgeReversals(int n, vector<vector<int>>& edges) {
4 // Build adjacency list with weighted edges
5 // Weight 1: original direction (from parent to child)
6 // Weight -1: reversed direction (from child to parent)
7 vector<vector<pair<int, int>>> adjacencyList(n);
8 vector<int> result(n);
9
10 // Build bidirectional graph with weights indicating edge direction
11 for (const auto& edge : edges) {
12 int from = edge[0];
13 int to = edge[1];
14 adjacencyList[from].emplace_back(to, 1); // Original direction: no reversal needed
15 adjacencyList[to].emplace_back(from, -1); // Reverse direction: reversal needed
16 }
17
18 // First DFS: Calculate the number of reversals needed from node 0
19 function<void(int, int)> calculateReversalsFromRoot = [&](int current, int parent) {
20 for (const auto& [neighbor, weight] : adjacencyList[current]) {
21 if (neighbor != parent) {
22 // If weight < 0, we need to reverse this edge to reach neighbor from root
23 if (weight < 0) {
24 result[0]++;
25 }
26 calculateReversalsFromRoot(neighbor, current);
27 }
28 }
29 };
30
31 // Second DFS: Calculate reversals for all other nodes based on root's result
32 function<void(int, int)> propagateReversals = [&](int current, int parent) {
33 for (const auto& [neighbor, weight] : adjacencyList[current]) {
34 if (neighbor != parent) {
35 // When moving root from current to neighbor:
36 // If weight > 0: edge was in correct direction, now needs reversal (+1)
37 // If weight < 0: edge needed reversal, now in correct direction (-1)
38 result[neighbor] = result[current] + weight;
39 propagateReversals(neighbor, current);
40 }
41 }
42 };
43
44 // Calculate reversals needed from node 0
45 calculateReversalsFromRoot(0, -1);
46
47 // Propagate to calculate reversals for all nodes
48 propagateReversals(0, -1);
49
50 return result;
51 }
52};
53
1/**
2 * Calculates minimum edge reversals needed to reach each node from every other node
3 * @param n - Number of nodes in the graph
4 * @param edges - Array of directed edges [from, to]
5 * @returns Array where ans[i] is the minimum reversals needed to make node i reachable from all nodes
6 */
7function minEdgeReversals(n: number, edges: number[][]): number[] {
8 // Build adjacency list with weights
9 // Weight 1: original edge direction (no reversal needed)
10 // Weight -1: reversed edge direction (reversal needed)
11 const adjacencyList: number[][][] = Array.from({ length: n }, () => []);
12
13 for (const [from, to] of edges) {
14 adjacencyList[from].push([to, 1]); // Original edge: from -> to (no reversal)
15 adjacencyList[to].push([from, -1]); // Reversed edge: to -> from (needs reversal)
16 }
17
18 // Initialize result array to store minimum reversals for each node
19 const result: number[] = Array(n).fill(0);
20
21 /**
22 * First DFS: Calculate reversals needed to reach all nodes from node 0
23 * @param currentNode - Current node being visited
24 * @param parentNode - Parent node to avoid revisiting
25 */
26 const calculateReversalsFromRoot = (currentNode: number, parentNode: number): void => {
27 for (const [neighborNode, edgeWeight] of adjacencyList[currentNode]) {
28 if (neighborNode !== parentNode) {
29 // If edge weight is negative, we need to reverse this edge
30 result[0] += edgeWeight < 0 ? 1 : 0;
31 calculateReversalsFromRoot(neighborNode, currentNode);
32 }
33 }
34 };
35
36 /**
37 * Second DFS: Re-root the tree to calculate reversals for all other nodes
38 * Uses the result from node 0 to efficiently compute results for other nodes
39 * @param currentNode - Current node being visited
40 * @param parentNode - Parent node to avoid revisiting
41 */
42 const reRootTree = (currentNode: number, parentNode: number): void => {
43 for (const [neighborNode, edgeWeight] of adjacencyList[currentNode]) {
44 if (neighborNode !== parentNode) {
45 // When moving root from currentNode to neighborNode:
46 // - If edgeWeight > 0: edge goes from current to neighbor (add 1 reversal)
47 // - If edgeWeight < 0: edge goes from neighbor to current (remove 1 reversal)
48 result[neighborNode] = result[currentNode] + edgeWeight;
49 reRootTree(neighborNode, currentNode);
50 }
51 }
52 };
53
54 // First pass: Calculate reversals needed from node 0
55 calculateReversalsFromRoot(0, -1);
56
57 // Second pass: Calculate reversals for all other nodes using re-rooting technique
58 reRootTree(0, -1);
59
60 return result;
61}
62
Time and Space Complexity
Time Complexity: O(n)
The algorithm consists of two depth-first search (DFS) traversals:
- The first DFS (
dfs
) visits each node exactly once, and for each node, it iterates through all its adjacent edges. Since this is a tree withn
nodes, there aren-1
edges, and each edge is stored twice in the adjacency list (once for each direction). This gives usO(n + 2(n-1)) = O(n)
. - The second DFS (
dfs2
) also visits each node exactly once and iterates through all adjacent edges, resulting in the sameO(n)
complexity. - The graph construction loop iterates through all
n-1
edges once, which isO(n)
.
Overall time complexity: O(n) + O(n) + O(n) = O(n)
Space Complexity: O(n)
The space usage includes:
- The adjacency list
g
which stores2(n-1)
edge entries total across all nodes:O(n)
- The answer array
ans
of sizen
:O(n)
- The recursive call stack for DFS, which in the worst case (a linear tree) can go up to depth
n
:O(n)
Overall space complexity: O(n) + O(n) + O(n) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Edge Weight Assignment
Pitfall: One of the most common mistakes is confusing the edge weight assignment logic. Developers often reverse the weights, assigning 1
to edges that need reversal and -1
to edges in the correct direction.
Incorrect Implementation:
# WRONG: This reverses the meaning of weights for source, destination in edges: adjacency_list[source].append((destination, -1)) # Incorrect! adjacency_list[destination].append((source, 1)) # Incorrect!
Why it's wrong: The weight should represent the cost adjustment when changing the root. When we traverse an edge in its original direction (source β destination), no reversal is needed from the perspective of the original root, but when we make the destination the new root, we'd need to reverse this edge.
Solution: Remember that the weight represents the change in reversal count when moving the root across that edge:
# CORRECT: Weight represents the change when moving root for source, destination in edges: adjacency_list[source].append((destination, 1)) # Moving root from source to dest adds 1 reversal adjacency_list[destination].append((source, -1)) # Moving root from dest to source removes 1 reversal
2. Misunderstanding the Weight's Role in the Second DFS
Pitfall: Incorrectly interpreting what the edge weight means during the propagation phase, leading to wrong formulas like ans[j] = ans[i] - k
or ans[j] = ans[i] + abs(k)
.
Why it happens: The dual meaning of edge weights can be confusing:
- In the first DFS:
k < 0
indicates an edge that needs reversal from node 0's perspective - In the second DFS:
k
represents the change in reversal count when moving the root
Solution: Understand that in the second DFS, the weight directly represents the adjustment needed:
- If traversing edge
i β j
with weightk = 1
: The edge goes from i to j originally, so making j the root requires one MORE reversal (to reach i from j) - If traversing edge
i β j
with weightk = -1
: The edge originally went from j to i, so making j the root requires one FEWER reversal
3. Forgetting to Handle the Tree Property
Pitfall: Not properly avoiding cycles by forgetting to track the parent node, causing infinite recursion.
Incorrect Implementation:
def dfs(node):
for neighbor, weight in adjacency_list[node]:
# WRONG: No parent check, will revisit nodes
ans[0] += int(weight < 0)
dfs(neighbor)
Solution: Always pass and check the parent node to avoid revisiting:
def dfs(node, parent):
for neighbor, weight in adjacency_list[node]:
if neighbor != parent: # Essential check
ans[0] += int(weight < 0)
dfs(neighbor, node)
4. Incorrect Initial Count in First DFS
Pitfall: Counting edge reversals incorrectly by checking k > 0
instead of k < 0
, or forgetting the conversion to integer.
Solution: Remember that in the first DFS, we count edges that point AWAY from the root (need reversal):
# Correct: Count edges with negative weight (originally pointing toward root)
ans[0] += int(k < 0)
The int()
conversion is crucial because k < 0
returns a boolean, and we need to add 1 or 0 to our count.
In a binary min heap, the minimum element can be found in:
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!