Lesson 120 of 164
Graph Valid Tree
You're given n vertices labeled 0 to n - 1 and a list of undirected edges. Return true if these edges form a valid tree: all vertices connected, and no cycles.
LeetCode: 261. Graph Valid Tree
Example 1: a tree
graph TB
0 --- 1
0 --- 2
0 --- 3
1 --- 4n = 5, edges = [(0,1), (0,2), (0,3), (1,4)]
Output: trueExample 2: has a cycle
graph TB
0 --- 1
1 --- 2
2 --- 3
1 --- 3
1 --- 4n = 5, edges = [(0,1), (1,2), (2,3), (1,3), (1,4)]
Output: false (1 - 2 - 3 - 1 is a cycle)The edge count rule
A tree with n vertices has exactly n - 1 edges. That gives a quick first check:
- More than
n - 1edges: there must be a cycle. - Fewer than
n - 1edges: the graph can't be connected.
With exactly n - 1 edges, "connected" and "no cycle" imply each other, so it is enough to check one of them. Both solutions below check for a cycle.
Brute force: before each edge, check if its ends are already connected
Add the edges one at a time. An edge (u, v) closes a cycle exactly when u and v are already connected by the edges added so far. Check that with a DFS over the partial graph.
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class Main {
static boolean connected(List<List<Integer>> adj, int a, int b) {
boolean[] visited = new boolean[adj.size()];
Deque<Integer> stack = new ArrayDeque<>();
stack.push(a);
while (!stack.isEmpty()) {
int v = stack.pop();
if (v == b) {
return true;
}
if (!visited[v]) {
visited[v] = true;
for (int w : adj.get(v)) {
stack.push(w);
}
}
}
return false;
}
static boolean validTree(int n, int[][] edges) {
if (edges.length + 1 != n) {
return false;
}
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
int u = e[0], v = e[1];
if (connected(adj, u, v)) {
return false; // this edge would close a cycle
}
adj.get(u).add(v);
adj.get(v).add(u);
}
return true; // n - 1 edges and no cycle: connected
}
public static void main(String[] args) {
System.out.println(validTree(5, new int[][]{{0, 1}, {0, 2}, {0, 3}, {1, 4}}));
System.out.println(validTree(5, new int[][]{{0, 1}, {1, 2}, {2, 3}, {1, 3}, {1, 4}}));
System.out.println(validTree(4, new int[][]{{0, 1}, {2, 3}, {1, 0}}));
}
}true
false
falseTime: O(E · (V + E)): one DFS per edge. Space: O(V + E).
Optimal: union-find
Union-find (also called disjoint set union) tracks which vertices are in the same connected group, and it answers "are a and b in the same group?" almost instantly. Each group is a small tree of parent links stored in an int array, and the vertex at the top is the group's root, its representative.
find(x): followparentlinks fromxup to the root.union(a, b): find both roots. If they're the same,aandbwere already connected, so this edge would close a cycle. Otherwise, attach one root under the other, merging the groups.
Two optimizations keep the trees almost flat:
- Path halving: during
find, point each visited node at its grandparent, which shortens the path for later searches. - Union by size: attach the smaller group under the larger one, so trees stay shallow.
Walkthrough with example 2. Each vertex starts as its own group: {0} {1} {2} {3} {4}.
| edge | find(u) | find(v) | same group? | groups after |
|---|---|---|---|---|
| (0, 1) | 0 | 1 | no, merge | {0, 1} {2} {3} {4} |
| (1, 2) | 0 | 2 | no, merge | {0, 1, 2} {3} {4} |
| (2, 3) | 0 | 3 | no, merge | {0, 1, 2, 3} {4} |
| (1, 3) | 0 | 0 | yes | cycle: return false |
public class Main {
static class UnionFind {
private final int[] parent;
private final int[] size;
UnionFind(int n) {
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // path halving
x = parent[x];
}
return x;
}
/** Merges the groups of a and b. Returns false if they were already in the same group. */
boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) {
return false;
}
if (size[ra] < size[rb]) {
int tmp = ra;
ra = rb;
rb = tmp;
}
parent[rb] = ra; // attach the smaller tree under the larger
size[ra] += size[rb];
return true;
}
}
static boolean validTree(int n, int[][] edges) {
if (edges.length + 1 != n) {
return false;
}
UnionFind uf = new UnionFind(n);
for (int[] e : edges) {
if (!uf.union(e[0], e[1])) {
return false;
}
}
return true;
}
public static void main(String[] args) {
System.out.println(validTree(5, new int[][]{{0, 1}, {0, 2}, {0, 3}, {1, 4}}));
System.out.println(validTree(5, new int[][]{{0, 1}, {1, 2}, {2, 3}, {1, 3}, {1, 4}}));
System.out.println(validTree(4, new int[][]{{0, 1}, {2, 3}, {1, 0}}));
System.out.println(validTree(1, new int[0][]));
}
}true
false
false
trueTime: O(E · α(V)), which is effectively O(E). α is the inverse Ackermann function, which grows so slowly that it's at most 4 for any input that fits in memory. Space: O(V).
The check edges.length + 1 != n returns false for n = 0 with no edges, since 0 + 1 != 0. A graph with no vertices is not treated as a tree.
Other uses of union-find
- Count connected components: start with
ngroups and subtract one for every successfulunion. - Kruskal's minimum spanning tree: sort the edges by weight and keep each edge whose
unionsucceeds. - Redundant connection (LeetCode 684): return the first edge whose
unionfails.
