Facebook Pixel

2876. Count Visited Nodes in a Directed Graph

Problem Description

You have a directed graph with n nodes labeled from 0 to n - 1. The graph has exactly n directed edges, where each node has exactly one outgoing edge. The graph is represented by a 0-indexed array edges where edges[i] indicates there is a directed edge from node i to node edges[i].

The problem asks you to simulate a specific process for each starting node:

  • Start from a node x
  • Keep following the edges to visit other nodes
  • Stop when you reach a node that you've already visited in this current process

Your task is to return an array answer where answer[i] represents the total number of different nodes you will visit when performing this process starting from node i.

For example, if you start from a node and follow edges: 2 → 3 → 4 → 3, you would visit nodes 2, 3, 4 and then 3 again (which was already visited), so you stop. The count of different nodes visited would be 3 (nodes 2, 3, and 4).

Since each node has exactly one outgoing edge, following the edges will eventually lead to a cycle. The traversal from any starting node will either:

  1. Enter directly into a cycle (if the starting node is part of a cycle)
  2. Follow a path that eventually leads into a cycle

The challenge is to efficiently compute the number of distinct nodes visited for all possible starting nodes from 0 to n - 1.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Since each node has exactly one outgoing edge, the graph structure forms what's called a "functional graph" - following edges from any starting point will eventually lead to a cycle. Think of it like a comet: there's a "tail" (path leading to the cycle) and a "head" (the cycle itself).

The key insight is that once we know the answer for one node, we can use that information to help calculate answers for other nodes. When we traverse from a node i, we're essentially walking along a path until we hit a cycle. There are two scenarios we need to handle:

  1. We hit a cycle that hasn't been processed yet: This means we've discovered a new cycle. Nodes within the cycle will visit all nodes in the cycle (they go round and round), while nodes on the path leading to the cycle will visit all nodes from their position to the cycle, plus all nodes in the cycle.

  2. We hit a node whose answer we already know: This is great! If we know node j visits ans[j] nodes, and we're currently at a node that's k steps away from j, then our current node visits k + ans[j] nodes.

To implement this efficiently, we use a vis array to track the order in which we visit nodes during our current traversal. When we revisit a node, we can determine:

  • If vis[j] > 0: We've found a cycle in our current traversal. The cycle length is current_count - vis[j] + 1.
  • If ans[j] > 0: We've reached a node that was already processed in a previous traversal.

By processing each unvisited node exactly once and reusing previously computed results, we achieve an efficient O(n) solution. The trick is carefully tracking whether we're inside or outside a cycle, and properly calculating distances based on our position relative to the cycle.

Learn more about Graph, Memoization and Dynamic Programming patterns.

Solution Approach

We use two arrays to track our progress:

  • ans[i]: stores the final answer for node i (number of distinct nodes visited starting from i)
  • vis[i]: temporarily stores the visit order for node i during the current traversal (0 means unvisited in current traversal)

For each unprocessed node i (where ans[i] == 0), we perform a traversal:

  1. First Pass - Mark Visit Order:

    • Start from node i and follow edges, incrementing a counter cnt for each step
    • Mark vis[j] = cnt for each visited node j
    • Continue until we hit a node that's already been visited in this traversal (vis[j] > 0) or has a known answer (ans[j] > 0)
  2. Determine Cycle Properties:

    • If we stopped because ans[j] > 0: We've reached a previously processed node
      • total = cnt + ans[j] (our path length plus the known answer)
      • cycle = 0 (no new cycle discovered)
    • If we stopped because vis[j] > 0: We've found a new cycle
      • cycle = cnt - vis[j] + 1 (the cycle length)
      • total = cnt (total nodes in our complete traversal)
  3. Second Pass - Fill Answers:

    • Traverse the path again from node i
    • For each node on the path:
      • If it's in the cycle: ans[j] = cycle
      • If it's outside the cycle: ans[j] = total, then decrement total for the next node
    • The max(total, cycle) ensures nodes in the cycle get the cycle length

The algorithm cleverly handles two cases:

  • Case 1: When we discover a new cycle, all nodes in the cycle get cycle as their answer, while nodes on the tail get progressively larger values as they're further from the cycle
  • Case 2: When we hit a known node, we can immediately calculate answers by adding distances

Time Complexity: O(n) - each node is processed at most twice Space Complexity: O(n) - for the ans and vis arrays

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with edges = [1, 2, 3, 1, 2]:

