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).
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 nodej
is the "parent" of nodei
- 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 EvaluatorExample 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 exactlyn
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)
- 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
- 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).
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
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
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!