Graph Valid Tree

A tree is a special shape. Everything is connected, and there are no loops anywhere. Graph Valid Tree asks you to check if a given graph fits that shape. This question rewards a clear definition of a tree, and it gives you one more chance to practice union-find.

🎯 The Problem

You answer one yes-or-no question: does this graph form a valid tree? Here are the rules.

  • You get nodes labeled 0 to n - 1 and a list of edges.
  • Each edge connects two nodes both ways, so the graph is undirected.
  • A valid tree needs every node reachable from every other node.
  • A valid tree has no cycles. A cycle is a loop that lets you return to a node by a different path.
  • A handy shortcut: a tree with n nodes always has exactly n - 1 edges.

For example, with 5 nodes and edges [[0,1],[0,2],[0,3],[1,4]], everything is reachable and there is no loop. So this is a valid tree.

Input: n = 5, edges = [[0,1],[0,2],[0,3],[1,4]]
Output: true
Explanation: All 5 nodes are connected and there is no cycle.

Here is the graph. It branches out from node 0 with no loops.

0

1

2

3

4

🐢 Approach 1: DFS Connectivity and Cycle Check (Alternative)

The idea in one line: walk the graph once, catch any cycle on the way, then check that every node was reached.

The idea:

  • Depth-first search follows one path as deep as it goes before backing up.
  • Build an adjacency list of neighbors.
  • Start at node 0 and visit everything reachable.

How it works:

  • Mark each node visited and remember the node you came from.
  • If you reach a visited node that is not the one you came from, that is a cycle. Not a tree.
  • After the walk, check whether every node was visited.
  • If some node was never reached, the graph is split. Not connected. Not a tree.

Why it is weak:

  • You must track the parent carefully or you flag a false cycle.
  • It needs the full adjacency list plus a separate connectivity scan.

Here is the DFS tree-check code:

graph_valid_tree_dfs.py
def valid_tree(n, edges):
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 = set()
def dfs(node, parent):
seen.add(node)
for nei in graph[node]:
if nei == parent: continue
if nei in seen or not dfs(nei, node): return False
return True
return dfs(0, -1) and len(seen) == n

⚡ Approach 2: Union-Find (Best)

The idea in one line: check the edge count is n - 1, then union each edge and fail the moment two endpoints already share a group.

The idea:

  • Union-find tracks which group each node is in and merges groups fast.
  • It is also called a disjoint set, because the groups never overlap.
  • Each node points to a parent. Following parents up leads to the root.
  • Two nodes are in the same group when they share a root.

How it works:

  • If the edge count is not n - 1, return false right away.
  • Start with every node in its own group.
  • Read each edge. Find the root of both nodes.
  • If they already share a root, this edge closes a loop. Not a tree.
  • Otherwise merge their groups and continue.
  • find(x) walks up to the root. union(a, b) joins two groups.
  • Path compression points nodes straight at the root.
  • Union by rank attaches the shorter group under the taller one.

Why it is fast:

  • With n - 1 edges and no cycle, the graph must be connected. No separate scan needed.
  • Path compression and union by rank make each operation almost constant time.

Here is the decision flow for the optimal check.

No

Yes

Yes

No, for all edges

Is edge count n - 1?

Not a tree

Union each edge

Two nodes share a root?

Cycle found, not a tree

Valid tree

Steps to Solve

  1. If the number of edges is not n - 1, return false right away.
  2. Create a parent array where every node is its own parent, and a rank array of zeros.
  3. Write find(x) that walks up to the root and points each node along the way straight to the root.
  4. Write union(a, b) that finds both roots and attaches the lower-rank root under the higher-rank one.
  5. For each edge, find both roots. If they match, a cycle exists, so return false.
  6. Otherwise union the two nodes. If every edge passed with no cycle, return true.

This Python version checks the edge count, then runs union-find with lists.

graph_valid_tree.py
def valid_tree(n, edges):
if len(edges) != n - 1: # a tree has exactly n - 1 edges
return False
parent = list(range(n)) # each node is its own parent
rank = [0] * n # rough height of each group
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb:
return False # already connected, a cycle
if rank[ra] < rank[rb]: # union by rank
parent[ra] = rb
elif rank[ra] > rank[rb]:
parent[rb] = ra
else:
parent[rb] = ra
rank[ra] += 1
return True
for a, b in edges:
if not union(a, b): # this edge closes a cycle
return False
return True
n = 5
edges = [[0, 1], [0, 2], [0, 3], [1, 4]]
print(valid_tree(n, edges))

The output of the above code will be:

True

Let us walk through the Python version line by line, because it combines two checks into one clean pass.

if len(edges) != n - 1: return False is the early exit. A tree with n nodes always has exactly n - 1 edges. Too few edges means the graph is split. Too many means a loop is guaranteed. Either way it is not a tree, so we stop here.

parent = list(range(n)) makes each node its own parent at first. rank = [0] * n starts every group with rank zero.

Inside find(x), the check if parent[x] != x: asks whether x is the root, the only node that points to itself. If not, parent[x] = find(parent[x]) finds the real root and points x straight at it. That is path compression.

Inside union(a, b), we get both roots. if ra == rb: return False is the cycle test. If the two nodes already share a root, joining them again would form a loop, so this is not a tree. The rank lines attach the shorter group under the taller one and only bump rank when the heights tie.

The loop reads each edge. if not union(a, b): return False stops the moment any edge would close a cycle. If every edge merged two separate groups, then with exactly n - 1 edges and no cycle, the graph must be fully connected. So we return True.

⏱️ Time and Space Complexity

The DFS approach builds an adjacency list and walks every node and edge once. So it is O(V + E) time and O(V + E) space. Union-find skips the adjacency list. The edge-count check is instant. With path compression and union by rank, each operation is almost constant time, the inverse Ackermann alpha of n, which stays tiny. So union-find runs in about O(E * alpha(V)) time and O(V) space. Both work well. Union-find is shorter to write and lighter on memory.

Approach Time Complexity Space Complexity
DFS connectivity and cycle check (alternative) O(V + E) O(V + E)
Union-find with compression and rank (best) O(E * alpha(V)) O(V)

Tip

The edge-count shortcut does a lot of work. With exactly n - 1 edges, “no cycle” and “fully connected” become the same thing. So if union-find finds no cycle, the graph is automatically connected. You do not need a separate connectivity scan.

🧩 Key Takeaways

  • ✅ A valid tree is fully connected and has no cycles.
  • ✅ A tree with n nodes always has exactly n - 1 edges. Check this first.
  • ✅ Union-find flags a cycle the moment an edge joins two nodes that already share a root.
  • ✅ With n - 1 edges and no cycle, the graph is guaranteed connected.
  • ✅ Path compression and union by rank keep each operation almost constant time.

Check Your Knowledge

4 questions Show quiz Hide quiz

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What two conditions make a graph a valid tree?

    Why: A valid tree connects every node to every other and contains no loops.

  2. 2

    How many edges does a valid tree with n nodes have?

    Why: A tree always has exactly n - 1 edges, which is a fast first check.

  3. 3

    In the union-find solution, when do you know there is a cycle?

    Why: If both endpoints of an edge already have the same root, that edge closes a loop.

  4. 4

    Why does no cycle plus exactly n - 1 edges guarantee a tree?

    Why: With n - 1 edges and no loop, the only way to use those edges is to connect all n nodes into one piece.

🚀 What’s Next?