Browse curriculum

Graph Valid Tree

Determine if a set of edges forms a valid tree.

Examples

Input:n = 5, edges = [[0,1], [0,2], [0,3], [1,4]]
Output:true
Explain:

No cycles, fully connected.

Input:n = 5, edges = [[0,1], [1,2], [2,3], [1,3], [1,4]]
Output:false
Explain:

Cycle exists: 1-2-3-1.

Problem Understanding

A Tree is a special type of Graph that satisfies two conditions:

  1. Fully Connected: You can reach any node from any other node.
  2. Acyclic: It contains no cycles.

Alternatively: A graph with N nodes is a tree if it is fully connected and has exactly N - 1 edges.

Algorithm Strategy (DFS)

We can use DFS to check for cycles and connectivity.

  1. Build Graph: Create adjacency list.
  2. DFS from Node 0: Keep track of visited set to detect cycles.
    • Since the graph is undirected, A-B means B is a neighbor of A. We must not count the immediate parent as a cycle.
    • Pass parent to the DFS call: dfs(node, parent).
    • If neighbor != parent and neighbor is visited -> Cycle Detected! Return false.
  3. Check Connectivity: After DFS, visited.size() must equal n. If not, the graph is disconnected.

Visualization

DFS from node 0 carries the node it came from. The edge back to that parent is skipped; any other edge that leads to a node already visited closes a cycle, and it turns red. Green edges are the ones DFS walked down. If DFS finishes without a cycle, the last check looks for a node it never reached — try the Two pieces preset.

Dry Run

Input: n = 5, edges = [[0,1], [1,2], [2,3], [1,3], [1,4]]
Adjacency list: 0:[1], 1:[0,2,3,4], 2:[1,3], 3:[2,1], 4:[1]

Step Call Visited Action
1 dfs(0, -1) {0} Mark 0. Neighbour 1: call dfs(1, 0).
2 dfs(1, 0) {0, 1} Mark 1. Neighbour 0 is the parent: skip. Neighbour 2: call dfs(2, 1).
3 dfs(2, 1) {0, 1, 2} Mark 2. Neighbour 1 is the parent: skip. Neighbour 3: call dfs(3, 2).
4 dfs(3, 2) {0, 1, 2, 3} Mark 3. Neighbour 2 is the parent: skip. Neighbour 1: call dfs(1, 3).
5 dfs(1, 3) {0, 1, 2, 3} 1 is already visited. The edge 3-1 closes the cycle 1-2-3. Return false.
6 Unwind — dfs(3, 2), dfs(2, 1), dfs(1, 0) and dfs(0, -1) each return false. The answer is false.

Edge Cases & Common Mistakes

  • Disconnected Graph: No cycle, but not a tree (forest). Must check visited.size() == n.
  • Single Node: LeetCode guarantees n ≥ 1. One node with no edges is a valid tree: there is no cycle, and DFS from node 0 reaches everything.
  • Self-Loops and Repeated Edges: LeetCode rules both out, and the parent check relies on it: a second copy of the edge back to the parent would be skipped too, so that two-edge cycle would go unnoticed.

Solution

DFS from node 0 with the parent passed down, then a check that every node was visited. Reaching a visited node through any edge except the one back to the parent means a cycle.

Note: LeetCode 261 is a Premium problem.

Graph Valid Tree Code
1var validTree = function(n, edges) {
2 const graph = Array.from({ length: n }, () => []);
3 for (const [a, b] of edges) {
4 graph[a].push(b);
5 graph[b].push(a);
6 }
7
8 const visited = new Array(n).fill(false);
9
10 function dfs(node, parent) {
11 if (visited[node]) {
12 return false;
13 }
14 visited[node] = true;
15 for (const neighbor of graph[node]) {
16 if (neighbor === parent) {
17 continue;
18 }
19 if (!dfs(neighbor, node)) {
20 return false;
21 }
22 }
23 return true;
24 }
25
26 if (!dfs(0, -1)) {
27 return false;
28 }
29 for (let node = 0; node < n; node++) {
30 if (!visited[node]) {
31 return false;
32 }
33 }
34 return true;
35};

The approach, step by step

  1. Build the adjacency list

    Add each undirected edge to both of its nodes’ lists.

  2. Look for a cycle

    Run DFS from node 0 with parent -1. Reaching a visited node through any edge except the parent’s means a cycle: return false.

  3. Check connectivity

    If any node is still unvisited, return false; otherwise return true.

Frequently asked questions

What makes a graph a valid tree?

It is connected and has no cycles. Equivalently, it is connected and has exactly n - 1 edges.


Why pass the parent into DFS?

In an undirected graph every edge is in both nodes’ lists, so a node always sees the node it came from as a neighbour. Skipping that one edge stops it being mistaken for a cycle; any other visited neighbour really does close one.


Why check that every node was visited?

A graph can have no cycle and still be in several pieces. DFS from node 0 only reaches its own piece, so any node left unvisited means the graph is not connected.

Applying for software engineering roles? Free, no sign-up:

AI resume checker — match your resume to the job description