Appearance
4.20.12 — Graph Valid Tree
LeetCode 261 · Medium · ★ Blind 75
The problem
Given n nodes and a list of undirected edges, return true if they form a valid tree.
n = 5, edges = [[0,1],[0,2],[0,3],[1,4]] → true
n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]] → false (there is a cycle)The pattern
A tree is a graph that is connected and has no cycle. Both conditions, and this problem exists because people check only one.
There is a shortcut that makes it much easier. For a graph with n nodes:
Connected + exactly
n − 1edges ⟹ no cycle.
Why: a connected graph needs at least n − 1 edges to link everything. Any extra edge must join two nodes already connected, which closes a cycle. So with exactly n − 1 edges, connectivity alone rules out cycles.
The same works the other way: acyclic + exactly n − 1 edges ⟹ connected.
So check two things, and one of them is a single comparison:
len(edges) == n - 1- the graph is connected
Check the edge count first. It is O(1) and it eliminates most invalid inputs before any traversal.
The solution — union-find
python
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
if len(edges) != n - 1:
return False # too few or too many, instantly wrong
parent = list(range(n))
def find(x: int) -> int:
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
for a, b in edges:
ra, rb = find(a), find(b)
if ra == rb:
return False # cycle
parent[rb] = ra # merge
return Truets
function validTree(n: number, edges: number[][]): boolean {
if (edges.length !== n - 1) return false;
const parent = Array.from({ length: n }, (_, i) => i);
function find(x: number): number {
while (parent[x] !== x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
for (const [a, b] of edges) {
const ra = find(a), rb = find(b);
if (ra === rb) return false;
parent[rb] = ra;
}
return true;
}With the edge count already checked, there is nothing left to verify at the end. If n − 1 edges all merged two different components, then n − 1 merges happened, and n components reduced by n − 1 leaves exactly one. Connected, automatically.
That is why the loop can return True immediately rather than counting components afterwards.
Union by size is omitted here because path halving alone is fast enough at this size. Include it if asked; it is two extra lines.
The DFS version
python
def validTree(self, n, edges):
if len(edges) != n - 1:
return False
adj = [[] for _ in range(n)]
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
seen = set()
stack = [0]
while stack:
node = stack.pop()
if node in seen:
continue
seen.add(node)
stack.extend(adj[node])
return len(seen) == n # reached everything → connectedWith the edge count already confirmed, the traversal only has to prove connectivity — reach every node from node 0. No cycle check is needed at all.
Without the edge-count shortcut, the DFS would have to detect cycles too, and for an undirected graph that means "I reached an already-visited node that is not the one I came from". Every undirected edge looks like a two-cycle otherwise, so the parent must be excluded:
python
def dfs(node, parent):
seen.add(node)
for nxt in adj[node]:
if nxt == parent: continue # the edge we arrived on
if nxt in seen: return False # a real cycle
if not dfs(nxt, node): return False
return TrueThat parent check is the undirected cycle rule, and it is different from the three-state rule for directed graphs in 4.20.8. Knowing which applies to which is worth more than either alone.
Edge cases
n = 1,edges = []— a single node with no edges is a valid tree.0 == 1 - 1✓.n = 0— arguably a tree, arguably not. Ask; the constraints usually rule it out.- A duplicate edge
[[0,1],[0,1]]— the second one finds both nodes already connected and returnsfalse. Correct: a duplicate edge is a two-node cycle. - A self-loop
[[0,0]]—find(0) == find(0), sofalse. Also correct.
Complexity
O(n \cdot \alpha(n)), effectively O(n), with O(n) space. The edge count check makes the whole thing O(1) for most invalid inputs.
Where this goes next
- Number of Connected Components — drop the tree requirement and count instead. 4.20.11.
- Redundant Connection —
nedges instead ofn − 1, so find the one that closes the cycle. 4.20.10. - Minimum Spanning Tree — build a tree from a weighted graph by adding the cheapest edges that do not close a cycle. Kruskal's algorithm is this loop with the edges sorted first. 4.21.
The n − 1 fact is the common thread, and it is worth carrying: a spanning tree of n nodes always has exactly n − 1 edges, which is why Kruskal's stops after n − 1 successful unions.
What the interviewer will push on
"What makes a graph a tree?" Connected and acyclic. Then give the shortcut: connected plus exactly n − 1 edges implies acyclic.
"Why does the edge count shortcut work?" n − 1 is the minimum for connectivity; any extra edge must close a cycle.
"How do you detect a cycle in an undirected graph with DFS?" Reaching a visited node that is not the parent. Contrast it with the three-state rule for directed graphs.
"What about a self-loop or a duplicate edge?" Both are cycles, and union-find catches both without special handling.
"Is a single node a tree?" Yes, and the arithmetic already agrees.
One thing to volunteer: state the two conditions and the shortcut before writing anything. Most wrong answers to this problem check connectivity and forget cycles, or the reverse — saying both up front shows you know why the shortcut is safe.
Next: 4.20.13 Word Ladder — the hardest problem in this chapter, where the graph is never built at all.