Graph structure:
0 1231 (cycle: 1231)
        4

Initial State:

  • ans = [0, 0, 0, 0, 0] (all unprocessed)
  • vis = [0, 0, 0, 0, 0] (reset for each traversal)

Processing node 0:

First Pass - Mark Visit Order:

  • Start at 0, cnt = 1, set vis[0] = 1
  • Move to 1, cnt = 2, set vis[1] = 2
  • Move to 2, cnt = 3, set vis[2] = 3
  • Move to 3, cnt = 4, set vis[3] = 4
  • Move to 1, but vis[1] = 2 > 0 (already visited!)
  • Stop with cnt = 4

Determine Cycle Properties:

  • Since vis[1] > 0, we found a cycle
  • cycle = cnt - vis[1] + 1 = 4 - 2 + 1 = 3 (nodes 1, 2, 3 form the cycle)
  • total = cnt = 4

Second Pass - Fill Answers:

  • Start at 0 again:
    • Node 0: Not in cycle (vis[0]=1 < vis[1]=2), so ans[0] = total = 4, then total = 3
    • Node 1: In cycle (vis[1]=2 >= 2), so ans[1] = max(3, 3) = 3
    • Node 2: In cycle (vis[2]=3 >= 2), so ans[2] = max(3, 3) = 3
    • Node 3: In cycle (vis[3]=4 >= 2), so ans[3] = max(3, 3) = 3
  • Result: ans = [4, 3, 3, 3, 0], vis reset to [0, 0, 0, 0, 0]

Processing node 4:

First Pass - Mark Visit Order:

  • Start at 4, cnt = 1, set vis[4] = 1
  • Move to 2, but ans[2] = 3 > 0 (already processed!)
  • Stop with cnt = 1

Determine Cycle Properties:

  • Since ans[2] > 0, we hit a known node
  • total = cnt + ans[2] = 1 + 3 = 4
  • cycle = 0 (no new cycle)

Second Pass - Fill Answers:

  • Start at 4 again:
    • Node 4: ans[4] = total = 4, then total = 3
  • Result: ans = [4, 3, 3, 3, 4]

Final Answer: [4, 3, 3, 3, 4]

This demonstrates how the algorithm:

  1. Discovers the cycle 1→2→3→1 when processing node 0
  2. Assigns cycle length (3) to nodes in the cycle
  3. Assigns increasing values (4) to nodes outside the cycle based on their distance
  4. Reuses the known answer for node 2 when processing node 4, avoiding redundant traversal

Solution Implementation

