150NeetCode 150

91.Graph Valid Tree

MediumBlind 75Graphs ยท 12/19

Problem

You have n nodes labelled 0 to n - 1 and a list of undirected edges. Determine whether these edges form a valid tree.

A valid tree must satisfy two conditions at the same time: it is connected (every node can reach every other node) and it is acyclic (there is no cycle). Return true if both hold, otherwise false.

Examples

Input:  n = 5, edges = [[0,1],[0,2],[0,3],[1,4]]
Output: true
Explanation: 4 edges, all 5 nodes reachable from 0, no cycle.
Input:  n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]]
Output: false
Explanation: 1 - 2 - 3 - 1 forms a cycle.
Input:  n = 4, edges = [[0,1],[2,3]]
Output: false
Explanation: No cycle, but {0,1} and {2,3} are disconnected.

Constraints

  • 1 <= n <= 2000
  • 0 <= edges.length <= 5000
  • edges[i].length == 2
  • 0 <= a, b < n, a != b
  • There are no self-loops or repeated edges.

๐Ÿ’ก Key Insight

A graph with n nodes is a tree iff it has exactly n - 1 edges and is connected (equivalently: n - 1 edges and no cycle). The edge-count check is O(1) and immediately rejects most bad inputs; after that, a single DFS/BFS checking that all nodes are reached โ€” or a Union-Find that never sees an edge joining two already-connected nodes โ€” finishes the job.

DFS with Parent Tracking

TimeO(V + E)SpaceO(V + E)

Intuition

Run a DFS from node 0. In an undirected graph each edge appears twice in the adjacency list, so going back to the node we just came from is not a cycle. If we ever reach an already-visited node other than our parent, there is a cycle. After DFS, the graph is connected only if we visited all n nodes.

Algorithm

  1. Build an undirected adjacency list.
  2. dfs(node, parent): mark node visited; for every neighbour next:
    • skip next == parent;
    • if next is visited, return false (cycle);
    • else recurse; propagate false.
  3. Return dfs(0, -1) and visitedCount == n.

Visualization

n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]]

dfs(0, -1) -> dfs(1, 0) -> dfs(2, 1) -> dfs(3, 2)
  at 3: neighbours 2 (parent, skip), 1 (visited, not parent!) -> cycle -> false

Code

java
import java.util.ArrayList;
import java.util.List;

class Solution {
    private List<List<Integer>> adj;
    private boolean[] visited;
    private int visitedCount;

    public boolean validTree(int n, int[][] edges) {
        adj = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            adj.add(new ArrayList<>());
        }
        for (int[] e : edges) {
            adj.get(e[0]).add(e[1]);
            adj.get(e[1]).add(e[0]);
        }
        visited = new boolean[n];
        visitedCount = 0;
        // no cycle AND every node reached
        return dfs(0, -1) && visitedCount == n;
    }

    private boolean dfs(int node, int parent) {
        visited[node] = true;
        visitedCount++;
        for (int next : adj.get(node)) {
            if (next == parent) {
                continue; // the edge we arrived on
            }
            if (visited[next]) {
                return false; // reached an ancestor through a different edge: cycle
            }
            if (!dfs(next, node)) {
                return false;
            }
        }
        return true;
    }
}

Complexity

  • Time: O(V + E) โ€” standard DFS.
  • Space: O(V + E) โ€” adjacency list and recursion stack.

Complexity Comparison

Approach Time Space
DFS with Parent Tracking O(V + E) O(V + E)
BFS with Edge-Count Check O(V + E) O(V + E)
Union-Find O(V ยท ฮฑ(V)) O(V)

Common Pitfalls

  • Treating the reverse of the edge you came from as a cycle โ€” in DFS you must skip the parent.
  • Checking only "no cycle" and forgetting connectivity (e.g. n = 4, edges = [[0,1],[2,3]]).
  • n = 1 with no edges is a valid tree; 0 == n - 1 handles it naturally.
  • Running the n - 1 shortcut but then still tracking parents โ€” unnecessary once the count check is in place.