LeetCode 2204 - Distance to a Cycle in Undirected Graph
This problem gives us a connected undirected graph with n nodes and exactly n edges. A connected graph with n nodes normally forms a tree when it has n - 1 edges. Since this graph has one extra edge, it contains exactly one cycle.
Difficulty: 🔴 Hard
Topics: Depth-First Search, Breadth-First Search, Graph Theory, Topological Sort
Solution
Problem Understanding
This problem gives us a connected undirected graph with n nodes and exactly n edges. A connected graph with n nodes normally forms a tree when it has n - 1 edges. Since this graph has one extra edge, it contains exactly one cycle.
Our goal is to compute, for every node, the minimum distance from that node to any node that belongs to the cycle.
The graph is undirected, so every edge can be traversed in both directions. The distance between two nodes is simply the minimum number of edges along a path connecting them.
For nodes that are part of the cycle itself, the answer is 0, because they are already on the cycle. For nodes outside the cycle, we must determine how many edges away they are from the nearest cycle node.
The constraints are important:
ncan be as large as100000- The graph is connected
- There is exactly one cycle
These constraints immediately tell us that we need a near linear solution. Any algorithm with quadratic complexity would be too slow.
The most important structural property is that every node not on the cycle belongs to a tree attached to the cycle. Since there is exactly one cycle, removing the cycle would leave several independent trees rooted at cycle nodes.
Several edge cases are important:
- The entire graph itself could be just a cycle, in which case every answer is
0 - Some nodes may be long chains extending from the cycle
- The cycle may be very small, such as a triangle
- The graph may be extremely skewed, producing very deep branches
- Recursive DFS could overflow the stack for large inputs, so iterative approaches are safer
Approaches
Brute Force Approach
A brute force solution would first determine which nodes belong to the cycle, then for every node perform a BFS to find the nearest cycle node.
One possible implementation is:
- Detect all cycle nodes
- For every node:
- Run BFS
- Stop once a cycle node is reached
- Record the distance
This works because BFS in an unweighted graph always finds the shortest path.
However, this is far too expensive. Running BFS from every node costs O(n * (n + e)), which becomes O(n^2) since e = n.
With n = 100000, this is not feasible.
Optimal Approach
The key insight is that nodes not on the cycle behave like leaves in trees attached to the cycle.
If we repeatedly remove leaf nodes, eventually only the cycle nodes remain.
This is similar to topological trimming:
- Nodes with degree
1cannot belong to a cycle - Remove all leaves
- Their neighbors may become leaves afterward
- Continue until no more leaves exist
After this process, the remaining nodes are exactly the cycle nodes.
Once we know the cycle nodes, we can perform a multi source BFS starting from all cycle nodes simultaneously.
This BFS computes the shortest distance from every node to the nearest cycle node in linear time.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Run BFS from every node |
| Optimal | O(n) | O(n) | Leaf trimming + multi source BFS |
Algorithm Walkthrough
Step 1, Build the Graph
Create an adjacency list for the graph and compute the degree of every node.
The adjacency list allows efficient traversal of neighbors, and the degree array helps identify leaves.
Step 2, Find Initial Leaves
Any node with degree 1 is definitely not part of the cycle.
Push all such nodes into a queue.
These nodes form the starting layer for the trimming process.
Step 3, Trim Leaves Iteratively
Process the queue:
- Remove a leaf node
- Mark it as not part of the cycle
- Reduce the degree of all its neighbors
- If a neighbor becomes degree
1, push it into the queue
This process peels away outer tree layers one by one.
Eventually, only cycle nodes remain because cycle nodes always maintain degree at least 2 within the remaining graph.
Step 4, Initialize BFS from Cycle Nodes
After trimming finishes:
- Nodes not removed are cycle nodes
- Their distance is
0
Push all cycle nodes into a BFS queue.
Step 5, Run Multi Source BFS
Perform BFS starting from all cycle nodes simultaneously.
For each popped node:
- Visit its neighbors
- If a neighbor has not been assigned a distance:
- Set distance = current distance + 1
- Push neighbor into the queue
Because BFS expands level by level, the first time a node is visited gives the shortest distance to the cycle.
Why it works
The trimming phase correctly identifies cycle nodes because any node outside the cycle eventually becomes a leaf after repeatedly removing outer layers.
Cycle nodes are never removed because every cycle node always retains at least two neighbors inside the cycle.
The multi source BFS is correct because BFS in an unweighted graph computes shortest path distances. Starting BFS from all cycle nodes simultaneously guarantees that each node receives the minimum distance to any cycle node.
Python Solution
from collections import deque
from typing import List
class Solution:
def distanceToCycle(self, n: int, edges: List[List[int]]) -> List[int]:
graph = [[] for _ in range(n)]
degree = [0] * n
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
degree[u] += 1
degree[v] += 1
queue = deque()
for node in range(n):
if degree[node] == 1:
queue.append(node)
in_cycle = [True] * n
while queue:
node = queue.popleft()
in_cycle[node] = False
for neighbor in graph[node]:
degree[neighbor] -= 1
if degree[neighbor] == 1:
queue.append(neighbor)
distance = [-1] * n
queue = deque()
for node in range(n):
if in_cycle[node]:
distance[node] = 0
queue.append(node)
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if distance[neighbor] == -1:
distance[neighbor] = distance[node] + 1
queue.append(neighbor)
return distance
The implementation begins by constructing the adjacency list and computing node degrees.
The first BFS style process performs leaf trimming. Every node with degree 1 is removed, and its neighbors have their degrees reduced. Nodes that survive this process are exactly the cycle nodes.
The second BFS computes shortest distances. Since all cycle nodes are inserted initially with distance 0, the BFS naturally expands outward layer by layer, assigning the minimum distance to every non cycle node.
The use of iterative queues avoids recursion depth problems that could occur with deep graphs.
Go Solution
package main
import "container/list"
func distanceToCycle(n int, edges [][]int) []int {
graph := make([][]int, n)
degree := make([]int, n)
for _, edge := range edges {
u := edge[0]
v := edge[1]
graph[u] = append(graph[u], v)
graph[v] = append(graph[v], u)
degree[u]++
degree[v]++
}
queue := list.New()
for i := 0; i < n; i++ {
if degree[i] == 1 {
queue.PushBack(i)
}
}
inCycle := make([]bool, n)
for i := 0; i < n; i++ {
inCycle[i] = true
}
for queue.Len() > 0 {
front := queue.Front()
queue.Remove(front)
node := front.Value.(int)
inCycle[node] = false
for _, neighbor := range graph[node] {
degree[neighbor]--
if degree[neighbor] == 1 {
queue.PushBack(neighbor)
}
}
}
distance := make([]int, n)
for i := 0; i < n; i++ {
distance[i] = -1
}
queue = list.New()
for i := 0; i < n; i++ {
if inCycle[i] {
distance[i] = 0
queue.PushBack(i)
}
}
for queue.Len() > 0 {
front := queue.Front()
queue.Remove(front)
node := front.Value.(int)
for _, neighbor := range graph[node] {
if distance[neighbor] == -1 {
distance[neighbor] = distance[node] + 1
queue.PushBack(neighbor)
}
}
}
return distance
}
The Go implementation follows the same logic as the Python version.
The primary difference is queue management. Go does not have a built in deque like Python, so container/list is used for BFS queues.
Slices are used for adjacency lists and arrays. Since all distances are bounded by n, integer overflow is not a concern.
Worked Examples
Example 1
n = 7
edges = [[1,2],[2,4],[4,3],[3,1],[0,1],[5,2],[6,5]]
Initial Graph
| Node | Neighbors | Degree |
|---|---|---|
| 0 | [1] | 1 |
| 1 | [2,3,0] | 3 |
| 2 | [1,4,5] | 3 |
| 3 | [4,1] | 2 |
| 4 | [2,3] | 2 |
| 5 | [2,6] | 2 |
| 6 | [5] | 1 |
Leaf Trimming
Initial queue:
[0, 6]
Process node 0:
- Remove 0
- Degree of 1 becomes 2
Queue:
[6]
Process node 6:
- Remove 6
- Degree of 5 becomes 1
- Push 5
Queue:
[5]
Process node 5:
- Remove 5
- Degree of 2 becomes 2
Queue becomes empty.
Remaining cycle nodes:
1, 2, 3, 4
Multi Source BFS
Initial distances:
| Node | Distance |
|---|---|
| 1 | 0 |
| 2 | 0 |
| 3 | 0 |
| 4 | 0 |
Queue:
[1,2,3,4]
From node 1:
- Visit
0 - distance[0] = 1
From node 2:
- Visit
5 - distance[5] = 1
From node 5:
- Visit
6 - distance[6] = 2
Final result:
[1,0,0,0,0,1,2]
Example 2
n = 9
edges = [[0,1],[1,2],[0,2],[2,6],[6,7],[6,8],[0,3],[3,4],[3,5]]
Initial Degrees
| Node | Degree |
|---|---|
| 0 | 3 |
| 1 | 2 |
| 2 | 3 |
| 3 | 3 |
| 4 | 1 |
| 5 | 1 |
| 6 | 3 |
| 7 | 1 |
| 8 | 1 |
Initial leaves:
[4,5,7,8]
Trimming Process
Remove 4:
- Degree of 3 becomes 2
Remove 5:
- Degree of 3 becomes 1
- Push 3
Remove 7:
- Degree of 6 becomes 2
Remove 8:
- Degree of 6 becomes 1
- Push 6
Remove 3:
- Degree of 0 becomes 2
Remove 6:
- Degree of 2 becomes 2
Remaining cycle nodes:
0, 1, 2
BFS Distances
Cycle nodes start with distance 0.
BFS expands:
- Node 3 gets distance 1
- Node 6 gets distance 1
- Nodes 4 and 5 get distance 2
- Nodes 7 and 8 get distance 2
Final result:
[0,0,0,1,2,2,1,2,2]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each edge and node is processed a constant number of times |
| Space | O(n) | Adjacency list, degree array, queue, and distance array |
The graph contains exactly n edges, so the number of edges is linear in the number of nodes.
During leaf trimming, every node enters the queue at most once, and every edge is examined at most twice.
During BFS, each node and edge is again processed at most once.
Therefore, the overall complexity remains linear.
Test Cases
from typing import List
class Solution:
def distanceToCycle(self, n: int, edges: List[List[int]]) -> List[int]:
from collections import deque
graph = [[] for _ in range(n)]
degree = [0] * n
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
degree[u] += 1
degree[v] += 1
queue = deque()
for i in range(n):
if degree[i] == 1:
queue.append(i)
in_cycle = [True] * n
while queue:
node = queue.popleft()
in_cycle[node] = False
for neighbor in graph[node]:
degree[neighbor] -= 1
if degree[neighbor] == 1:
queue.append(neighbor)
distance = [-1] * n
queue = deque()
for i in range(n):
if in_cycle[i]:
distance[i] = 0
queue.append(i)
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if distance[neighbor] == -1:
distance[neighbor] = distance[node] + 1
queue.append(neighbor)
return distance
sol = Solution()
# Example 1
assert sol.distanceToCycle(
7,
[[1,2],[2,4],[4,3],[3,1],[0,1],[5,2],[6,5]]
) == [1,0,0,0,0,1,2]
# Example 2
assert sol.distanceToCycle(
9,
[[0,1],[1,2],[0,2],[2,6],[6,7],[6,8],[0,3],[3,4],[3,5]]
) == [0,0,0,1,2,2,1,2,2]
# Smallest possible cycle, all nodes in cycle
assert sol.distanceToCycle(
3,
[[0,1],[1,2],[2,0]]
) == [0,0,0]
# Single chain attached to cycle
assert sol.distanceToCycle(
5,
[[0,1],[1,2],[2,0],[2,3],[3,4]]
) == [0,0,0,1,2]
# Multiple branches attached to cycle
assert sol.distanceToCycle(
8,
[[0,1],[1,2],[2,0],[0,3],[3,4],[1,5],[5,6],[6,7]]
) == [0,0,0,1,2,1,2,3]
# Long linear branch
assert sol.distanceToCycle(
6,
[[0,1],[1,2],[2,0],[2,3],[3,4],[4,5]]
) == [0,0,0,1,2,3]
# Cycle not involving node 0
assert sol.distanceToCycle(
6,
[[1,2],[2,3],[3,1],[0,1],[3,4],[4,5]]
) == [1,0,0,0,1,2]
| Test | Why |
|---|---|
| Example 1 | Verifies standard mixed structure |
| Example 2 | Verifies multiple trees attached to cycle |
| Pure cycle graph | Ensures all answers become zero |
| Single branch | Tests increasing distance propagation |
| Multiple branches | Tests BFS expansion from several cycle nodes |
| Long chain | Tests deep distance computation |
| Cycle excluding node 0 | Ensures no assumptions about cycle position |
Edge Cases
One important edge case occurs when the entire graph is a cycle. In this situation, no node has degree 1, so the trimming queue starts empty. The algorithm correctly identifies every node as part of the cycle, and the BFS initializes all distances to 0.
Another important case is a very deep branch attached to the cycle. A recursive DFS implementation could overflow the call stack when the depth approaches 100000. This implementation avoids that issue entirely by using iterative queues for both trimming and BFS.
A third important edge case involves cycle placement. The cycle can appear anywhere in the graph and does not necessarily involve node 0. Some incorrect solutions accidentally assume the cycle begins near the start of traversal. This algorithm avoids such assumptions because it identifies cycle nodes purely through degree reduction.
A subtle edge case occurs when removing one leaf causes another node to become a leaf. If the algorithm does not dynamically update degrees and enqueue newly formed leaves, some non cycle nodes would incorrectly remain marked as cycle nodes. The implementation handles this correctly by decrementing neighbor degrees immediately during trimming.