1class Solution:
2    def countVisitedNodes(self, edges: List[int]) -> List[int]:
3        """
4        For each node in a functional graph, count how many nodes are visited
5        when following edges starting from that node.
6      
7        Since each node has exactly one outgoing edge, paths either lead to
8        a cycle or merge into a cycle.
9      
10        Args:
11            edges: List where edges[i] represents the destination from node i
12          
13        Returns:
14            List where result[i] is the count of visited nodes starting from node i
15        """
16        n = len(edges)
17        result = [0] * n  # Store the final answer for each node
18        visit_order = [0] * n  # Track visit order during traversal (0 means unvisited)
19      
20        for start_node in range(n):
21            # Skip if we already computed the result for this node
22            if result[start_node]:
23                continue
24              
25            # Phase 1: Traverse and mark visit order until we hit a visited node
26            visit_count = 0
27            current_node = start_node
28          
29            while visit_order[current_node] == 0:
30                visit_count += 1
31                visit_order[current_node] = visit_count
32                current_node = edges[current_node]
33          
34            # Phase 2: Determine cycle length and total path length
35            cycle_length = 0
36            total_path_length = visit_count + result[current_node]
37          
38            # If the node we stopped at doesn't have a result yet,
39            # it means we found a new cycle
40            if result[current_node] == 0:
41                # Calculate cycle length: current visit count - visit order of cycle start + 1
42                cycle_length = visit_count - visit_order[current_node] + 1
43                total_path_length = visit_count
44          
45            # Phase 3: Assign results to all nodes in the path
46            current_node = start_node
47          
48            while result[current_node] == 0:
49                # Nodes get either the full path length or cycle length (if in cycle)
50                result[current_node] = max(total_path_length, cycle_length)
51                total_path_length -= 1
52                current_node = edges[current_node]
53      
54        return result
55
1class Solution {
2    public int[] countVisitedNodes(List<Integer> edges) {
3        int n = edges.size();
4        int[] result = new int[n];  // Stores the final answer for each node
5        int[] visitOrder = new int[n];  // Tracks visit order during traversal (0 means unvisited)
6      
7        // Process each unprocessed node
8        for (int startNode = 0; startNode < n; startNode++) {
9            if (result[startNode] == 0) {  // Node hasn't been processed yet
10                int stepCount = 0;
11                int currentNode = startNode;
12              
13                // Traverse the path until we hit a visited node or a processed node
14                while (visitOrder[currentNode] == 0) {
15                    visitOrder[currentNode] = ++stepCount;  // Mark visit order
16                    currentNode = edges.get(currentNode);  // Move to next node
17                }
18              
19                // Determine cycle length and total path length
20                int cycleLength = 0;
21                int totalPathLength = stepCount + result[currentNode];
22              
23                // If we hit an unprocessed node, we found a cycle
24                if (result[currentNode] == 0) {
25                    cycleLength = stepCount - visitOrder[currentNode] + 1;
26                }
27              
28                // Backtrack and assign results to all nodes in the path
29                currentNode = startNode;
30                while (result[currentNode] == 0) {
31                    // Nodes in cycle get cycleLength, others get decreasing totalPathLength
32                    result[currentNode] = Math.max(totalPathLength--, cycleLength);
33                    currentNode = edges.get(currentNode);
34                }
35            }
36        }
37      
38        return result;
39    }
40}
41
1class Solution {
2public:
3    vector<int> countVisitedNodes(vector<int>& edges) {
4        int n = edges.size();
5        vector<int> result(n, 0);      // Store the final answer for each node
6        vector<int> visitOrder(n, 0);  // Track visit order during traversal
7      
8        for (int startNode = 0; startNode < n; ++startNode) {
9            // Skip if this node has already been processed
10            if (result[startNode] != 0) {
11                continue;
12            }
13          
14            // Phase 1: Traverse and mark nodes with their visit order
15            int visitCount = 0;
16            int currentNode = startNode;
17          
18            while (visitOrder[currentNode] == 0) {
19                visitOrder[currentNode] = ++visitCount;
20                currentNode = edges[currentNode];
21            }
22          
23            // Phase 2: Determine cycle information
24            int cycleLength = 0;
25            int totalPathLength = visitCount + result[currentNode];
26          
27            // If we reached an unprocessed node, we found a cycle
28            if (result[currentNode] == 0) {
29                cycleLength = visitCount - visitOrder[currentNode] + 1;
30            }
31          
32            // Phase 3: Assign results to all nodes in the path
33            currentNode = startNode;
34            while (result[currentNode] == 0) {
35                // Nodes in cycle get cycleLength, nodes before cycle get decreasing totalPathLength
36                result[currentNode] = max(totalPathLength--, cycleLength);
37                currentNode = edges[currentNode];
38            }
39        }
40      
41        return result;
42    }
43};
44
1/**
2 * Counts the number of nodes that can be visited from each node in a directed graph
3 * where each node has exactly one outgoing edge.
4 * 
5 * @param edges - Array where edges[i] represents the destination node from node i
6 * @returns Array where result[i] is the number of nodes visitable from node i
7 */
8function countVisitedNodes(edges: number[]): number[] {
9    const nodeCount: number = edges.length;
10    const result: number[] = Array(nodeCount).fill(0);
11    const visitOrder: number[] = Array(nodeCount).fill(0);
12  
13    for (let startNode = 0; startNode < nodeCount; ++startNode) {
14        // Skip if this node has already been processed
15        if (result[startNode] === 0) {
16            let visitCount: number = 0;
17            let currentNode: number = startNode;
18          
19            // Traverse the path and mark visit order until we hit a visited node
20            while (visitOrder[currentNode] === 0) {
21                visitOrder[currentNode] = ++visitCount;
22                currentNode = edges[currentNode];
23            }
24          
25            // Calculate cycle length and total visitable nodes
26            let cycleLength: number = 0;
27            let totalVisitable: number = visitCount + result[currentNode];
28          
29            // If we hit an unprocessed node, we found a cycle
30            if (result[currentNode] === 0) {
31                cycleLength = visitCount - visitOrder[currentNode] + 1;
32            }
33          
34            // Backtrack and assign results to all nodes in the path
35            currentNode = startNode;
36            while (result[currentNode] === 0) {
37                // Nodes in cycle get cycleLength, others get decreasing total
38                result[currentNode] = Math.max(totalVisitable--, cycleLength);
39                currentNode = edges[currentNode];
40            }
41        }
42    }
43  
44    return result;
45}
46

