Facebook Pixel

2204. Distance to a Cycle in Undirected Graph πŸ”’

Problem Description

You have a connected undirected graph with n nodes (numbered from 0 to n - 1) that contains exactly one cycle. The graph is given as a list of edges where edges[i] = [node1_i, node2_i] represents a bidirectional edge between node1_i and node2_i.

The distance between two nodes is defined as the minimum number of edges needed to travel from one node to the other.

Your task is to find the minimum distance from each node to any node that is part of the cycle. Return an array answer of size n where answer[i] represents the minimum distance from node i to the closest node in the cycle.

For example, if a node is already part of the cycle, its distance would be 0. If a node is directly connected to a node in the cycle (but not in the cycle itself), its distance would be 1, and so on.

The key insight is that this graph has a special structure: it's a tree with one extra edge that creates exactly one cycle. Nodes that are part of the cycle will have distance 0, while nodes outside the cycle will have distances based on how many edges they need to traverse to reach the nearest cycle node.

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 connected undirected graph with n nodes and edges connecting them.

Is it a tree?

  • No: While the graph has tree-like properties, it's not a tree because it contains exactly one cycle. A tree by definition has no cycles, but this graph has one extra edge that creates a cycle.

Is the problem related to directed acyclic graphs?

  • No: The graph is undirected (edges are bidirectional) and contains a cycle, so it's neither directed nor acyclic.

Is the problem related to shortest paths?

  • Yes: We need to find the minimum distance (shortest path) from each node to the nearest node in the cycle.

Is the graph weighted?

  • No: All edges have the same weight (unweighted graph). The distance is simply the count of edges.

BFS

  • Yes: For unweighted shortest path problems, BFS is typically the algorithm of choice.

Conclusion: The flowchart suggests using BFS for this problem. However, the actual solution uses a clever variation that combines topological sorting with BFS-like layer processing. The algorithm removes nodes layer by layer from the outside (leaf nodes with degree 1) working inward until only the cycle remains, then calculates distances by backtracking through the removed nodes. This approach efficiently identifies the cycle and computes distances in O(n) time.

While pure DFS could also solve this problem by first detecting the cycle and then computing distances, the topological sorting approach is more elegant and efficient for this specific graph structure (tree + one extra edge).

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

Intuition

The key insight is recognizing the special structure of this graph: it's essentially a tree with one extra edge that creates a cycle. Think of it like a necklace (the cycle) with several chains hanging from it (the tree branches).

If we imagine peeling away the graph layer by layer from the outside, like peeling an onion, we would start with the outermost nodes (leaves) and work our way inward. The leaves are easy to identify - they're nodes with degree 1 (only one connection). These nodes cannot be part of the cycle because cycle nodes must have at least degree 2.

Here's the brilliant observation: if we keep removing leaf nodes and update the degrees of their neighbors, new leaves will emerge. We continue this process until we can't remove any more nodes. What remains? Only the cycle! Because cycle nodes all have degree at least 2 even after removing all external branches.

As we remove nodes, we can track which node each removed node was connected to before removal. This creates a "parent" relationship - each removed node remembers its connection closer to the cycle. This is captured in the array f[i] in the solution.

Once we've identified all non-cycle nodes (those that were removed) and their parent relationships, calculating distances becomes straightforward. We process the removed nodes in reverse order (from last removed to first removed). Why reverse? Because:

  • The last nodes removed were directly connected to the cycle (distance 1)
  • The second-to-last layer was connected to those nodes (distance 2)
  • And so on...

Each node's distance is simply its parent's distance plus 1: ans[i] = ans[f[i]] + 1. The cycle nodes themselves remain at distance 0 since they were never removed.

This approach elegantly avoids the need to explicitly detect the cycle or run BFS from multiple sources. Instead, it uses the graph's structure to naturally separate cycle nodes from non-cycle nodes through topological-style processing.

Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.

Solution Approach

Let's walk through the implementation step by step, following the topological sorting approach described in the reference solution.

Step 1: Build the Adjacency List

First, we convert the edge list into an adjacency list representation using a dictionary of sets:

g = defaultdict(set)
for a, b in edges:
    g[a].add(b)
    g[b].add(a)

Using sets instead of lists makes it easier to remove edges later and check degrees efficiently.

Step 2: Initialize Queue with Leaf Nodes

We find all nodes with degree 1 (leaf nodes) and add them to a queue:

