186. Validate an Undirected Tree
Problem
Return whether n vertices and an undirected edge list form a tree. Vertices are 0 through n-1. A tree is nonempty, connected and acyclic; n=0 returns false, while one vertex with no edges returns true. Self-loops and repeated edges are allowed as input but cannot form a tree.
Examples
Example 1
valid_tree(4,[(0,1),(1,2),(1,3)])
=> TrueThree edges connect four vertices without a cycle.
Example 2
valid_tree(4,[(0,1),(1,2),(2,0)])
=> FalseThe edge count matches, but vertex 3 is disconnected.
Example 3
valid_tree(1,[])
=> TrueA singleton is a tree.
Example 4
valid_tree(0,[])
=> FalseThe contract requires a nonempty tree.
Reveal the worked solution and reasoning
Why this mechanism works
A finite undirected tree has exactly n-1 edges. With that count established, connectivity is enough: a connected graph with a cycle would contain an extra edge beyond a spanning tree. Check the cheap edge count before building the adjacency list and traversing from vertex zero.
State and boundaries
seen contains vertices connected to zero through processed paths. The edge-count precheck fixes the only count compatible with a tree.
Protocol
- Reject n=0 or an edge count other than n-1.
- Build undirected adjacency.
- Traverse from zero, marking on push.
- Return whether all n vertices were reached.
Visualization
triangle 0-1-2-0 plus isolated 3
n=4, edges=3 passes n-1
only 3 reachable vertices -> rejectPython implementation
from kata_181 import adjacency_list
def valid_tree(n, edges):
if n == 0 or len(edges) != n - 1:
return False
adj = adjacency_list(n, edges)
seen, stack = {0}, [0]
while stack:
u = stack.pop()
for v in adj[u]:
if v not in seen:
seen.add(v)
stack.append(v)
return len(seen) == n
Worked execution
valid_tree(4,[(0,1),(1,2),(1,3)])
L4 valid_tree: if n == 0 or len(edges) != n - 1:
n=4; edges=[(0, 1), (1, 2), (1, 3)]
L6 valid_tree: adj = adjacency_list(n, edges)
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]
L7 valid_tree: seen, stack = {0}, [0]
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0}; stack=[0]
L9 valid_tree: u = stack.pop()
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0}; stack=[]; u=0
L10 valid_tree: for v in adj[u]:
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0}; stack=[]; u=0; v=1
L12 valid_tree: seen.add(v)
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0, 1}; stack=[]; u=0; v=1
L13 valid_tree: stack.append(v)
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0, 1}; stack=[1]; u=0; v=1
L9 valid_tree: u = stack.pop()
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0, 1}; stack=[]; u=1; v=1
... 6 intermediate state changes omitted ...
L13 valid_tree: stack.append(v)
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0, 1, 2, 3}; stack=[2, 3]; u=1; v=3
L9 valid_tree: u = stack.pop()
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0, 1, 2, 3}; stack=[2]; u=3; v=3
L10 valid_tree: for v in adj[u]:
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0, 1, 2, 3}; stack=[2]; u=3; v=1
L9 valid_tree: u = stack.pop()
n=4; edges=[(0, 1), (1, 2), (1, 3)]; adj=[[1], [0, 2, 3], [1], [1]]; seen={0, 1, 2, 3}; stack=[]; u=2; v=1Result: True. Three edges connect four vertices without a cycle.
valid_tree(4,[(0,1),(1,2),(2,0)])
L4 valid_tree: if n == 0 or len(edges) != n - 1:
n=4; edges=[(0, 1), (1, 2), (2, 0)]
L6 valid_tree: adj = adjacency_list(n, edges)
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]
L7 valid_tree: seen, stack = {0}, [0]
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0}; stack=[0]
L9 valid_tree: u = stack.pop()
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0}; stack=[]; u=0
L10 valid_tree: for v in adj[u]:
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0}; stack=[]; u=0; v=1
L12 valid_tree: seen.add(v)
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0, 1}; stack=[]; u=0; v=1
L13 valid_tree: stack.append(v)
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0, 1}; stack=[1]; u=0; v=1
L10 valid_tree: for v in adj[u]:
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0, 1}; stack=[1]; u=0; v=2
... 3 intermediate state changes omitted ...
L10 valid_tree: for v in adj[u]:
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0, 1, 2}; stack=[1]; u=2; v=1
L10 valid_tree: for v in adj[u]:
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0, 1, 2}; stack=[1]; u=2; v=0
L9 valid_tree: u = stack.pop()
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0, 1, 2}; stack=[]; u=1; v=0
L10 valid_tree: for v in adj[u]:
n=4; edges=[(0, 1), (1, 2), (2, 0)]; adj=[[1, 2], [0, 2], [1, 0], []]; seen={0, 1, 2}; stack=[]; u=1; v=2Result: False. The edge count matches, but vertex 3 is disconnected.
Why the solution is correct
Every tree passes the count and connectivity tests. Conversely, a connected graph on n vertices contains a spanning tree with n-1 edges. If the whole graph has only those many edges, none remain for a cycle, loop or parallel-edge cycle. Thus acceptance implies a tree.
Time and space
O(n+m) time and space after the count check; incompatible counts are rejected in O(1) when edges is a list.
Common mistakes
The n-1 count alone accepts a cyclic component plus an isolated vertex. Connectivity alone accepts graphs with extra cycle edges.
Reasoning card
State: seen contains vertices connected to zero through processed paths. The edge-count precheck fixes the only count compatible with a tree.
Invariant: A tree has n-1 edges and one component.
Decision to rehearse: Check edge count and connectivity or detect parent-aware cycles.
Finish: Return whether all n vertices were reached
Transfer exercise
Detect cycles during DFS with edge IDs rather than only a parent vertex, so parallel edges are handled correctly.