Time and Space Complexity

Time Complexity: O(n)

Each node is visited at most twice in the algorithm:

  • Once during the initial traversal to detect cycles (the first while loop)
  • Once during the backtracking to assign answer values (the second while loop)

For each starting node i, the algorithm follows edges until it either hits a visited node or a node with a computed answer. Since each node can only be the starting point once (due to the if not ans[i] check), and each edge is traversed a constant number of times throughout the entire algorithm, the total time complexity is O(n).

Space Complexity: O(n)

The algorithm uses three arrays:

  • ans: stores the final answer for each node, size n
  • vis: tracks visitation order during cycle detection, size n
  • No recursive calls or additional data structures that grow with input size

Therefore, the space complexity is O(n), where n is the length of the array edges.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Not Resetting visit_order Between Different Starting Nodes

The Problem: The visit_order array is used to track the visiting sequence during traversal from each starting node. A critical mistake is forgetting that this array needs to be "logically reset" for each new traversal. While the code doesn't explicitly reset the entire array (which would be O(n) per starting node), it relies on the fact that visit_order[node] == 0 means "unvisited in current traversal."

Why This Breaks: If you don't properly handle the visit_order values from previous traversals, you might:

  • Incorrectly detect cycles where none exist
  • Calculate wrong cycle lengths
  • Miss actual cycles because nodes appear "already visited"

The Solution: The current code handles this correctly by only checking visit_order[current_node] == 0 in the while loop. However, there's an implicit assumption that needs attention: after processing a path, the visit_order values remain non-zero. This works because:

  1. We only process nodes where result[node] == 0 (unprocessed nodes)
  2. Once a node gets a result, we never traverse from it again
  3. The leftover visit_order values don't interfere with future traversals

Better Approach for Clarity:

# Option 1: Reset visit_order for the current path after processing
visited_nodes = []
while visit_order[current_node] == 0:
    visit_count += 1
    visit_order[current_node] = visit_count
    visited_nodes.append(current_node)
    current_node = edges[current_node]

# ... process the path ...

# Clean up visit_order for these nodes
for node in visited_nodes:
    visit_order[node] = 0

Or Option 2: Use a different tracking mechanism:

# Use a set for the current path instead of reusing visit_order
current_path = {}
visit_count = 0
current_node = start_node

while current_node not in current_path and result[current_node] == 0:
    visit_count += 1
    current_path[current_node] = visit_count
    current_node = edges[current_node]

Pitfall 2: Incorrect Cycle Length Calculation

The Problem: When calculating cycle_length = visit_count - visit_order[current_node] + 1, it's easy to get the formula wrong by:

  • Forgetting the +1
  • Using the wrong node's visit_order
  • Confusing visit_count with visit_order

Example of the Error: If we have a path: 0 → 1 → 2 → 3 → 1 (cycle: 1→2→3→1)

  • visit_order: [1, 2, 3, 4, ...]
  • When we reach node 1 again, visit_count = 4
  • Cycle length should be: 4 - 2 + 1 = 3 (nodes 1, 2, 3)
  • Common mistake: 4 - 2 = 2 (missing one node!)

The Solution: Always remember that cycle length = (current position - first occurrence position + 1). Draw out small examples to verify your formula.

Pitfall 3: Mishandling the Two Different Stop Conditions

The Problem: The traversal can stop for two different reasons:

  1. We hit a node with a known result (result[node] > 0)
  2. We hit a node we've seen in the current traversal (visit_order[node] > 0)

Mixing up these conditions leads to incorrect total_path_length calculations.

The Solution: Structure your conditions clearly:

if result[current_node] > 0:
    # Case 1: Hit a processed node
    cycle_length = 0
    total_path_length = visit_count + result[current_node]
elif visit_order[current_node] > 0:
    # Case 2: Found a new cycle
    cycle_length = visit_count - visit_order[current_node] + 1
    total_path_length = visit_count

This makes it explicit which case you're handling and prevents mixing up the logic.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More