q = deque(i for i in range(n) if len(g[i]) == 1)

These are the outermost nodes that definitely aren't part of the cycle.

Step 3: Layer-by-Layer Removal

We process nodes from the queue, removing them from the graph:

f = [0] * n  # tracks parent relationship
seq = []     # records removal order

while q:
    i = q.popleft()
    seq.append(i)  # record this node was removed
  
    for j in g[i]:  # for each neighbor
        g[j].remove(i)  # remove edge from neighbor to current node
        f[i] = j        # j is the parent of i (closer to cycle)
      
        if len(g[j]) == 1:  # if neighbor becomes a leaf
            q.append(j)     # add to queue for removal
  
    g[i].clear()  # clear all edges from current node

The key points here:

  • f[i] = j records that node j is the "parent" of node i - the node that's one step closer to the cycle
  • When a node's degree drops to 1 after removal, it becomes a new leaf and gets added to the queue
  • seq maintains the order of removal, which we'll use to calculate distances

Step 4: Calculate Distances in Reverse

After removal, nodes still in the graph (with degree β‰₯ 2) form the cycle. We calculate distances by processing removed nodes in reverse order:

ans = [0] * n  # initialize all distances to 0

for i in seq[::-1]:  # process in reverse order
    ans[i] = ans[f[i]] + 1

Why does this work?

  • Cycle nodes keep distance 0 (they were never removed, so not in seq)
  • The last nodes removed were directly connected to cycle nodes, so their distance is 0 + 1 = 1
  • Each earlier removed node is one step further from the cycle than its parent

Time and Space Complexity:

  • Time: O(n) - each node and edge is processed once
  • Space: O(n) - for the adjacency list, queue, and auxiliary arrays

This approach cleverly avoids explicitly detecting the cycle or running multiple BFS traversals, instead using the graph's structure to naturally identify cycle nodes and compute distances efficiently.

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 to illustrate the solution approach.

Consider a graph with 6 nodes and edges: [[0,1], [1,2], [2,3], [3,1], [3,4], [4,5]]

    0---1---2
        |   |
        +---3---4---5

The cycle consists of nodes 1, 2, and 3. Let's trace through the algorithm:

Step 1: Build adjacency list

  • Node 0: {1}
  • Node 1: {0, 2, 3}
  • Node 2: {1, 3}
  • Node 3: {1, 2, 4}
  • Node 4: {3, 5}
  • Node 5: {4}

Step 2: Find initial leaf nodes (degree = 1)

  • Nodes 0 and 5 are leaves (degree 1)
  • Queue: [0, 5]

Step 3: Layer-by-layer removal

Round 1: Remove node 0

  • Remove 0 from node 1's adjacency list
  • Set f[0] = 1 (node 1 is parent of node 0)
  • Node 1 now has degree 2 (still connected to 2 and 3)
  • seq = [0]

Round 2: Remove node 5

  • Remove 5 from node 4's adjacency list
  • Set f[5] = 4 (node 4 is parent of node 5)
  • Node 4 now has degree 1 (becomes a new leaf!)
  • Add node 4 to queue
  • seq = [0, 5]

Round 3: Remove node 4

  • Remove 4 from node 3's adjacency list
  • Set f[4] = 3 (node 3 is parent of node 4)
  • Node 3 now has degree 2 (still connected to 1 and 2)
  • seq = [0, 5, 4]

No more nodes with degree 1, so we stop. Nodes 1, 2, and 3 remain (the cycle).

Step 4: Calculate distances in reverse order

Initial state: ans = [0, 0, 0, 0, 0, 0]

Process seq in reverse: [4, 5, 0]

Process node 4:

  • ans[4] = ans[f[4]] + 1 = ans[3] + 1 = 0 + 1 = 1

Process node 5:

  • ans[5] = ans[f[5]] + 1 = ans[4] + 1 = 1 + 1 = 2

Process node 0:

  • ans[0] = ans[f[0]] + 1 = ans[1] + 1 = 0 + 1 = 1

Final Result: ans = [1, 0, 0, 0, 1, 2]

This means:

  • Nodes 1, 2, 3 (cycle nodes): distance 0
  • Nodes 0, 4 (directly connected to cycle): distance 1
  • Node 5 (two edges away from cycle): distance 2

