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:
undirected, [0, 1] is the same as [1, 0] and thus will not appear together in edges.Constraints:
1 <= n <= 1000 <= edges.length <= n * (n - 1) / 2
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.
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?
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?
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.
Before attempting this problem, you should be comfortable with:
A graph is a valid tree if:
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.
n - 1, return false.dfs from node 0, passing the parent to avoid false cycle detection.dfs finds a visited node (not the parent), a cycle exists → return false.dfs, check if all nodes were visited (graph is connected).true only 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) == nWhere is the number vertices and is the number of edges in the graph.
A graph is a valid tree if:
Using BFS, we traverse the graph level by level.
Also, a tree with n nodes can have at most n - 1 edges.
n - 1, return false.bfs from node 0, store (node, parent) in the queue.false.bfs, check if visited node count equals n.true only 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) == nWhere is the number vertices and is the number of edges in the graph.
A graph is a valid tree if:
Using Disjoint Set Union (Union-Find):
Also, a tree with n nodes can have at most n - 1 edges.
n - 1, return false.n components.(u, v):union(u, v) fails → cycle detected → return false.1.true if only one component exists, else false.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() == 1Where is the number of vertices and is the number of edges in the graph. is used for amortized complexity.
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.
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.
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.