Graph Valid Tree

MediumUnion-Find · Graph Connectivity🧩 Union-FindLeetCode #261 ↗Updated

Problem

You get n nodes labelled 0..n-1 and a list of undirected edges. Decide whether these edges form a single valid tree: every node is reachable from every other node and there is no cycle.
Inputn = 4, edges = [[0,1],[1,2],[1,3]]Outputtrue
Inputn = 4, edges = [[0,1],[1,2],[2,0]]Outputfalse (cycle, node 3 isolated)
Inputn = 3, edges = [[0,1]]Outputfalse (node 2 unreachable)

1 ≤ n ≤ 2000  ·  0 ≤ edges.length ≤ 5000  ·  no self-loops or repeated edges

Try it yourself

Java
class Solution {
    public boolean validTree(int n, int[][] edges) {
        
    }
}
Your code is saved in this browser.Submit on LeetCode ↗
Key insight
A graph on n nodes is a tree iff it has exactly n-1 edges and is connected (equivalently: n-1 edges and acyclic). So check the edge count first, then either confirm one component or that no edge closes a cycle.
PatternUnion-Find

Instinct Items get grouped by "connected", "same as" or "shares a" links and you keep asking which group something is in: merge sets with Union-Find.

Memory hack Find your boss, merge the bosses; squash the chain (path compression) and hang the short tree under the tall one (rank).

Open the Union-Find guide →

Approaches

Brute ForceReachability Check Before Each Edge

Time O(E·(V + E))Space O(V + E)

Add edges one by one. Before adding (u, v), BFS over the edges added so far: if v is already reachable from u, this edge closes a cycle. If no edge does, the graph is a forest, and a forest with exactly n-1 edges is a single tree.

Java
public boolean validTree(int n, int[][] edges) {
    List<List<Integer>> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
    for (int[] e : edges) {
        if (reachable(adj, e[0], e[1], n)) return false; // edge would close a cycle
        adj.get(e[0]).add(e[1]);
        adj.get(e[1]).add(e[0]);
    }
    return edges.length == n - 1; // acyclic + n-1 edges => connected
}

private boolean reachable(List<List<Integer>> adj, int src, int dst, int n) {
    boolean[] seen = new boolean[n];
    Deque<Integer> queue = new ArrayDeque<>();
    queue.add(src);
    seen[src] = true;
    while (!queue.isEmpty()) {
        int u = queue.poll();
        if (u == dst) return true;
        for (int v : adj.get(u)) {
            if (!seen[v]) {
                seen[v] = true;
                queue.add(v);
            }
        }
    }
    return false;
}

Correct, but re-explores the graph for every edge.

At a glance

LevelApproachTimeSpace
Brute ForceReachability Check Before Each EdgeO(E·(V + E))O(V + E)
BetterEdge Count + One TraversalO(V + E)O(V + E)
OptimalEdge Count + Union-FindO(V + E·α(V))O(V)
Common pitfalls
  • Checking only for cycles — a forest (n=3, [[0,1]]) is not a tree.
  • Checking only the edge count — a cycle plus an isolated node also has n-1 edges.
  • Using directed DFS cycle detection — in an undirected graph the parent edge is not a cycle.
Interview tips
  • Lead with the theorem: tree ⇔ connected and exactly n-1 edges.
  • Remember n = 1 with no edges is a valid tree.
  • Offer union-find when edges arrive as a stream; BFS when the graph is already built.
Pattern
Union-Find → incremental connectivity and cycle detection in undirected graphs.
My notes saved in this browser