The algorithm correctly identified the cycle nodes by process of elimination and calculated the shortest distances without explicitly finding the cycle or running BFS from multiple sources.

Solution Implementation

1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5    def distanceToCycle(self, n: int, edges: List[List[int]]) -> List[int]:
6        # Build adjacency list representation of the graph
7        graph = defaultdict(set)
8        for node_a, node_b in edges:
9            graph[node_a].add(node_b)
10            graph[node_b].add(node_a)
11      
12        # Initialize queue with all leaf nodes (degree 1)
13        queue = deque(node for node in range(n) if len(graph[node]) == 1)
14      
15        # parent[i] stores the parent of node i in the tree structure after removing leaves
16        parent = [0] * n
17      
18        # Store the order of nodes removed (leaves to inner nodes)
19        removal_order = []
20      
21        # Process nodes layer by layer, removing leaves
22        while queue:
23            current_node = queue.popleft()
24            removal_order.append(current_node)
25          
26            # Process the single neighbor of this leaf node
27            for neighbor in graph[current_node]:
28                graph[neighbor].remove(current_node)
29                parent[current_node] = neighbor
30              
31                # If neighbor becomes a leaf after removal, add to queue
32                if len(graph[neighbor]) == 1:
33                    queue.append(neighbor)
34          
35            # Clear the current node's adjacency list
36            graph[current_node].clear()
37      
38        # Calculate distances from each node to the cycle
39        # Nodes remaining in graph are part of the cycle (distance 0)
40        distances = [0] * n
41      
42        # Process removed nodes in reverse order to calculate distances
43        # Each removed node's distance is its parent's distance + 1
44        for node in reversed(removal_order):
45            distances[node] = distances[parent[node]] + 1
46      
47        return distances
48
1class Solution {
2    public int[] distanceToCycle(int n, int[][] edges) {
3        // Build adjacency list representation of the graph
4        Set<Integer>[] graph = new Set[n];
5        Arrays.setAll(graph, index -> new HashSet<>());
6      
7        // Add edges to the graph (undirected)
8        for (int[] edge : edges) {
9            int nodeA = edge[0];
10            int nodeB = edge[1];
11            graph[nodeA].add(nodeB);
12            graph[nodeB].add(nodeA);
13        }
14      
15        // Queue for BFS to process leaf nodes (nodes with degree 1)
16        Deque<Integer> leafQueue = new ArrayDeque<>();
17      
18        // Find all initial leaf nodes
19        for (int node = 0; node < n; ++node) {
20            if (graph[node].size() == 1) {
21                leafQueue.offer(node);
22            }
23        }
24      
25        // Parent array to track which node leads to the cycle for each leaf
26        int[] parent = new int[n];
27      
28        // Stack to store the order of processed nodes for distance calculation
29        Deque<Integer> processedNodes = new ArrayDeque<>();
30      
31        // Process leaf nodes layer by layer (topological sort)
32        while (!leafQueue.isEmpty()) {
33            int currentNode = leafQueue.poll();
34            processedNodes.push(currentNode);
35          
36            // Process the single neighbor of this leaf node
37            for (int neighbor : graph[currentNode]) {
38                // Remove the edge between current node and its neighbor
39                graph[neighbor].remove(currentNode);
40              
41                // Mark the neighbor as parent of current node
42                parent[currentNode] = neighbor;
43              
44                // If neighbor becomes a leaf after removal, add to queue
45                if (graph[neighbor].size() == 1) {
46                    leafQueue.offer(neighbor);
47                }
48            }
49        }
50      
51        // Calculate distances from each node to the cycle
52        int[] distances = new int[n];
53      
54        // Process nodes in reverse order to calculate distances
55        // Nodes in cycle have distance 0, others have distance = parent's distance + 1
56        while (!processedNodes.isEmpty()) {
57            int node = processedNodes.pop();
58            distances[node] = distances[parent[node]] + 1;
59        }
60      
61        return distances;
62    }
63}
64
1class Solution {
2public:
3    vector<int> distanceToCycle(int n, vector<vector<int>>& edges) {
4        // Build adjacency list representation of the graph using sets
5        // Sets allow efficient insertion and deletion of edges
6        unordered_set<int> adjacencyList[n];
7        for (auto& edge : edges) {
8            int nodeA = edge[0];
9            int nodeB = edge[1];
10            adjacencyList[nodeA].insert(nodeB);
11            adjacencyList[nodeB].insert(nodeA);
12        }
13      
14        // Find all leaf nodes (nodes with degree 1) and add them to queue
15        queue<int> leafQueue;
16        for (int node = 0; node < n; ++node) {
17            if (adjacencyList[node].size() == 1) {
18                leafQueue.push(node);
19            }
20        }
21      
22        // parent[i] stores the parent of node i in the tree structure
23        // (the node connected to i that remains after i is removed)
24        int parent[n];
25      
26        // Store the order in which nodes are removed (leaf nodes first)
27        int removalOrder[n];
28        int removalCount = 0;
29      
30        // Process leaf nodes iteratively (topological sort approach)
31        // Remove leaves layer by layer until only the cycle remains
32        while (!leafQueue.empty()) {
33            int currentNode = leafQueue.front();
34            leafQueue.pop();
35          
36            // Record this node in removal order
37            removalOrder[removalCount++] = currentNode;
38          
39            // Process the single neighbor of this leaf node
40            for (int neighbor : adjacencyList[currentNode]) {
41                // Remove edge from neighbor to current node
42                adjacencyList[neighbor].erase(currentNode);
43              
44                // Record parent relationship for distance calculation later
45                parent[currentNode] = neighbor;
46              
47                // If neighbor becomes a leaf after removal, add to queue
48                if (adjacencyList[neighbor].size() == 1) {
49                    leafQueue.push(neighbor);
50                }
51            }
52          
53            // Clear the current node's adjacency list
54            adjacencyList[currentNode].clear();
55        }
56      
57        // Initialize result array to store distances to cycle
58        vector<int> distanceToNearestCycle(n, 0);
59      
60        // Calculate distances by processing removed nodes in reverse order
61        // Nodes on the cycle have distance 0 (not in removalOrder)
62        // Each removed node's distance is its parent's distance + 1
63        for (; removalCount > 0; --removalCount) {
64            int node = removalOrder[removalCount - 1];
65            distanceToNearestCycle[node] = distanceToNearestCycle[parent[node]] + 1;
66        }
67      
68        return distanceToNearestCycle;
69    }
70};
71
1/**
2 * Calculates the distance from each node to the nearest node in the cycle
3 * @param n - The number of nodes in the graph
4 * @param edges - The edges of the graph (undirected)
5 * @returns An array where each element represents the distance from that node to the cycle
6 */
7function distanceToCycle(n: number, edges: number[][]): number[] {
8    // Build adjacency list representation of the graph
9    const adjacencyList: Set<number>[] = new Array(n)
10        .fill(0)
11        .map(() => new Set<number>());
12  
13    for (const [nodeA, nodeB] of edges) {
14        adjacencyList[nodeA].add(nodeB);
15        adjacencyList[nodeB].add(nodeA);
16    }
17  
18    // Queue to store nodes with degree 1 (leaf nodes)
19    const leafQueue: number[] = [];
20  
21    // Find all leaf nodes (nodes with degree 1)
22    for (let node = 0; node < n; ++node) {
23        if (adjacencyList[node].size === 1) {
24            leafQueue.push(node);
25        }
26    }
27  
28    // Parent array to track the parent of each node during removal
29    const parent: number[] = Array(n).fill(0);
30  
31    // Sequence of nodes removed (used for backtracking)
32    const removalSequence: number[] = [];
33  
34    // Remove leaf nodes layer by layer (topological sort approach)
35    while (leafQueue.length > 0) {
36        const currentNode = leafQueue.pop()!;
37        removalSequence.push(currentNode);
38      
39        // Process all neighbors of the current leaf node
40        for (const neighbor of adjacencyList[currentNode]) {
41            // Remove edge between current node and its neighbor
42            adjacencyList[neighbor].delete(currentNode);
43          
44            // Store the parent relationship
45            parent[currentNode] = neighbor;
46          
47            // If neighbor becomes a leaf after removal, add to queue
48            if (adjacencyList[neighbor].size === 1) {
49                leafQueue.push(neighbor);
50            }
51        }
52      
53        // Clear the adjacency list for the removed node
54        adjacencyList[currentNode].clear();
55    }
56  
57    // Initialize result array with 0 (nodes in cycle have distance 0)
58    const distances: number[] = Array(n).fill(0);
59  
60    // Backtrack through removal sequence to calculate distances
61    // Nodes removed last are processed first (closer to cycle)
62    while (removalSequence.length > 0) {
63        const node = removalSequence.pop()!;
64        // Distance is parent's distance + 1
65        distances[node] = distances[parent[node]] + 1;
66    }
67  
68    return distances;
69}
70

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs the following operations:

  • Building the adjacency list from edges: O(n) since there are exactly n edges in a tree with one extra edge forming a cycle
  • Initial queue construction to find all nodes with degree 1: O(n)
  • BFS-like traversal where each node is processed exactly once: O(n)
    • For each node, we iterate through its neighbors, but since each edge is processed at most twice (once from each endpoint), the total work across all iterations is O(n)
  • Reversing the sequence and calculating distances: O(n)

