Graph Valid Tree

medium

Given n nodes labeled from 0 to n - 1 and a list of undirected edges where each edge is a pair [a, b], determine if these edges form a valid tree.

A valid tree must satisfy two conditions: it must be fully connected (every node is reachable from every other node) and it must contain no cycles. Equivalently, a graph with n nodes is a valid tree if and only if it is connected and has exactly n - 1 edges.

Examples

Example 1

  • Input: n = 5, edges = [[0,1],[0,2],[0,3],[1,4]]
  • Output: true
  • Explanation: All 5 nodes are connected with exactly 4 edges and no cycles, forming a valid tree.

Example 2

  • Input: n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]]
  • Output: false
  • Explanation: Nodes 1, 2, and 3 form a cycle, so this is not a valid tree.

Example 3

  • Input: n = 4, edges = [[0,1],[2,3]]
  • Output: false
  • Explanation: The graph has two disconnected components, so it is not a valid tree.

Constraints

  • 1 <= n <= 2000
  • 0 <= edges.length <= 5000
  • edges[i].length == 2
  • 0 <= a, b < n
  • a != b
  • There are no duplicate edges.

Approaches

2 worked solutions ranked from straightforward to optimal — each with intuition, algorithm, complexity, and verified code in C++, Java, Python, and Go.

Approach 1

DFS Cycle and Connectivity Check

Intuition

A valid tree with n nodes must have exactly n - 1 edges, be fully connected, and contain no cycles. We first check the edge count: if it is not n - 1, we immediately return false. Then we build an adjacency list and run DFS from node 0, tracking visited nodes. If any unvisited neighbor leads back to a visited node that is not the parent, a cycle exists. After DFS completes, we verify all nodes were visited (connectivity check). The parent tracking is essential for undirected graphs to avoid falsely detecting the edge we just traversed as a cycle.

Algorithm

  1. If the number of edges is not `n - 1`, return false immediately.
  2. Build an adjacency list from the edges.
  3. Run DFS from node 0, keeping track of visited nodes and each node's parent.
  4. For each neighbor of the current node, skip the parent. If a neighbor is already visited, a cycle exists -- return false. Otherwise, recurse.
  5. After DFS, check if all `n` nodes were visited. If not, the graph is disconnected.
  6. Return true only if no cycle was found and all nodes were visited.
TimeO(V + E) where V is the number of nodes and E is the number of edgesSpaceO(V + E) for the adjacency list and visited set

Code (C++ · Java · Python · Go)

C++17
class Solution {
public:
    bool validTree(int n, vector<vector<int>>& edges) {
        if (edges.size() != n - 1) return false;

        vector<vector<int>> graph(n);
        for (auto& e : edges) {
            graph[e[0]].push_back(e[1]);
            graph[e[1]].push_back(e[0]);
        }

        vector<bool> visited(n, false);
        if (hasCycle(graph, visited, 0, -1)) return false;

        for (bool v : visited) {
            if (!v) return false;
        }

        return true;
    }

private:
    bool hasCycle(vector<vector<int>>& graph, vector<bool>& visited, int node, int parent) {
        visited[node] = true;

        for (int neighbor : graph[node]) {
            if (neighbor == parent) continue;
            if (visited[neighbor]) return true;
            if (hasCycle(graph, visited, neighbor, node)) return true;
        }

        return false;
    }
};
Java 21
class Solution {
    public boolean validTree(int n, int[][] edges) {
        if (edges.length != n - 1) return false;

        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            graph.add(new ArrayList<>());
        }
        for (int[] e : edges) {
            graph.get(e[0]).add(e[1]);
            graph.get(e[1]).add(e[0]);
        }

        boolean[] visited = new boolean[n];
        if (hasCycle(graph, visited, 0, -1)) return false;

        for (boolean v : visited) {
            if (!v) return false;
        }

        return true;
    }

    private boolean hasCycle(List<List<Integer>> graph, boolean[] visited, int node, int parent) {
        visited[node] = true;

        for (int neighbor : graph.get(node)) {
            if (neighbor == parent) continue;
            if (visited[neighbor]) return true;
            if (hasCycle(graph, visited, neighbor, node)) return true;
        }

        return false;
    }
}
Python 3.12
from typing import List


class Solution:
    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        if len(edges) != n - 1:
            return False

        graph = [[] for _ in range(n)]
        for a, b in edges:
            graph[a].append(b)
            graph[b].append(a)

        visited = set()

        def has_cycle(node, parent):
            visited.add(node)
            for neighbor in graph[node]:
                if neighbor == parent:
                    continue
                if neighbor in visited:
                    return True
                if has_cycle(neighbor, node):
                    return True
            return False

        if has_cycle(0, -1):
            return False

        return len(visited) == n
Go 1.26
func validTree(n int, edges [][]int) bool {
    if len(edges) != n-1 {
        return false
    }

    graph := make([][]int, n)
    for i := range graph {
        graph[i] = []int{}
    }
    for _, e := range edges {
        graph[e[0]] = append(graph[e[0]], e[1])
        graph[e[1]] = append(graph[e[1]], e[0])
    }

    visited := make([]bool, n)

    var hasCycle func(node, parent int) bool
    hasCycle = func(node, parent int) bool {
        visited[node] = true
        for _, neighbor := range graph[node] {
            if neighbor == parent {
                continue
            }
            if visited[neighbor] {
                return true
            }
            if hasCycle(neighbor, node) {
                return true
            }
        }
        return false
    }

    if hasCycle(0, -1) {
        return false
    }

    for _, v := range visited {
        if !v {
            return false
        }
    }

    return true
}
Approach 2

