261. Graph Valid Tree π
Problem Description
You are given a graph with n
nodes labeled from 0
to n - 1
. You also receive a list of edges where each edge edges[i] = [ai, bi]
represents an undirected connection between nodes ai
and bi
.
Your task is to determine if these edges form a valid tree. Return true
if the graph is a valid tree, and false
otherwise.
A valid tree must satisfy these properties:
- It must be connected (all nodes are reachable from any other node)
- It must have no cycles (there's exactly one path between any two nodes)
- For
n
nodes, a tree must have exactlyn - 1
edges
The solution uses a Union-Find (Disjoint Set Union) data structure to detect cycles. The algorithm works by:
- Initializing each node as its own parent (creating
n
separate sets) - For each edge
[a, b]
, finding the root parents of both nodes - If both nodes share the same root parent, a cycle exists (return
false
) - Otherwise, merging the two sets by connecting one root to the other
- Decreasing the count of connected components by 1 for each successful merge
- After processing all edges, checking if exactly 1 connected component remains (indicating all nodes are connected in a single tree structure)
The find
function implements path compression to optimize future lookups by directly connecting nodes to their root parent during the search process.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly states we have a graph with
n
nodes and a list of edges connecting these nodes.
Is it a tree?
- Yes: The problem asks us to determine if the given graph forms a valid tree structure.
DFS
- Yes: Since we're dealing with a tree validation problem, DFS (Depth-First Search) is an appropriate approach.
Conclusion: The flowchart suggests using DFS for this tree validation problem.
Why DFS Works Here
DFS is particularly suitable for this problem because:
-
Cycle Detection: DFS can detect cycles by keeping track of visited nodes and checking if we revisit a node (excluding the immediate parent in an undirected graph).
-
Connectivity Check: DFS can verify that all nodes are reachable from a starting node, ensuring the graph is connected.
-
Tree Properties: A valid tree must have exactly
n-1
edges forn
nodes, be connected, and have no cycles - all properties that DFS can efficiently verify.
Alternative Approach in the Solution
While the flowchart leads us to DFS, the provided solution actually uses Union-Find (Disjoint Set Union), which also appears in the flowchart under the connectivity branch. This is equally valid because:
- The problem fundamentally involves connectivity (checking if all nodes form one connected component)
- Union-Find efficiently detects cycles (when two nodes being connected already share the same root)
- Both DFS and Union-Find can solve this problem effectively, with Union-Find being particularly elegant for cycle detection and connectivity verification
Intuition
When we think about what makes a valid tree, we need to recall the fundamental properties: a tree is a connected graph with no cycles. For n
nodes, it must have exactly n-1
edges.
The key insight is that we need to detect two potential failures:
- Cycles: If we can reach the same node through multiple paths
- Disconnected components: If some nodes are unreachable from others
Initially, we might think of using DFS to traverse the graph and check these properties. However, there's an elegant observation: as we build the graph edge by edge, we can detect cycles immediately using Union-Find.
Think of it this way - imagine each node starts in its own isolated group. As we add edges, we're connecting these groups. If an edge tries to connect two nodes that are already in the same group, we've found a cycle! This is because there was already a path between them, and adding this edge would create a second path.
The Union-Find approach naturally tracks connected components. We start with n
separate components (each node by itself). Each valid edge should merge two different components, reducing our total count by 1. After processing all edges, if we end up with exactly 1 component, we know all nodes are connected.
The beauty of this solution is its efficiency - we detect cycles immediately as we process edges, and we simultaneously track connectivity. If at any point we find a cycle (two nodes already in the same set), we return false
. If after processing all edges we have exactly one connected component (n == 1
after decreasing for each merge), we have a valid tree.
This approach avoids the complexity of tracking visited nodes and parent relationships that would be necessary in a DFS solution, making the code cleaner and more intuitive.
Solution Approach
The solution implements Union-Find (Disjoint Set Union) to validate whether the graph forms a tree. Let's walk through the implementation step by step:
Data Structure Initialization
p = list(range(n))
We create a parent array p
where each node initially points to itself. This means we start with n
separate components, where p[i] = i
for all nodes.
Find Operation with Path Compression
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x])
return p[x]
The find
function locates the root parent of a node. It implements path compression optimization - as we traverse up to find the root, we directly connect each node to the root, flattening the tree structure for faster future lookups.
Processing Edges
for a, b in edges: pa, pb = find(a), find(b) if pa == pb: return False p[pa] = pb n -= 1
For each edge [a, b]
:
- Find roots: We find the root parents
pa
andpb
of nodesa
andb
- Cycle detection: If
pa == pb
, both nodes are already in the same component, meaning there's already a path between them. Adding this edge would create a cycle, so we returnFalse
- Union operation: If they're in different components, we merge them by setting
p[pa] = pb
, making one root point to the other - Component counting: We decrease
n
by 1, effectively tracking the number of remaining components
Final Validation
return n == 1
After processing all edges, we check if exactly one component remains. If n == 1
, all nodes are connected in a single tree structure. If n > 1
, the graph is disconnected with multiple components.
Why This Works
The algorithm cleverly uses the fact that a tree with n
nodes must have exactly n-1
edges and be connected. By starting with n
components and merging them one by one, we ensure:
- No cycles exist (checked during union operation)
- All nodes are connected (verified by final component count)
- The edge count is implicitly correct (if we have cycles or disconnected components, we'll catch them)
The time complexity is O(E Γ Ξ±(n))
where E
is the number of edges and Ξ±
is the inverse Ackermann function (practically constant), making this solution highly efficient.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the Union-Find approach validates a tree structure.
Example 1: Valid Tree
n = 5
nodes (labeled 0-4)edges = [[0,1], [0,2], [0,3], [1,4]]
Initial State:
Parent array p = [0, 1, 2, 3, 4] Components: {0}, {1}, {2}, {3}, {4} n = 5 (number of components)
Processing Edge [0,1]:
- Find root of 0:
find(0) = 0
- Find root of 1:
find(1) = 1
- Different roots (0 β 1), so no cycle
- Union: Set
p[0] = 1
- Decrease n:
n = 4
- State:
p = [1, 1, 2, 3, 4]
, Components: {0,1}, {2}, {3}, {4}
Processing Edge [0,2]:
- Find root of 0:
find(0) β p[0] = 1 β find(1) = 1
- Find root of 2:
find(2) = 2
- Different roots (1 β 2), so no cycle
- Union: Set
p[1] = 2
- Decrease n:
n = 3
- State:
p = [1, 2, 2, 3, 4]
, Components: {0,1,2}, {3}, {4}
Processing Edge [0,3]:
- Find root of 0:
find(0) β find(1) β find(2) = 2
- Find root of 3:
find(3) = 3
- Different roots (2 β 3), so no cycle
- Union: Set
p[2] = 3
- Decrease n:
n = 2
- State:
p = [1, 2, 3, 3, 4]
, Components: {0,1,2,3}, {4}
Processing Edge [1,4]:
- Find root of 1:
find(1) β find(2) β find(3) = 3
- Find root of 4:
find(4) = 4
- Different roots (3 β 4), so no cycle
- Union: Set
p[3] = 4
- Decrease n:
n = 1
- State:
p = [1, 2, 3, 4, 4]
, Components: {0,1,2,3,4}
Final Check: n == 1
β (All nodes connected in one component)
Result: True
- This is a valid tree!
Example 2: Invalid Tree (Contains Cycle)
n = 5
nodesedges = [[0,1], [1,2], [2,3], [1,3], [1,4]]
Initial State:
Parent array p = [0, 1, 2, 3, 4]
n = 5
Processing first three edges [[0,1], [1,2], [2,3]]: After these edges, we have:
p = [1, 2, 3, 3, 4]
n = 2
- Components: {0,1,2,3}, {4}
Processing Edge [1,3] (Creates a cycle!):
- Find root of 1:
find(1) β find(2) β find(3) = 3
- Find root of 3:
find(3) = 3
- Same root (3 == 3)! This means nodes 1 and 3 are already connected
- Adding this edge would create a cycle: 1β2β3 and 1β3
- Return False immediately
The algorithm detects the cycle as soon as we try to connect two nodes that are already in the same component, preventing us from creating an invalid tree structure.
Solution Implementation
1class Solution:
2 def validTree(self, n: int, edges: List[List[int]]) -> bool:
3 """
4 Determines if the given edges form a valid tree with n nodes.
5 A valid tree must:
6 1. Be connected (all nodes in one component)
7 2. Have no cycles (exactly n-1 edges for n nodes)
8
9 Args:
10 n: Number of nodes (labeled from 0 to n-1)
11 edges: List of undirected edges [node1, node2]
12
13 Returns:
14 True if edges form a valid tree, False otherwise
15 """
16
17 def find(node: int) -> int:
18 """
19 Finds the root parent of a node with path compression.
20
21 Args:
22 node: The node to find the root parent for
23
24 Returns:
25 The root parent of the node
26 """
27 if parent[node] != node:
28 # Path compression: directly connect node to root
29 parent[node] = find(parent[node])
30 return parent[node]
31
32 # Initialize parent array where each node is its own parent
33 parent = list(range(n))
34
35 # Track number of connected components (initially n separate nodes)
36 num_components = n
37
38 # Process each edge
39 for node_a, node_b in edges:
40 # Find root parents of both nodes
41 root_a = find(node_a)
42 root_b = find(node_b)
43
44 # If both nodes already have the same root, adding this edge creates a cycle
45 if root_a == root_b:
46 return False
47
48 # Union: connect the two components by making one root the parent of the other
49 parent[root_a] = root_b
50
51 # Decrease component count as two components are merged
52 num_components -= 1
53
54 # A tree must have exactly one connected component
55 return num_components == 1
56
1class Solution {
2 // Parent array for Union-Find (Disjoint Set Union) data structure
3 private int[] parent;
4
5 /**
6 * Determines if the given edges form a valid tree.
7 * A valid tree must:
8 * 1. Be connected (all nodes in one component)
9 * 2. Have no cycles
10 * 3. Have exactly n-1 edges for n nodes
11 *
12 * @param n The number of nodes (labeled from 0 to n-1)
13 * @param edges The edges represented as pairs of nodes
14 * @return true if the edges form a valid tree, false otherwise
15 */
16 public boolean validTree(int n, int[][] edges) {
17 // Initialize parent array where each node is its own parent initially
18 parent = new int[n];
19 for (int i = 0; i < n; i++) {
20 parent[i] = i;
21 }
22
23 // Process each edge
24 for (int[] edge : edges) {
25 // Find the root parents of both nodes in the edge
26 int parentA = find(edge[0]);
27 int parentB = find(edge[1]);
28
29 // If both nodes already have the same parent, adding this edge creates a cycle
30 if (parentA == parentB) {
31 return false;
32 }
33
34 // Union: Connect the two components by making one parent point to the other
35 parent[parentA] = parentB;
36
37 // Decrease the number of components
38 n--;
39 }
40
41 // After processing all edges, we should have exactly 1 component
42 // (started with n components, merged n-1 times)
43 return n == 1;
44 }
45
46 /**
47 * Finds the root parent of a node using path compression.
48 * Path compression optimizes future lookups by making nodes point directly to root.
49 *
50 * @param x The node to find the root parent for
51 * @return The root parent of node x
52 */
53 private int find(int x) {
54 // If x is not its own parent, recursively find the root and compress path
55 if (parent[x] != x) {
56 parent[x] = find(parent[x]); // Path compression
57 }
58 return parent[x];
59 }
60}
61
1class Solution {
2public:
3 bool validTree(int n, vector<vector<int>>& edges) {
4 // Initialize parent array for Union-Find (Disjoint Set Union)
5 // Each node initially points to itself as its parent
6 vector<int> parent(n);
7 iota(parent.begin(), parent.end(), 0);
8
9 // Find function with path compression
10 // Returns the root parent of node x
11 function<int(int)> find = [&](int x) {
12 if (parent[x] != x) {
13 // Path compression: make x point directly to root
14 parent[x] = find(parent[x]);
15 }
16 return parent[x];
17 };
18
19 // Process each edge
20 for (auto& edge : edges) {
21 // Find root parents of both nodes
22 int rootA = find(edge[0]);
23 int rootB = find(edge[1]);
24
25 // If both nodes have the same root, adding this edge creates a cycle
26 if (rootA == rootB) {
27 return false;
28 }
29
30 // Union: connect the two components by making one root point to the other
31 parent[rootA] = rootB;
32
33 // Decrease the number of connected components
34 --n;
35 }
36
37 // A valid tree should have exactly one connected component
38 return n == 1;
39 }
40};
41
1/**
2 * Determines if a given undirected graph is a valid tree.
3 * A valid tree must have exactly n-1 edges and be fully connected without cycles.
4 *
5 * @param n - The number of nodes in the graph (labeled from 0 to n-1)
6 * @param edges - Array of edges where each edge is represented as [node1, node2]
7 * @returns true if the graph forms a valid tree, false otherwise
8 */
9function validTree(n: number, edges: number[][]): boolean {
10 // Initialize parent array for Union-Find data structure
11 // Each node initially points to itself as its parent
12 const parent: number[] = Array.from({ length: n }, (_, index) => index);
13
14 /**
15 * Find operation with path compression for Union-Find.
16 * Finds the root parent of a node and compresses the path for optimization.
17 *
18 * @param node - The node whose root parent we want to find
19 * @returns The root parent of the node
20 */
21 const find = (node: number): number => {
22 if (parent[node] !== node) {
23 // Path compression: make the node point directly to its root
24 parent[node] = find(parent[node]);
25 }
26 return parent[node];
27 };
28
29 // Process each edge to build the Union-Find structure
30 for (const [nodeA, nodeB] of edges) {
31 // Find the root parents of both nodes
32 const rootA: number = find(nodeA);
33 const rootB: number = find(nodeB);
34
35 // If both nodes have the same root, adding this edge would create a cycle
36 if (rootA === rootB) {
37 return false;
38 }
39
40 // Union operation: connect the two components by making one root point to the other
41 parent[rootA] = rootB;
42
43 // Decrease the count of connected components
44 n--;
45 }
46
47 // A valid tree should have exactly one connected component remaining
48 return n === 1;
49}
50
Time and Space Complexity
Time Complexity: O(n Γ Ξ±(n))
where Ξ±(n)
is the inverse Ackermann function, which is effectively O(n Γ log n)
for practical purposes.
The algorithm uses Union-Find (Disjoint Set Union) with path compression. For each edge, we perform two find
operations and one union operation. With path compression in the find
function, each operation has an amortized time complexity of O(Ξ±(n))
, where Ξ±(n)
is the inverse Ackermann function. Since we process at most n-1
edges (for a valid tree), the total time complexity is O(n Γ Ξ±(n))
. The inverse Ackermann function grows extremely slowly and is effectively constant for all practical values of n
, but can be upper bounded by O(log n)
, giving us O(n Γ log n)
.
Space Complexity: O(n)
The space is primarily used for the parent array p
which stores n
elements. The recursive call stack for the find
function with path compression can go up to O(log n)
depth in the worst case, but since path compression flattens the tree structure, subsequent calls have reduced depth. The dominant space usage is the parent array, resulting in O(n)
space complexity.
Common Pitfalls
1. Forgetting to Check for Disconnected Components
Pitfall: Many implementations focus only on cycle detection but forget to verify that all nodes are connected. Simply checking that no cycles exist is insufficient - the graph could consist of multiple disconnected trees.
Example of Incorrect Implementation:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
for a, b in edges:
pa, pb = find(a), find(b)
if pa == pb:
return False # Cycle detected
parent[pa] = pb
return True # Wrong! Doesn't check if all nodes are connected
The Issue: This code would incorrectly return True
for a disconnected forest like:
- n = 4, edges = [[0,1], [2,3]]
- This forms two separate trees (0-1 and 2-3), not a single valid tree
Solution: Always track the number of components and verify exactly one remains:
num_components = n for a, b in edges: pa, pb = find(a), find(b) if pa == pb: return False parent[pa] = pb num_components -= 1 # Track component merging return num_components == 1 # Must have exactly 1 component
2. Incorrect Edge Count Validation
Pitfall: Some solutions try to validate the edge count separately but place the check incorrectly or use it as the only validation.
Example of Incorrect Implementation:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
# Wrong: Checking edge count alone is insufficient
if len(edges) != n - 1:
return False
# ... rest of union-find implementation
The Issue: While a tree must have exactly n-1 edges, having n-1 edges doesn't guarantee a tree structure. For example:
- n = 4, edges = [[0,1], [1,2], [0,2]]
- Has 3 edges (n-1), but contains a cycle (0-1-2-0)
Solution: The edge count check can be an early optimization but shouldn't replace cycle and connectivity validation:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
# Early return optimization (optional)
if len(edges) != n - 1:
return False
# Still need full union-find validation
parent = list(range(n))
components = n
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
for a, b in edges:
pa, pb = find(a), find(b)
if pa == pb:
return False
parent[pa] = pb
components -= 1
return components == 1
3. Handling Edge Cases Incorrectly
Pitfall: Not handling special cases like empty graphs or single nodes properly.
Common Mistakes:
# Wrong: Doesn't handle n = 0 or n = 1 correctly
def validTree(self, n: int, edges: List[List[int]]) -> bool:
if not edges:
return False # Wrong! A single node with no edges is a valid tree
Solution: Handle edge cases explicitly:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
# Edge cases
if n == 0:
return False # Empty graph is not a tree
if n == 1:
return len(edges) == 0 # Single node is a tree only with no edges
# For n >= 2, must have exactly n-1 edges
if len(edges) != n - 1:
return False
# Continue with union-find...
4. Path Compression Implementation Error
Pitfall: Implementing path compression incorrectly, which doesn't affect correctness but loses the optimization benefit.
Incorrect Implementation:
def find(x):
while parent[x] != x:
x = parent[x] # No path compression
return x
Correct Implementation with Path Compression:
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # Recursively compress path
return parent[x]
The recursive version ensures all nodes along the path point directly to the root, significantly improving performance for subsequent operations.
How many ways can you arrange the three letters A, B and C?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!