Overall time complexity: O(n) + O(n) + O(n) + O(n) = O(n)

Space Complexity: O(n)

The algorithm uses:

  • Graph adjacency list g: O(n) to store all edges
  • Queue q: O(n) in the worst case
  • Array f: O(n) to store parent relationships
  • Array seq: O(n) to store the traversal sequence
  • Array ans: O(n) for the final answer

Overall space complexity: O(n)

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

Common Pitfalls

Pitfall 1: Incorrectly Handling the Parent Assignment

The Problem: A common mistake is assigning the parent relationship incorrectly or at the wrong time. Some might try to assign parent[j] = i instead of parent[i] = j, or attempt to track parents for all nodes including cycle nodes.

Why It Happens:

# Incorrect approach - assigning parent in wrong direction
for neighbor in graph[current_node]:
    parent[neighbor] = current_node  # WRONG!

The confusion arises because in typical tree traversals, we often think of the parent as the node we came from. However, in this algorithm, we're removing leaves and moving inward, so the "parent" is actually the node closer to the cycle.

The Solution:

# Correct approach - parent is the neighbor (closer to cycle)
for neighbor in graph[current_node]:
    graph[neighbor].remove(current_node)
    parent[current_node] = neighbor  # current_node's parent is its neighbor

Pitfall 2: Not Properly Removing Edges from Both Directions

