Count Visited Nodes in a Directed Graph
HardPrompt
There is a directed graph consisting of n nodes numbered from 0 to n - 1 and n directed edges.
You are given a 0-indexed array edges where edges[i] indicates that there is an edge from node i to node edges[i].
Consider the following process on the graph:
- You start from a node
xand keep visiting other nodes through edges until you reach a node that you have already visited before on this same process.
Return an array answer where answer[i] is the number of different nodes that you will visit if you perform the process starting from node i.
Example 1:
Input: edges = [1,2,0,0]
Output: [3,3,3,4]
Explanation: We perform the process starting from each node in the following way:
- Starting from node 0, we visit the nodes 0 -> 1 -> 2 -> 0. The number of different nodes we visit is 3.
- Starting from node 1, we visit the nodes 1 -> 2 -> 0 -> 1. The number of different nodes we visit is 3.
- Starting from node 2, we visit the nodes 2 -> 0 -> 1 -> 2. The number of different nodes we visit is 3.
- Starting from node 3, we visit the nodes 3 -> 0 -> 1 -> 2 -> 0. The number of different nodes we visit is 4.Example 2:
Input: edges = [1,2,3,4,0]
Output: [5,5,5,5,5]
Explanation: Starting from any node we can visit every node in the graph in the process.
Constraints:
n == edges.length2 <= n <= 1050 <= edges[i] <= n - 1edges[i] != i
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem for each starting node. For every node from 0 to n-1, we traverse the graph, keeping track of the nodes visited in the current path using a HashSet. The traversal stops when we encounter a node that has already been seen in this specific traversal. The number of nodes collected in the HashSet gives us the answer for the starting node.
Algorithm
- Create an integer array
answerof sizen. - For each node
ifrom0ton-1:- Create a
HashSet<Integer>pathVisitedto keep track of nodes visited in the current traversal. - Initialize
currentNode = i. - In a loop, continue as long as
currentNodecan be added topathVisited(meaning it's a new node for this path).- In each iteration, update
currentNodetoedges[currentNode].
- In each iteration, update
- The size of
pathVisitedis the number of unique nodes visited. Store this inanswer[i].
- Create a
- Return the
answerarray.
Walkthrough
We initialize an answer array of size n. We then iterate through each node i from 0 to n-1 to calculate answer[i]. For each i, we begin a simulation. A HashSet called visited_in_path is used to store nodes visited in the current traversal, which starts from node i. We also use a counter, count, initialized to 0. We start with currentNode = i. In a loop, as long as currentNode is not in visited_in_path, we add it to the set, increment our count, and move to the next node by setting currentNode = edges[currentNode]. When the loop terminates, it means we've encountered a node that was already in visited_in_path. The final value of count (or the size of the set) is the number of different nodes visited, which we store in answer[i]. After iterating through all possible starting nodes, the answer array is complete and returned.
import java.util.HashSet;import java.util.Set; class Solution { public int[] countVisitedNodes(int[] edges) { int n = edges.length; int[] answer = new int[n]; for (int i = 0; i < n; i++) { Set<Integer> visitedInPath = new HashSet<>(); int currentNode = i; while (visitedInPath.add(currentNode)) { currentNode = edges[currentNode]; } answer[i] = visitedInPath.size(); } return answer; }}Complexity
Time
O(n^2) - For each of the `n` starting nodes, the traversal can take up to O(n) steps in the worst case (a single long path). This results in a quadratic time complexity.
Space
O(n) - In each simulation, the `HashSet` can store up to `n` nodes in the worst-case scenario where the path traverses all nodes before repeating.
Trade-offs
Pros
Simple to understand and implement.
Directly follows the logic described in the problem statement.
Cons
Highly inefficient due to redundant computations. The traversal from one node might largely overlap with the traversal from another, but this work is repeated.
The time complexity is too high for the given constraints, which will likely result in a 'Time Limit Exceeded' error on most platforms.
This approach improves upon the brute force method by using memoization to avoid re-computation. We use an answer array to store the results. When we traverse from a node, if we encounter another node whose result is already known, we can use that result to quickly calculate the answers for all nodes in the current path. This ensures that each node's answer is computed exactly once, leading to a linear time complexity.
Algorithm
- Initialize an
answerarray of sizenwith all zeros. A zero indicates the answer for that node has not been computed. - Iterate through each node
ifrom0ton-1. - If
answer[i]is 0, start a traversal fromi:- Use a
Map<Integer, Integer>calledpathto store each node in the current traversal path and its distance from the starting node. - Traverse from
curr = i, incrementing distance, untilcurris a node that is already inpath(a cycle is found) oranswer[curr]is non-zero (a previously computed path is reached). - Case 1: Cycle found. The node
curris already inpath. Calculate the cycle length. For all nodes in the cycle, the answer is the cycle length. For all nodes on the path leading to the cycle, the answer is(distance to cycle) + (cycle length). - Case 2: Known path reached.
answer[curr]is non-zero. The answer for any nodeuin the currentpathisanswer[curr] + (distance from u to curr).
- Use a
- Update the
answerarray for all nodes visited in the current traversal. - Return
answer.
Walkthrough
The key insight is that the path from any node i will eventually enter a cycle or merge with a path whose properties are already known. We can exploit this structure.
We use an answer array of size n, initialized to zeros. A non-zero value answer[i] means the result for node i is known.
We iterate from i = 0 to n-1. If answer[i] is zero, we initiate a traversal. During this traversal, we use a HashMap to keep track of the nodes in the current path and their respective distances from the starting node i. The traversal curr = edges[curr] continues until we hit a node curr that either has a non-zero answer or is already in our path map.
If answer[curr] is non-zero, we've reached a segment of the graph for which we have answers. We can then backtrack along the path we just traversed, calculating the answer for each node based on answer[curr] and its distance to curr.
If curr is already in our path map, we've detected a cycle. We can calculate the cycle's length and the length of the path leading to it. All nodes within the cycle will have the cycle length as their answer. Nodes on the path leading to the cycle will have an answer equal to the cycle length plus their distance to the cycle's entry point.
By storing results, we ensure every node is fully processed only once.
import java.util.HashMap;import java.util.Map; class Solution { public int[] countVisitedNodes(int[] edges) { int n = edges.length; int[] answer = new int[n]; // 0 indicates not calculated yet for (int i = 0; i < n; i++) { if (answer[i] == 0) { // Start traversal from node i int dist = 0; int curr = i; Map<Integer, Integer> path = new HashMap<>(); // Traverse until we find a cycle or a node with a known answer while (answer[curr] == 0 && !path.containsKey(curr)) { path.put(curr, dist); dist++; curr = edges[curr]; } if (path.containsKey(curr)) { // Cycle detected in the current traversal int cycleStartDist = path.get(curr); int cycleLen = dist - cycleStartDist; for (Map.Entry<Integer, Integer> entry : path.entrySet()) { if (entry.getValue() >= cycleStartDist) { // Node is in the cycle answer[entry.getKey()] = cycleLen; } else { // Node is on the path leading to the cycle answer[entry.getKey()] = cycleLen + (cycleStartDist - entry.getValue()); } } } else { // Hit a node with a pre-computed answer int knownLen = answer[curr]; for (Map.Entry<Integer, Integer> entry : path.entrySet()) { answer[entry.getKey()] = knownLen + (dist - entry.getValue()); } } } } return answer; }}Complexity
Time
O(n) - Each node is visited a constant number of times across all traversals. Once a node's answer is computed, it's never part of a traversal path again, effectively making the total work proportional to the number of nodes.
Space
O(n) - We need O(n) for the `answer` array. The `path` map used during a traversal can also grow up to O(n) in the worst case of a single component containing all nodes.
Trade-offs
Pros
Highly efficient with linear time complexity, making it suitable for large inputs.
Avoids redundant computations by storing and reusing results (memoization).
Cons
More complex to implement compared to the brute force approach.
Requires careful handling of the two termination cases: finding a cycle versus hitting a pre-computed path.
Solutions
Solution
class Solution { public int [] countVisitedNodes ( List < Integer > edges ) { int n = edges . size (); int [] ans = new int [ n ]; int [] vis = new int [ n ]; for ( int i = 0 ; i < n ; ++ i ) { if ( ans [ i ] == 0 ) { int cnt = 0 , j = i ; while ( vis [ j ] == 0 ) { vis [ j ] = ++ cnt ; j = edges . get ( j ); } int cycle = 0 , total = cnt + ans [ j ]; if ( ans [ j ] == 0 ) { cycle = cnt - vis [ j ] + 1 ; } j = i ; while ( ans [ j ] == 0 ) { ans [ j ] = Math . max ( total --, cycle ); j = edges . get ( j ); } } } return ans ; } }class Solution {public: vector<int> countVisitedNodes(vector<int> &edges) { int n = edges.size(); vector<int> ans(n), vis(n); for (int i = 0; i < n; ++i) { if (!ans[i]) { int cnt = 0, j = i; while (vis[j] == 0) { vis[j] = ++cnt; j = edges[j]; } int cycle = 0, total = cnt + ans[j]; if (ans[j] == 0) { cycle = cnt - vis[j] + 1; } j = i; while (ans[j] == 0) { ans[j] = max(total--, cycle); j = edges[j]; } } } return ans; }};class Solution: def countVisitedNodes(self, edges: List[int]) -> List[int]: n = len(edges) ans = [0] * n vis = [0] * n for i in range(n): if not ans[i]: cnt, j = 0, i while not vis[j]: cnt += 1 vis[j] = cnt j = edges[j] cycle, total = 0, cnt + ans[j] if not ans[j]: cycle = cnt - vis[j] + 1 total = cnt j = i while not ans[j]: ans[j] = max(total, cycle) total -= 1 j = edges[j] return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.