Union-Find

Intuition

Union-Find (Disjoint Set Union) provides an elegant solution. We initialize each node as its own component. For each edge, we find the roots of both endpoints. If they share the same root, adding this edge creates a cycle -- return false. Otherwise, we union them. After processing all edges, we check if there is exactly one connected component. The quick check: a tree with n nodes must have exactly n - 1 edges, and no edge should connect two nodes already in the same component. Union-Find with path compression and union by rank makes this very efficient.

Algorithm

  1. If the number of edges is not `n - 1`, return false.
  2. Initialize a parent array where each node is its own parent, and a rank array for union by rank.
  3. For each edge, find the root of both endpoints using path compression.
  4. If both endpoints have the same root, a cycle exists -- return false.
  5. Union the two components using union by rank.
  6. If all edges are processed without finding a cycle, return true (edge count already guarantees connectivity).
TimeO(E * alpha(V)) which is nearly O(E), where alpha is the inverse Ackermann functionSpaceO(V) for the parent and rank arrays

Code (C++ · Java · Python · Go)

C++17
class Solution {
public:
    bool validTree(int n, vector<vector<int>>& edges) {
        if (edges.size() != n - 1) return false;

        vector<int> parent(n);
        vector<int> rank(n, 0);
        iota(parent.begin(), parent.end(), 0);

        for (auto& e : edges) {
            int rootA = find(parent, e[0]);
            int rootB = find(parent, e[1]);

            if (rootA == rootB) return false;

            if (rank[rootA] < rank[rootB]) swap(rootA, rootB);
            parent[rootB] = rootA;
            if (rank[rootA] == rank[rootB]) rank[rootA]++;
        }

        return true;
    }

private:
    int find(vector<int>& parent, int x) {
        if (parent[x] != x) {
            parent[x] = find(parent, parent[x]);
        }
        return parent[x];
    }
};
Java 21
class Solution {
    private int[] parent;
    private int[] rank;

    public boolean validTree(int n, int[][] edges) {
        if (edges.length != n - 1) return false;

        parent = new int[n];
        rank = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }

        for (int[] e : edges) {
            int rootA = find(e[0]);
            int rootB = find(e[1]);

            if (rootA == rootB) return false;

            if (rank[rootA] < rank[rootB]) {
                int temp = rootA;
                rootA = rootB;
                rootB = temp;
            }
            parent[rootB] = rootA;
            if (rank[rootA] == rank[rootB]) rank[rootA]++;
        }

        return true;
    }

    private int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }
}
Python 3.12
from typing import List


class Solution:
    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        if len(edges) != n - 1:
            return False

        parent = list(range(n))
        rank = [0] * n

        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]

        def union(a, b):
            root_a, root_b = find(a), find(b)
            if root_a == root_b:
                return False
            if rank[root_a] < rank[root_b]:
                root_a, root_b = root_b, root_a
            parent[root_b] = root_a
            if rank[root_a] == rank[root_b]:
                rank[root_a] += 1
            return True

        for a, b in edges:
            if not union(a, b):
                return False

        return True
Go 1.26
func validTree(n int, edges [][]int) bool {
    if len(edges) != n-1 {
        return false
    }

    parent := make([]int, n)
    rank := make([]int, n)
    for i := range parent {
        parent[i] = i
    }

    var find func(int) int
    find = func(x int) int {
        if parent[x] != x {
            parent[x] = find(parent[x])
        }
        return parent[x]
    }

    for _, e := range edges {
        rootA, rootB := find(e[0]), find(e[1])
        if rootA == rootB {
            return false
        }
        if rank[rootA] < rank[rootB] {
            rootA, rootB = rootB, rootA
        }
        parent[rootB] = rootA
        if rank[rootA] == rank[rootB] {
            rank[rootA]++
        }
    }

    return true
}

Frequently asked questions

What is the optimal time complexity of Graph Valid Tree?

The most efficient approach in this guide ("Union-Find") runs in O(E * alpha(V)) which is nearly O(E), where alpha is the inverse Ackermann function time and uses O(V) for the parent and rank arrays extra space. Earlier brute-force approaches are slower; see all 2 approaches above for the full progression.

What pattern does Graph Valid Tree use?

Graph Valid Tree is a medium-level Graphs problem involving Graph, Depth-First Search, Union-Find. Recognizing the pattern is half the work — see the intuition section above for how to spot it.

Can Graph Valid Tree be solved without extra space?

The most space-efficient approach in this guide uses O(V) for the parent and rank arrays extra space (excluding the input). If you're aiming for in-place — see the trade-off in the algorithm section above — the brute-force approach uses O(V + E) for the adjacency list and visited set, while the optimized version uses O(V) for the parent and rank arrays.

Which programming languages does this Ratta solution support?

Every approach above ships with verified, runnable solutions in C++, Java, Python, and Go. The starter templates in the workspace match the same four languages so you can practice in your interview language of choice.

Related Problems

Loading editor...
n=
5
edges=
[[0,1],[0,2],[0,3],[1,4]]
true