The Problem: Forgetting to remove the edge from both directions in the undirected graph, or clearing the current node's adjacency list too early.

Why It Happens:

# Incorrect - only removing from one direction
while queue:
    current_node = queue.popleft()
    graph[current_node].clear()  # Clearing too early!
  
    for neighbor in graph[current_node]:  # Now this loop won't execute
        # ... rest of logic

The Solution:

# Correct order of operations
while queue:
    current_node = queue.popleft()
    removal_order.append(current_node)
  
    for neighbor in graph[current_node]:
        graph[neighbor].remove(current_node)  # Remove from neighbor first
        parent[current_node] = neighbor
      
        if len(graph[neighbor]) == 1:
            queue.append(neighbor)
  
    graph[current_node].clear()  # Clear after processing all neighbors

Pitfall 3: Using Lists Instead of Sets for Adjacency List

The Problem: Using lists for the adjacency list makes edge removal inefficient and degree checking cumbersome.

Why It Happens:

# Inefficient approach using lists
graph = defaultdict(list)
for a, b in edges:
    graph[a].append(b)
    graph[b].append(a)

# Later, removing an edge becomes O(n) operation
graph[neighbor].remove(current_node)  # O(n) for list

The Solution:

# Efficient approach using sets
graph = defaultdict(set)
for a, b in edges:
    graph[a].add(b)
    graph[b].add(a)

# Edge removal is O(1) with sets
graph[neighbor].remove(current_node)  # O(1) for set

Pitfall 4: Misunderstanding the Distance Calculation

The Problem: Attempting to calculate distances during the forward pass (while removing nodes) instead of in reverse order.

Why It Happens:

# Incorrect - trying to calculate distance while removing
while queue:
    current_node = queue.popleft()
    for neighbor in graph[current_node]:
        distances[current_node] = distances[neighbor] + 1  # neighbor's distance unknown!

At this point, we haven't determined which nodes are in the cycle yet, so we can't calculate accurate distances.

The Solution:

# Correct - calculate distances in reverse after identifying cycle nodes
for node in reversed(removal_order):
    distances[node] = distances[parent[node]] + 1

By processing in reverse order, we ensure that a node's parent's distance is already calculated before we use it, since parents are always closer to the cycle and were removed later (or not at all if they're in the cycle).

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

What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

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

Load More