Example 1
ready- Input
n = 5 edges = [ [ 0, 1 ], [ 0, 2 ], [ 0, 3 ], [ 1, 4 ] ]- Expected
true- Why
- four edges connect all five nodes without a loop
A network can reach every machine and still be wrong: one extra cable may create a loop. A tree allows neither gaps nor loops.
You're given n nodes labeled from 0 through n - 1. Each pair [a, b] is an
undirected edge, meaning you can travel between a and b in either direction.
Return true only when all the edges together form a valid tree.
That requires both conditions:
A single node with no edges is a valid tree.
1 <= n <= 2_0000 <= edges.length <= 5_0000..n - 1.Hints
A tree on n nodes always has exactly n - 1 edges. That count can reject many
inputs immediately, but it can't prove that the remaining edges are connected.
Start with every node in its own group. An edge should merge two different groups. What does it mean if both endpoints are already in the same group?
Union-find catches a cycle as soon as an edge joins an existing group to itself. After every edge, check that only one group remains.
Suppose the edges arrive one at a time. Can you reject the first edge that makes a tree impossible without rebuilding the graph after every arrival?
Visible cases
n = 5
edges = [
[
0,
1
],
[
0,
2
],
[
0,
3
],
[
1,
4
]
]truen = 5
edges = [
[
0,
1
],
[
1,
2
],
[
2,
3
],
[
1,
3
],
[
1,
4
]
]falsen = 1
edges = []trueInterview signal
A tree needs exactly n - 1 edges, so reject any other count first. Then build an
adjacency list, a list of each node's neighbors, and walk from node 0.
This is the graph cycle detection pattern: remember the parent you arrived from.
Seeing that parent again is just the same undirected edge; seeing any other visited
neighbor exposes a cycle. The walk must also visit all n nodes.
def valid_tree(n: int, edges: list[list[int]]) -> bool:
if len(edges) != n - 1:
return False
graph = [[] for _ in range(n)]
for a, b in edges:
graph[a].append(b)
graph[b].append(a)
seen = {0}
stack = [(0, -1)]
while stack:
node, parent = stack.pop()
for neighbor in graph[node]:
if neighbor == parent:
continue
if neighbor in seen:
return False
seen.add(neighbor)
stack.append((neighbor, node))
return len(seen) == nTime O(V + E), space O(V + E) — the walk touches each node and edge once, while the adjacency list stores both directions of every edge.
Union-find, also called disjoint-set union, tracks which nodes already belong to the same connected group. Each edge asks for the roots of its endpoints:
def valid_tree(n: int, edges: list[list[int]]) -> bool:
parent = list(range(n))
size = [1] * n
components = n
def find(node: int) -> int:
while node != parent[node]:
parent[node] = parent[parent[node]]
node = parent[node]
return node
for a, b in edges:
root_a = find(a)
root_b = find(b)
if root_a == root_b:
return False
if size[root_a] < size[root_b]:
root_a, root_b = root_b, root_a
parent[root_b] = root_a
size[root_a] += size[root_b]
components -= 1
return components == 1Path compression shortens future root searches, while union by size keeps trees
shallow. The inverse Ackermann function α(V) grows so slowly that it behaves like
a tiny constant at any practical input size.
Time O(V + E·α(V)), space O(V) for the parent and size arrays.
The useful union-find instinct is crisp: an undirected edge whose endpoints already share a root is exactly the edge that closes a cycle.
Ready to run.
3 cases are queued.
Visible testcase
Case 1
Expected
true