261. Graph Valid Tree - Explanation
Description
Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each edge is a pair of nodes), write a function to check whether these edges make up a valid tree.
Example 1:
Input: n = 5, edges = [[0,1],[0,2],[0,3],[1,4]]
Output: trueExample 2:
Input: n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]]
Output: falseNote:
- You can assume that no duplicate edges will appear in edges. Since all edges are
undirected,[0, 1]is the same as[1, 0]and thus will not appear together in edges.
Constraints:
1 <= n <= 20000 <= edges.length <= 5000edges[i].length == 20 <= a_i, b_i < na_i != b_i- There are no self-loops or repeated edges.
Topics
Recommended Time & Space Complexity
You should aim for a solution as good or better than O(V + E) time and O(V + E) space, where V is the number vertices and E is the number of edges in the graph.
Hint 1
According to the definition of a tree, a tree is an undirected graph with no cycles, all the nodes are connected as one component, and any two nodes have exactly one path. Can you think of a recursive algorithm to detect a cycle in the given graph and ensure it is a tree?
Hint 2
We can use the Depth First Search (DFS) algorithm to detect a cycle in the graph. Since a tree has only one component, we can start the DFS from any node, say node 0. During the traversal, we recursively visit its neighbors (children). If we encounter any already visited node that is not the parent of the current node, we return false as it indicates a cycle. How would you implement this?
Hint 3
We start DFS from node 0, assuming -1 as its parent. We initialize a hash set visit to track the visited nodes in the graph. During the DFS, we first check if the current node is already in visit. If it is, we return false, detecting a cycle. Otherwise, we mark the node as visited and perform DFS on its neighbors, skipping the parent node to avoid revisiting it. After all DFS calls, if we have visited all nodes, we return true, as the graph is connected. Otherwise, we return false because a tree must contain all nodes.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Graph Representation - Building and working with adjacency lists for undirected graphs
- Depth-First Search (DFS) - Traversing graphs recursively while tracking visited nodes
- Breadth-First Search (BFS) - Level-by-level graph traversal using a queue
- Union-Find (Disjoint Set Union) - Efficiently tracking connected components and detecting cycles
1. Cycle Detection (DFS)
Intuition
A graph is a valid tree if:
- It has no cycles
- It is fully connected
Using DFS, we can detect cycles by checking if we visit a node again from a path other than its parent.
Also, a tree with n nodes must have exactly n - 1 edges — otherwise it’s invalid.
Algorithm
- If number of edges >
n - 1, returnfalse. - Build an adjacency list for the undirected graph.
- Run
dfsfrom node0, passing the parent to avoid false cycle detection. - If
dfsfinds a visited node (not the parent), a cycle exists → returnfalse. - After
dfs, check if all nodes were visited (graph is connected). - Return
trueonly if no cycle and all nodes are visited.
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
if len(edges) > (n - 1):
return False
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
visit = set()
def dfs(node, par):
if node in visit:
return False
visit.add(node)
for nei in adj[node]:
if nei == par:
continue
if not dfs(nei, node):
return False
return True
return dfs(0, -1) and len(visit) == nTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number vertices and is the number of edges in the graph.
2. Breadth First Search
Intuition
A graph is a valid tree if:
- It has no cycles
- It is fully connected
Using BFS, we traverse the graph level by level.
- If we ever reach a node that was already visited (and is not the parent) → a cycle exists.
- After BFS, if all nodes are visited, the graph is connected.
Also, a tree with n nodes can have at most n - 1 edges.
Algorithm
- If number of edges >
n - 1, returnfalse. - Build an adjacency list for the undirected graph.
- Start
bfsfrom node0, store(node, parent)in the queue. - For each neighbor:
- Ignore the parent.
- If already visited → cycle found → return
false. - Otherwise, mark visited and add to queue.
- After
bfs, check if visited node count equalsn. - Return
trueonly if no cycle and all nodes are visited.
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
if len(edges) > n - 1:
return False
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
visit = set()
q = deque([(0, -1)]) # (current node, parent node)
visit.add(0)
while q:
node, parent = q.popleft()
for nei in adj[node]:
if nei == parent:
continue
if nei in visit:
return False
visit.add(nei)
q.append((nei, node))
return len(visit) == nTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number vertices and is the number of edges in the graph.
3. Disjoint Set Union
Intuition
A graph is a valid tree if:
- It has no cycles
- It is fully connected
Using Disjoint Set Union (Union-Find):
- Each node starts in its own component
- When we connect two nodes:
- If they are already in the same component, adding this edge creates a cycle
- Otherwise, we merge their components
- In the end, a valid tree must have exactly one connected component
Also, a tree with n nodes can have at most n - 1 edges.
Algorithm
- If number of edges >
n - 1, returnfalse. - Initialize DSU with
ncomponents. - For each edge
(u, v):- If
union(u, v)fails → cycle detected → returnfalse.
- If
- After processing all edges:
- Check if number of components is
1.
- Check if number of components is
- Return
trueif only one component exists, elsefalse.
class DSU:
def __init__(self, n):
self.comps = n
self.Parent = list(range(n + 1))
self.Size = [1] * (n + 1)
def find(self, node):
if self.Parent[node] != node:
self.Parent[node] = self.find(self.Parent[node])
return self.Parent[node]
def union(self, u, v):
pu = self.find(u)
pv = self.find(v)
if pu == pv:
return False
self.comps -= 1
if self.Size[pu] < self.Size[pv]:
pu, pv = pv, pu
self.Size[pu] += self.Size[pv]
self.Parent[pv] = pu
return True
def components(self):
return self.comps
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
if len(edges) > n - 1:
return False
dsu = DSU(n)
for u, v in edges:
if not dsu.union(u, v):
return False
return dsu.components() == 1Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of vertices and is the number of edges in the graph. is used for amortized complexity.
Common Pitfalls
Detecting False Cycles Due to Parent Edges
In an undirected graph represented with an adjacency list, each edge (u, v) appears twice: once in adj[u] and once in adj[v]. When traversing from node u to v, you will see u in v's neighbor list. Without tracking the parent, this would incorrectly be detected as a cycle. Always pass and check the parent node to avoid this false positive.
Forgetting to Check Connectivity
A graph can be cycle-free but still not be a valid tree if it is disconnected. After running DFS or BFS from any starting node, you must verify that all n nodes were visited. If visited.size() < n, the graph has multiple connected components and is not a valid tree.
Not Using the Edge Count Shortcut
A valid tree with n nodes must have exactly n - 1 edges. If there are more than n - 1 edges, the graph definitely has a cycle. If there are fewer than n - 1 edges, the graph cannot be connected. Checking this condition first can save unnecessary traversal and provides an early exit for invalid inputs.
Sign in to join the discussion