TL;DR
A tree = exactly n β 1 edges and connected. Check the count, then verify connectivity with DFS/BFS, or detect any cycle with union-find β all O(V + E).
Approach 1 β Brute force (count edges, then DFS for connectivity)
The naive-but-correct intuition: a tree on n nodes must have exactly n β 1 edges. With that count fixed, βno cycleβ and βconnectedβ become the same condition, so a single connectivity check settles it. Reject early if the count is wrong.
class Solution:
def validTree(self, n: int, edges: list[list[int]]) -> bool:
if len(edges) != n - 1:
return False
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
seen = set()
def dfs(node: int) -> None:
seen.add(node)
for nei in adj[node]:
if nei not in seen:
dfs(nei)
dfs(0)
return len(seen) == n
Complexity: O(V + E) time and space. Thereβs nothing to βkillβ here β this is efficient; itβs labeled brute force only because it leans on the edge-count shortcut rather than reasoning about cycles directly.
Approach 2 β BFS connectivity check
The insight: same n β 1 edge shortcut, but confirm connectivity with an iterative BFS from node 0. Preferred over recursive DFS when the tree could be a long chain (up to 2000 deep) that would risk Pythonβs recursion limit; DFS is a touch shorter to write.
from collections import deque
class Solution:
def validTree(self, n: int, edges: list[list[int]]) -> bool:
if len(edges) != n - 1:
return False
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
seen = {0}
queue = deque([0])
while queue:
node = queue.popleft()
for nei in adj[node]:
if nei not in seen:
seen.add(nei)
queue.append(nei)
return len(seen) == n
Walkthrough (n = 5, edges = [[0,1],[0,2],[0,3],[1,4]]):
len(edges) = 4 = 5 β 1 β. Adjacency: 0:[1,2,3], 1:[0,4], 2:[0], 3:[0], 4:[1].
- BFS from 0 visits 0, then 1,2,3, then 4.
seen = {0,1,2,3,4}, size 5 = n β True.
- On
[[0,1],[2,3]] (n=4): only 2 edges β 3, rejected instantly β False.
Complexity: O(V + E) time, O(V + E) space.
Approach 3 β Union-find (cycle detection while building)
The insight: process edges one at a time, keeping disjoint sets of connected nodes. For edge [u, v], if u and v are already in the same set, adding this edge would close a cycle β not a tree. Otherwise union them. After all edges, a single remaining component means connected. Union-find (disjoint-set union with path compression + union by rank) makes each operation near-constant. This version needs no separate edge-count check β it catches both failure modes directly.
class Solution:
def validTree(self, n: int, edges: list[list[int]]) -> bool:
parent = list(range(n))
rank = [0] * n
components = n
def find(x: int) -> int:
while parent[x] != x:
parent[x] = parent[parent[x]] # path compression
x = parent[x]
return x
for u, v in edges:
ru, rv = find(u), find(v)
if ru == rv:
return False # edge closes a cycle
if rank[ru] < rank[rv]:
ru, rv = rv, ru
parent[rv] = ru
if rank[ru] == rank[rv]:
rank[ru] += 1
components -= 1
return components == 1
Walkthrough (n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]]):
- Union 0-1, 1-2, 2-3 β all merge,
components drops 5β4β3β2.
- Edge
[1,3]: find(1) and find(3) already share a root (0/1/2/3 are one set) β cycle β return False.
Complexity: O(E Β· Ξ±(V)) time (Ξ± = inverse Ackermann, effectively constant), O(V) space. No adjacency list needed.
Common pitfalls
- Skipping the connectivity check after seeing
n β 1 edges β a graph can have n β 1 edges yet be a cycle plus an isolated node (disconnected). The count alone is not sufficient; you need count and connectivity (or the union-find cycle check).
- Treating the undirected graph as directed: add both
uβv and vβu to the adjacency list.
- In a plain-DFS cycle check (without the edge-count trick) you must skip the edge back to your parent, or every undirected edge looks like a 2-cycle. The union-find and count+connectivity approaches sidestep this entirely.
- Forgetting the
n = 1, edges = [] base case β 0 edges = 1 β 1, one node reached, valid tree.
Pattern takeaway
βIs this graph a tree?β reduces to two independent facts β exactly n β 1 edges and connected β and any two of {nβ1 edges, connected, acyclic} imply the third. Verify connectivity with one traversal, or let union-find detect the cycle as you add edges. Union-find is the natural tool whenever βwould adding this edge merge two already-joined nodes?β is the question.