Loading problems...
TL;DR: The optimal solution uses the Union-Find (DSU) data structure to detect cycles and verify that the graph contains exactly one connected component with edges.
You are given an integer n (the number of nodes) and a list of edges. The goal is to determine if these edges form a valid tree. In the context of graph theory, a valid tree must satisfy two strict conditions: it must be fully connected (all nodes can reach each other) and it must be acyclic (contain no loops). This is a popular interview question for testing graph theory fundamentals and efficient state management.
A naive brute force approach to solve the Graph Valid Tree problem focuses on verifying the two tree properties—connectivity and acyclicity—independently and inefficiently.
python# Pseudo-code for Naive Approach def validTree(n, edges): # 1. Build Adjacency Matrix (Space O(N^2)) adj = [[0] * n for _ in range(n)] for u, v in edges: adj[u][v] = adj[v][u] = 1 # 2. Check Connectivity (Time O(N * (N + E))) # Run BFS/DFS from every node to every other node for i in range(n): for j in range(i + 1, n): if not hasPath(i, j, adj): return False # 3. Check Cycles # ... additional traversal logic ... return True
Time Complexity: . We potentially traverse the entire graph for every node pair verification. Why it fails: This approach results in a Time Limit Exceeded (TLE) error on large inputs. Furthermore, using an adjacency matrix requires space, which causes Memory Limit Exceeded errors when is large (e.g., ).
The core insight for solving LeetCode 261 efficiently lies in the definition of a tree in graph theory. A graph with nodes is a tree if and only if:
If a graph has edges and no cycles, it is guaranteed to be connected.
The Union-Find data structure is optimized for two specific operations: find (determining which subset a generic element belongs to) and union (joining two subsets into a single subset).
By processing the edges one by one using Union-Find, we can enforce the tree constraints:
u and node v, we check if they essentially belong to the same set (i.e., find(u) == find(v)). If they do, a path already exists between them, and adding this edge would close a loop, creating a cycle.union operation reduces the number of disjoint sets by one. For a valid tree, after processing all edges, we must have exactly 1 connected component remaining.Visual Description:
Imagine the algorithm state as a collection of disjoint sets. Initially, every node is an isolated root [0, 1, 2, ...]. When an edge [0, 1] is processed, the set containing 0 merges with the set containing 1. The structure updates to point 1 towards 0. If we later encounter an edge connecting two nodes that already trace back to the same root (e.g., 0), the algorithm immediately identifies a cycle.

Let's trace the algorithm with n = 5 and edges = [[0,1], [0,2], [0,3], [1,4]].
Initialization:
parent array: [0, 1, 2, 3, 4]Process Edge [0, 1]:
find(0) returns 0. find(1) returns 1.parent becomes [1, 1, 2, 3, 4] (0 points to 1).Process Edge [0, 2]:
find(0) -> points to 1. Root is 1.find(2) -> returns 2. Root is 2.1 and 2 are different. Union sets.parent becomes [1, 1, 1, 3, 4] (2 points to 1).Process Edge [0, 3]:
find(0) -> points to 1. Root is 1.find(3) -> returns 3. Root is 3.1 and 3 are different. Union sets.parent becomes [1, 1, 1, 1, 4] (3 points to 1).Process Edge [1, 4]:
find(1) -> returns 1. Root is 1.find(4) -> returns 4. Root is 4.1 and 4 are different. Union sets.parent becomes [1, 1, 1, 1, 1] (4 points to 1).Completion:
true.The correctness relies on the properties of trees and the invariants of the Union-Find data structure.
find(u) == find(v) check ensures that we never add an edge between two nodes that are already in the same connected component. This guarantees the graph remains acyclic.edges.length == n - 1 initially and ensuring no cycles exist during processing, we implicitly guarantee that the graph is fully connected. If there were no cycles but the graph was disconnected, it would require fewer than edges.cppclass Solution { public: // Helper function to find the representative of the set int find(int node, vector<int>& parent) { if (parent[node] == node) { return node; } // Path compression: point node directly to root return parent[node] = find(parent[node], parent); } bool validTree(int n, vector<vector<int>>& edges) { // Condition 1: A tree with n nodes must have exactly n-1 edges if (edges.size() != n - 1) { return false; } // Initialize DSU structure vector<int> parent(n); for (int i = 0; i < n; ++i) { parent[i] = i; } // Process edges for (const auto& edge : edges) { int u = edge[0]; int v = edge[1]; int rootU = find(u, parent); int rootV = find(v, parent); // Condition 2: If roots are same, a cycle exists if (rootU == rootV) { return false; } // Union the sets parent[rootU] = rootV; } // If we processed n-1 edges without cycles, it is a valid tree return true; } };
javaclass Solution { // Helper method to find the representative with path compression private int find(int node, int[] parent) { if (parent[node] == node) { return node; } parent[node] = find(parent[node], parent); // Path compression return parent[node]; } public boolean validTree(int n, int[][] edges) { // A valid tree with n nodes must have exactly n-1 edges if (edges.length != n - 1) { return false; } int[] parent = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; } for (int[] edge : edges) { int u = edge[0]; int v = edge[1]; int rootU = find(u, parent); int rootV = find(v, parent); // If they share the same root, a cycle is detected if (rootU == rootV) { return false; } // Union logic parent[rootU] = rootV; } return true; } }
pythonclass Solution: def validTree(self, n: int, edges: List[List[int]]) -> bool: # A tree must have exactly n - 1 edges if len(edges) != n - 1: return False parent = list(range(n)) def find(node): if parent[node] != node: # Path compression parent[node] = find(parent[node]) return parent[node] for u, v in edges: root_u = find(u) root_v = find(v) # If roots are the same, a cycle exists if root_u == root_v: return False # Union the sets parent[root_u] = root_v return True
Time Complexity:
The time complexity is determined by the Union-Find operations. With path compression and union by rank (or just path compression), the amortized time complexity for each find and union operation is , where is the Inverse Ackermann function. For all practical purposes, , making this nearly linear, effectively given we process edges.
Space Complexity:
We require space to store the parent array (and optionally a rank or size array) to maintain the disjoint sets.
The Graph - Union-Find pattern used in LeetCode 261 is a fundamental technique applicable to several other problems:
Why does my solution fail on disconnected graphs?
true for a forest, which is incorrect. This is solved by checking edges.length == n - 1 or ensuring the final component count is 1.Why is my solution slow on large inputs?
find function without path compression.find operations to instead of nearly constant time.Why does the algorithm fail immediately for some inputs?
n=1 and edges=[].edges is non-empty or miscalculates the n-1 check, it may fail edge cases. (Note: The len(edges) != n - 1 check handles this correctly since ).