Problem Walkthrough

Graph Valid Tree LeetCode 261 Guide

Determine if n nodes and a list of undirected edges form a valid tree by verifying exactly n-1 edges plus full connectivity — solved efficiently with Union-Find or DFS traversal.

8 min read|

Graph Valid Tree

LeetCode 261

Problem Overview — Undirected Graph to Valid Tree

LeetCode 261 Graph Valid Tree gives you n nodes labeled 0 to n-1 and a list of undirected edges. Your task is to determine whether these nodes and edges form a valid tree. A tree is a special type of graph — it is connected and acyclic. Both conditions must hold simultaneously.

The problem appears in LeetCode premium but is widely discussed and tested in interviews. It is a classic Union-Find and graph traversal problem that tests whether you know the mathematical definition of a tree and can verify it efficiently. The input can have up to 2000 nodes and 3000 edges.

This problem is important because it sits at the foundation of many harder graph problems. Understanding what makes a graph a tree — and how to check both properties efficiently — is a prerequisite for problems like Redundant Connection, Minimum Spanning Tree, and Number of Connected Components.

  • n nodes labeled 0 to n-1, 1 <= n <= 2000
  • edges is a list of undirected pairs [u, v] with 0 <= edges.length <= 5000
  • A tree must be connected: all nodes reachable from any starting node
  • A tree must be acyclic: no cycles in the graph
  • Return true if the edges form a valid tree, false otherwise

Tree Properties — Two Necessary and Sufficient Conditions

A valid tree with n nodes has exactly two equivalent definitions: (1) connected and acyclic, or (2) connected with exactly n-1 edges. These two formulations are logically equivalent for undirected graphs. If you have n nodes and n-1 edges and the graph is connected, there can be no cycles. If there were a cycle, you would need at least n edges to connect n nodes while maintaining one.

The key insight is that either condition alone is not sufficient. Having exactly n-1 edges does not guarantee connectivity — a disconnected forest with n nodes also has n-1 edges (it is just split across multiple trees). Being connected does not guarantee acyclicity — a graph with extra edges beyond n-1 can still be connected but will contain cycles.

Checking both conditions together is fast and clean. Verify edge count first as a constant-time pre-check: if edges.length != n-1, immediately return false without any graph traversal. Then verify connectivity using BFS, DFS, or Union-Find. This two-step approach is efficient and easy to reason about correctly.

💡

Quick Reject: Check Edge Count First

Always check if edges.length == n-1 before any traversal. Too many edges means a cycle exists somewhere — return false immediately. Too few edges means at least one component is disconnected — return false immediately. This O(1) pre-check eliminates a large fraction of invalid inputs without touching the adjacency structure.

Union-Find Approach — Cycle Detection with Path Compression

Union-Find (also called Disjoint Set Union or DSU) is the most elegant approach for this problem. Initialize n components, each node in its own set. For each edge [u, v]: call find(u) and find(v) to get their root representatives. If find(u) == find(v), the two nodes are already in the same component — adding this edge creates a cycle, so return false. Otherwise, call union(u, v) to merge their components.

After processing all edges without finding a cycle, verify connectivity: the number of remaining components must be exactly 1. With the edge count pre-check (edges.length == n-1) already verified, a cycle-free graph with n-1 edges is guaranteed to be connected — so the component count check is redundant but good practice. Path compression and union by rank make each operation nearly O(1) amortized.

The Union-Find approach handles large inputs efficiently. With path compression, find() runs in nearly constant time. The entire algorithm is O(E * α(n)) where α is the inverse Ackermann function — effectively O(E) for all practical purposes. Space is O(n) for the parent and rank arrays.

  1. 1Initialize parent[i] = i and rank[i] = 0 for all n nodes
  2. 2Define find(x): return x if parent[x] == x, else path-compress and recurse
  3. 3Define union(x, y): find roots of x and y; merge smaller rank into larger
  4. 4Return false immediately if edges.length != n-1
  5. 5For each edge [u, v]: if find(u) == find(v), cycle detected — return false
  6. 6Otherwise call union(u, v) to merge the two components
  7. 7After all edges processed, return true (connectivity guaranteed by n-1 edges + no cycles)

