You are given a connected undirected graph with n nodes labeled from 1 to n. Initially, it contained no cycles and consisted of n-1 edges.
We have now added one additional edge to the graph. The edge has two different vertices chosen from 1 to n, and was not an edge that previously existed in the graph.
The graph is represented as an array edges of length n where edges[i] = [ai, bi] represents an edge between nodes ai and bi in the graph.
Return an edge that can be removed so that the graph is still a connected non-cyclical graph. If there are multiple answers, return the edge that appears last in the input edges.
Example 1:
Input: edges = [[1,2],[1,3],[3,4],[2,4]]
Output: [2,4]Example 2:
Input: edges = [[1,2],[1,3],[1,4],[3,4],[4,5]]
Output: [3,4]Constraints:
n == edges.length3 <= n <= 1001 <= edges[i][0] < edges[i][1] <= edges.length
You should aim for a solution as good or better than O(V + E) time and O(V + E) space, where V is the number vertices and E is the number of edges in the graph.
There will be only one edge that creates the cycle in the given problem. Why? Because the graph is initially acyclic, and a cycle is formed only after adding one extra edge that was not present in the graph initially. Can you think of an algorithm that helps determine whether the current connecting edge forms a cycle? Perhaps a component-oriented algorithm?
We can use the Union-Find (DSU) algorithm to create the graph from the given edges. While connecting the edges, if we fail to connect any edge, it means this is the redundant edge, and we return it. How would you implement this?
We create an instance of the DSU object and traverse through the given edges. For each edge, we attempt to connect the nodes using the union function. If the union function returns false, indicating that the current edge forms a cycle, we immediately return that edge.
Before attempting this problem, you should be comfortable with:
A tree cannot contain a cycle.
While adding edges one by one, the first edge that creates a cycle is the redundant connection.
For each new edge (u, v):
dfs to check if a cycle existsdfs revisits a node (not coming from its parent), a cycle is formed(u, v) in order:(u, v) to the graph.dfs starting from u to detect a cycle:class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
adj = [[] for _ in range(n + 1)]
def dfs(node, par):
if visit[node]:
return True
visit[node] = True
for nei in adj[node]:
if nei == par:
continue
if dfs(nei, node):
return True
return False
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
visit = [False] * (n + 1)
if dfs(u, -1):
return [u, v]
return []Where is the number of vertices and is the number of edges in the graph.
Instead of checking for a cycle after every edge, we build the whole graph once and find the cycle nodes in a single dfs.
Key idea:
n edges on n nodes, there is exactly one cycle.dfs, if we reach a node that is already visited, we just found the start of the cycle.After we have the set cycle (all nodes that lie on the cycle):
(u, v) where u and v are both in cycle.dfs once to detect the cycle:visited[].dfs enters an already visited node, mark it as cycleStart.cycle until reaching cycleStart, then stop marking.(u, v) where both endpoints are in cycle.[] (shouldn't happen for valid inputs).class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
adj = [[] for _ in range(n + 1)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
visit = [False] * (n + 1)
cycle = set()
cycleStart = -1
def dfs(node, par):
nonlocal cycleStart
if visit[node]:
cycleStart = node
return True
visit[node] = True
for nei in adj[node]:
if nei == par:
continue
if dfs(nei, node):
if cycleStart != -1:
cycle.add(node)
if node == cycleStart:
cycleStart = -1
return True
return False
dfs(1, -1)
for u, v in reversed(edges):
if u in cycle and v in cycle:
return [u, v]
return []Where is the number of vertices and is the number of edges in the graph.
This uses the "peel off leaves" idea (often called topological trimming).
Even though the graph is undirected, we can still remove nodes with degree 1 repeatedly:
1 cannot be inside a cycle (a cycle needs every node to have degree ≥ 2).Finally, the redundant edge must be an edge whose both ends are still in the cycle.
Because we need the last such edge in input order, we scan edges in reverse and return the first edge connecting two remaining cycle nodes.
indegree/degree of every node.1 to a queue.x and "remove" it (decrease its degree).y of x, decrease y's degree.y becomes degree 1, push y into the queue.(u, v) where both degree[u] > 0 and degree[v] > 0.class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
indegree = [0] * (n + 1)
adj = [[] for _ in range(n + 1)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
indegree[u] += 1
indegree[v] += 1
q = deque()
for i in range(1, n + 1):
if indegree[i] == 1:
q.append(i)
while q:
node = q.popleft()
indegree[node] -= 1
for nei in adj[node]:
indegree[nei] -= 1
if indegree[nei] == 1:
q.append(nei)
for u, v in reversed(edges):
if indegree[u] == 2 and indegree[v]:
return [u, v]
return []Where is the number of vertices and is the number of edges in the graph.
Use Disjoint Set Union (Union-Find) to track connected components while adding edges one by one.
(u, v):u and v are already in the same component, adding this edge creates a cycle.Because edges are processed in order, the first edge that fails to union is the answer.
(u, v):u and v.(u, v) → this edge creates a cycle.This works because a tree with n nodes has exactly n - 1 edges, and any extra edge must form a cycle.
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
par = [i for i in range(len(edges) + 1)]
rank = [1] * (len(edges) + 1)
def find(n):
p = par[n]
while p != par[p]:
par[p] = par[par[p]]
p = par[p]
return p
def union(n1, n2):
p1, p2 = find(n1), find(n2)
if p1 == p2:
return False
if rank[p1] > rank[p2]:
par[p2] = p1
rank[p1] += rank[p2]
else:
par[p1] = p2
rank[p2] += rank[p1]
return True
for n1, n2 in edges:
if not union(n1, n2):
return [n1, n2]Where is the number of vertices and is the number of edges in the graph. is used for amortized complexity.
The problem asks for the edge that appears last in the input among all edges that could be removed to break the cycle. A common mistake is returning the first edge that creates a cycle during processing. When using Union-Find, processing edges in order naturally returns the correct answer, but DFS-based approaches must scan edges in reverse to find the last valid edge.
This problem involves an undirected graph, but some solutions incorrectly handle edges as directed. When building the adjacency list, each edge (u, v) must be added in both directions. Forgetting this causes cycle detection to miss valid paths and return incorrect results.
When using DFS to detect cycles in an undirected graph, you must skip the parent node to avoid false positives. In an undirected graph, the edge (u, v) appears as both u -> v and v -> u in the adjacency list. Without parent tracking, DFS would immediately detect a "cycle" by going back to the node it just came from.