DFS/BFS Approach — Traversal from Node 0

The DFS/BFS approach builds an adjacency list and then traverses the graph from node 0. Track a visited set. For each node visited, mark it as seen and recursively visit all neighbors. If you encounter a neighbor that is already visited (and it is not the parent node you came from), a cycle exists — return false. After full traversal, check that all n nodes were visited to confirm connectivity.

The parent tracking is critical for undirected graphs. In a DFS over an undirected graph, every node can see its direct predecessor as a neighbor. Without parent tracking, the DFS would immediately report a false cycle the first time it looks back along the edge it came from. Pass the parent node as a parameter (DFS) or track it in a separate map (BFS) to avoid this false positive.

BFS works identically: use a queue, a visited set, and a parent dictionary. Start from node 0. Dequeue a node, mark visited, enqueue unvisited neighbors, track their parent. If a visited neighbor is not the current node's parent, a cycle exists. After BFS, check visited size equals n.

ℹ️

Edge Count Check Simplifies DFS/BFS

With the edge count pre-check (edges.length == n-1) in place, you only need to verify one of the two tree conditions — not both. For DFS/BFS, you only need to check connectivity (all nodes visited) without cycle tracking. For Union-Find, you only need to check acyclicity (no union conflicts) without counting components. The pre-check does half the work for free.

Edge Count Optimization — O(1) Pre-filter

The edge count check deserves its own section because of how powerfully it simplifies the remaining logic. If edges.length != n-1, the input cannot possibly be a tree regardless of connectivity — return false immediately. This covers two distinct failure modes: too many edges (cycle exists somewhere) and too few edges (graph is disconnected).

After confirming edges.length == n-1, you only need to verify one additional condition. For Union-Find: just run union-find and check for cycles. If no cycle is found with exactly n-1 edges, the graph must be connected and therefore a valid tree. For DFS/BFS: just check that all n nodes are reachable from node 0. If all nodes are visited with exactly n-1 edges, there can be no cycles.

This optimization reduces the Union-Find implementation to about 15 lines and the DFS implementation to about 20 lines. The clean separation — edge count as the fast reject, traversal as the deep check — makes the code easy to read, test, and explain in interviews.

  1. 1Step 1: if len(edges) != n - 1: return False
  2. 2Step 2 (Union-Find path): initialize DSU, iterate edges, return false on cycle conflict
  3. 3Step 2 (DFS path): build adjacency list, DFS from node 0, return len(visited) == n
  4. 4Step 2 (BFS path): build adjacency list, BFS from node 0, return len(visited) == n
  5. 5No need to check both cycle detection AND connectivity — the edge count covers one implicitly

Code Walkthrough — Python and Java Implementations

Python Union-Find (15 lines): define parent list with parent[i]=i, define find with path compression as one-liner (parent[x] = find(parent[x]) or recurse), define union. In validTree: check len(edges)!=n-1 return False; for u,v in edges: pu,pv=find(u),find(v); if pu==pv: return False; parent[pu]=pv; return True. This is compact, readable, and runs in O(E*α(n)).

Python DFS (20 lines): build adjacency list as defaultdict(list). Check len(edges)!=n-1 return False. visited=set(). DFS function(node, parent): add to visited; for each neighbor: if neighbor==parent skip; if neighbor in visited return False; if not dfs(neighbor,node) return False; return True. Call dfs(0,-1) and return len(visited)==n. The parent parameter prevents false cycle detection on undirected back-edges.

Java follows the same logic: Union-Find uses int[] parent initialized in a loop, find() with path compression, union() merging by appending one root to the other. For DFS, use List<List<Integer>> adjacency, Set<Integer> visited, recursive helper with parentNode int param. Both solutions are O(V+E) time and O(V+E) space — linear in input size.

⚠️

DFS: Always Track the Parent Node

In the DFS approach for undirected graphs, you MUST track the parent node to avoid false cycle detection. When you visit a neighbor of the current node, skip the neighbor if it equals the parent you came from — that edge is the one you just traversed, not a cycle. Forgetting this causes every undirected edge to be detected as a cycle since the reverse edge is also in the adjacency list.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now