1361. Validate Binary Tree Nodes
Problem Description
You are given n
binary tree nodes numbered from 0
to n - 1
. For each node i
, you have:
leftChild[i]
: the node number of its left child (or-1
if no left child exists)rightChild[i]
: the node number of its right child (or-1
if no right child exists)
Your task is to determine if these nodes form exactly one valid binary tree.
A valid binary tree must satisfy these conditions:
- Exactly one root: There must be exactly one node that has no parent (not referenced as anyone's child)
- No multiple parents: Each non-root node must have exactly one parent
- No cycles: The structure must not contain any cycles
- Single connected component: All nodes must be connected, forming one tree (not a forest)
The nodes themselves have no values - you're only working with the node numbers and their parent-child relationships.
Example scenarios that would return false
:
- A node appears as a child of multiple parents
- The nodes form a cycle
- The nodes form multiple disconnected trees (a forest)
- All nodes are children of some other node (no root exists)
Example scenario that would return true
:
- The nodes form a single connected tree with one root and no cycles
The solution uses a Union-Find (Disjoint Set Union) data structure to track connected components and detect cycles. It processes each parent-child relationship, checking for violations of the tree properties, and finally verifies that all nodes belong to a single connected component.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves nodes with parent-child relationships. A binary tree is a special type of graph where each node has at most two children and forms a hierarchical structure.
Is it a tree?
- Yes: We need to validate if the given nodes form exactly one valid binary tree. The problem explicitly asks us to check if the structure is a valid tree (connected, acyclic, with one root).
DFS
- Yes: DFS is the natural choice for tree validation problems. We can use DFS to:
- Traverse from the root to check if all nodes are reachable (connectivity)
- Detect cycles during traversal
- Ensure each node is visited exactly once
Conclusion: The flowchart suggests using DFS for this tree validation problem.
Why DFS is Appropriate
DFS perfectly fits this problem because:
- Tree Traversal: Starting from the root, we can traverse the entire tree using DFS to verify all nodes are connected
- Cycle Detection: During DFS traversal, if we encounter a node we've already visited (except for the parent), we've found a cycle
- Connectivity Check: After DFS completes, we can verify that all
n
nodes were visited exactly once - Parent Validation: While traversing, we can ensure each node (except root) has exactly one parent
The solution uses a Union-Find approach as an optimization, but the core problem-solving pattern aligns with DFS principles - exploring the graph structure to validate tree properties.
Intuition
When we think about what makes a valid binary tree, we need to ensure several key properties:
- One and only one root: Exactly one node should have no parent
- No node has multiple parents: Each child appears in at most one parent's left or right child array
- No cycles exist: Following parent-child relationships shouldn't lead us back to where we started
- All nodes are connected: Starting from the root, we should be able to reach all
n
nodes
The natural approach would be to use DFS - find the root node and traverse the tree to check these properties. However, there's a clever insight: we can check all these conditions simultaneously while building the tree structure using Union-Find.
Here's the key realization: as we process each parent-child relationship, we're essentially connecting components in a graph. If the nodes form a valid tree:
- We should start with
n
separate components (one for each node) - Each valid parent-child connection reduces the number of components by 1
- We should end with exactly 1 component containing all nodes
The Union-Find approach elegantly handles cycle detection too. When we try to connect a parent with its child:
- If they're already in the same component, adding this edge would create a cycle
- If the child already has a parent (tracked by
vis
array), it violates the "one parent" rule
Think of it like building a puzzle: we start with n
separate pieces. Each parent-child relationship tells us how to connect two pieces. If we follow all the instructions correctly:
- No piece should be forced to connect twice (no multiple parents)
- We shouldn't create loops (no cycles)
- We should end up with exactly one complete picture (one connected tree)
This is why the final check is simply n == 1
- after processing all valid connections, if we've reduced our n
components down to exactly 1, we have a valid binary tree.
Learn more about Tree, Depth-First Search, Breadth-First Search, Union Find, Graph and Binary Tree patterns.
Solution Approach
The solution uses the Union-Find (Disjoint Set Union) data structure to validate the binary tree. Let's walk through the implementation step by step:
Data Structures Used
- Parent array
p
: Initiallyp = [0, 1, 2, ..., n-1]
, where each node is its own parent (representingn
separate components) - Visited array
vis
: Tracks whether a node has already been assigned as someone's child (to detect multiple parents) - Component counter: We reuse variable
n
to count the number of connected components
Algorithm Implementation
Step 1: Initialize Union-Find
p = list(range(n)) # Each node is initially its own parent
vis = [False] * n # No node has a parent yet
Step 2: Process Each Parent-Child Relationship
For each node i
and its children leftChild[i]
and rightChild[i]
:
for i, (a, b) in enumerate(zip(leftChild, rightChild)):
for j in (a, b): # Process both left child (a) and right child (b)
if j != -1: # If child exists
Step 3: Validation Checks
For each valid child j
:
-
Check for multiple parents: If
vis[j]
isTrue
, nodej
already has a parentif vis[j]: return False # Multiple parents detected
-
Check for cycles: If
find(i) == find(j)
, parent and child are already in the same componentif find(i) == find(j): return False # Adding this edge would create a cycle
Step 4: Union Operation
If validations pass, connect the components:
p[find(i)] = find(j) # Union the two components vis[j] = True # Mark child as having a parent n -= 1 # Reduce component count by 1
Step 5: Path Compression in Find
The find
function uses path compression for efficiency:
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x]) # Recursively find root and compress path
return p[x]
Step 6: Final Validation
After processing all relationships:
return n == 1 # Should have exactly 1 connected component
Why This Works
- Starting state:
n
separate components - Each valid edge: Reduces components by 1
- Invalid cases caught:
- Multiple parents:
vis
array prevents a node from being a child twice - Cycles: Union-Find detects when connecting nodes already in the same component
- Multiple parents:
- Final state: If we have exactly 1 component (
n == 1
), all nodes are connected in a valid tree structure
The beauty of this approach is that it validates all tree properties simultaneously while building the structure, making it both elegant and efficient with O(n)
time complexity (with path compression, operations are nearly constant time).
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example Input:
n = 4
(nodes numbered 0, 1, 2, 3)leftChild = [1, -1, 3, -1]
rightChild = [2, -1, -1, -1]
This represents the tree:
0 / \ 1 2 / 3
Step-by-step execution:
Initial State:
p = [0, 1, 2, 3]
(each node is its own parent/component)vis = [False, False, False, False]
(no node has a parent yet)n = 4
(4 separate components)
Processing Node 0:
-
Left child = 1, Right child = 2
-
Process child 1:
vis[1] = False
✓ (node 1 has no parent yet)find(0) = 0
,find(1) = 1
→ different components ✓- Union:
p[0] = 1
(connect component 0 to component 1) vis[1] = True
(mark node 1 as having a parent)n = 3
(reduced to 3 components)- State:
p = [1, 1, 2, 3]
, components: {0,1}, {2}, {3}
-
Process child 2:
vis[2] = False
✓ (node 2 has no parent yet)find(0) = 1
,find(2) = 2
→ different components ✓- Union:
p[1] = 2
(connect component 1 to component 2) vis[2] = True
(mark node 2 as having a parent)n = 2
(reduced to 2 components)- State:
p = [1, 2, 2, 3]
, components: {0,1,2}, {3}
Processing Node 1:
- Left child = -1, Right child = -1
- No children to process, move to next node
Processing Node 2:
- Left child = 3, Right child = -1
- Process child 3:
vis[3] = False
✓ (node 3 has no parent yet)find(2) = 2
,find(3) = 3
→ different components ✓- Union:
p[2] = 3
(connect component 2 to component 3) vis[3] = True
(mark node 3 as having a parent)n = 1
(reduced to 1 component)- State:
p = [1, 2, 3, 3]
, components: {0,1,2,3}
Processing Node 3:
- Left child = -1, Right child = -1
- No children to process
Final Check:
n == 1
✓ (exactly one connected component)- Return
True
Example of Failure Case:
If we had leftChild = [1, 3, 3, -1]
instead (both node 1 and node 2 trying to have node 3 as a child):
- When processing node 1's child 3:
vis[3]
becomesTrue
- When processing node 2's child 3:
vis[3] = True
→ Multiple parents detected! - Return
False
Solution Implementation
1class Solution:
2 def validateBinaryTreeNodes(
3 self, n: int, leftChild: List[int], rightChild: List[int]
4 ) -> bool:
5 # Union-Find helper function to find the root of a node
6 def find_root(node: int) -> int:
7 # Path compression: make all nodes point directly to root
8 if parent[node] != node:
9 parent[node] = find_root(parent[node])
10 return parent[node]
11
12 # Initialize parent array where each node is its own parent (root)
13 parent = list(range(n))
14
15 # Track whether a node has been visited (has a parent assigned)
16 has_parent = [False] * n
17
18 # Counter for number of components (initially n separate nodes)
19 component_count = n
20
21 # Iterate through each node and its children
22 for node_index, (left_child, right_child) in enumerate(zip(leftChild, rightChild)):
23 # Check both left and right children
24 for child in (left_child, right_child):
25 if child != -1: # -1 indicates no child
26 # Check if child already has a parent (multiple parents not allowed)
27 # or if adding this edge would create a cycle
28 if has_parent[child] or find_root(node_index) == find_root(child):
29 return False
30
31 # Union operation: merge the two components
32 parent[find_root(node_index)] = find_root(child)
33
34 # Mark child as having a parent
35 has_parent[child] = True
36
37 # Decrease component count as two components are merged
38 component_count -= 1
39
40 # Valid binary tree should have exactly one component (all nodes connected)
41 return component_count == 1
42
1class Solution {
2 private int[] parent; // Union-Find parent array
3
4 public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) {
5 // Initialize Union-Find structure where each node is its own parent
6 parent = new int[n];
7 for (int i = 0; i < n; i++) {
8 parent[i] = i;
9 }
10
11 // Track which nodes have been visited (have a parent pointing to them)
12 boolean[] hasIncomingEdge = new boolean[n];
13
14 // Process each node and its children
15 int componentsCount = n; // Initially, each node is its own component
16
17 for (int currentNode = 0; currentNode < n; currentNode++) {
18 // Check both left and right children
19 int[] children = {leftChild[currentNode], rightChild[currentNode]};
20
21 for (int childNode : children) {
22 if (childNode != -1) { // Valid child exists
23 // Check if child already has a parent (invalid for tree)
24 if (hasIncomingEdge[childNode]) {
25 return false;
26 }
27
28 // Check if adding this edge creates a cycle
29 if (find(currentNode) == find(childNode)) {
30 return false;
31 }
32
33 // Union the two components: connect parent's root to child's root
34 parent[find(currentNode)] = find(childNode);
35
36 // Mark child as having an incoming edge
37 hasIncomingEdge[childNode] = true;
38
39 // Decrease component count as two components merge
40 componentsCount--;
41 }
42 }
43 }
44
45 // Valid binary tree should have exactly one component
46 return componentsCount == 1;
47 }
48
49 /**
50 * Find operation with path compression for Union-Find
51 * Returns the root parent of the given node
52 */
53 private int find(int node) {
54 if (parent[node] != node) {
55 parent[node] = find(parent[node]); // Path compression
56 }
57 return parent[node];
58 }
59}
60
1class Solution {
2public:
3 bool validateBinaryTreeNodes(int n, vector<int>& leftChild, vector<int>& rightChild) {
4 // Initialize Union-Find parent array where each node is its own parent initially
5 vector<int> parent(n);
6 iota(parent.begin(), parent.end(), 0);
7
8 // Track which nodes have been visited (have a parent assigned)
9 vector<bool> hasParent(n, false);
10
11 // Union-Find with path compression to find the root of a node
12 function<int(int)> findRoot = [&](int x) {
13 if (parent[x] != x) {
14 parent[x] = findRoot(parent[x]); // Path compression
15 }
16 return parent[x];
17 };
18
19 // Track the number of components (initially n separate nodes)
20 int componentCount = n;
21
22 // Process each node and its children
23 for (int nodeIndex = 0; nodeIndex < n; ++nodeIndex) {
24 // Check both left and right children
25 for (int childNode : {leftChild[nodeIndex], rightChild[nodeIndex]}) {
26 if (childNode != -1) {
27 // Check if child already has a parent (invalid tree structure)
28 if (hasParent[childNode]) {
29 return false;
30 }
31
32 // Check if adding this edge would create a cycle
33 int parentRoot = findRoot(nodeIndex);
34 int childRoot = findRoot(childNode);
35 if (parentRoot == childRoot) {
36 return false; // Cycle detected
37 }
38
39 // Union the two components
40 parent[parentRoot] = childRoot;
41
42 // Mark child as having a parent
43 hasParent[childNode] = true;
44
45 // Decrease component count as two components are merged
46 --componentCount;
47 }
48 }
49 }
50
51 // Valid binary tree should have exactly one component
52 return componentCount == 1;
53 }
54};
55
1function validateBinaryTreeNodes(n: number, leftChild: number[], rightChild: number[]): boolean {
2 // Initialize Union-Find parent array where each node is its own parent initially
3 const parent: number[] = Array.from({ length: n }, (_, i) => i);
4
5 // Track which nodes have been visited (have a parent assigned)
6 const hasParent: boolean[] = new Array(n).fill(false);
7
8 // Union-Find with path compression to find the root of a node
9 const findRoot = (x: number): number => {
10 if (parent[x] !== x) {
11 parent[x] = findRoot(parent[x]); // Path compression
12 }
13 return parent[x];
14 };
15
16 // Track the number of components (initially n separate nodes)
17 let componentCount: number = n;
18
19 // Process each node and its children
20 for (let nodeIndex = 0; nodeIndex < n; nodeIndex++) {
21 // Check both left and right children
22 for (const childNode of [leftChild[nodeIndex], rightChild[nodeIndex]]) {
23 if (childNode !== -1) {
24 // Check if child already has a parent (invalid tree structure)
25 if (hasParent[childNode]) {
26 return false;
27 }
28
29 // Check if adding this edge would create a cycle
30 const parentRoot: number = findRoot(nodeIndex);
31 const childRoot: number = findRoot(childNode);
32 if (parentRoot === childRoot) {
33 return false; // Cycle detected
34 }
35
36 // Union the two components
37 parent[parentRoot] = childRoot;
38
39 // Mark child as having a parent
40 hasParent[childNode] = true;
41
42 // Decrease component count as two components are merged
43 componentCount--;
44 }
45 }
46 }
47
48 // Valid binary tree should have exactly one component
49 return componentCount === 1;
50}
51
Time and Space Complexity
The time complexity is O(n × α(n))
, where n
is the number of nodes and α(n)
is the inverse Ackermann function. The inverse Ackermann function grows extremely slowly and is effectively constant for all practical values of n
, typically less than 5.
This complexity arises from:
- The main loop iterates through all
n
nodes once:O(n)
- For each node, we potentially examine up to 2 children (left and right)
- For each child, we perform union-find operations (
find
and union), which have amortized time complexity ofO(α(n))
per operation due to path compression - Total:
O(n × α(n))
The space complexity is O(n)
, which comes from:
- The parent array
p
of sizen
:O(n)
- The visited array
vis
of sizen
:O(n)
- The recursion stack for the
find
function in the worst case:O(n)
(though with path compression, this is typically much smaller) - Total:
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Union Direction
The most critical pitfall in this solution is the direction of the union operation. In the provided code:
parent[find_root(node_index)] = find_root(child)
This makes the parent point to the child's component, which is backwards from the logical tree structure. While this doesn't affect connectivity checking, it can cause confusion and potential issues if the Union-Find structure is used for other purposes.
Correct approach:
parent[find_root(child)] = find_root(node_index)
This makes the child's root point to the parent's root, maintaining the logical parent-child relationship.
2. Missing Root Validation
The code doesn't explicitly check that exactly one root exists. While the component count check catches many cases, it's clearer to verify that exactly one node has no parent:
# After processing all edges
root_count = sum(1 for i in range(n) if not has_parent[i])
return root_count == 1 and component_count == 1
3. Inefficient Find Without Path Compression
Some implementations might forget to update the parent during path compression:
Inefficient version:
def find_root(node):
while parent[node] != node:
node = parent[node]
return node
Efficient version with path compression:
def find_root(node):
if parent[node] != node:
parent[node] = find_root(parent[node]) # Compress path
return parent[node]
4. Not Checking Self-Loops
The code should verify that a node doesn't point to itself as a child:
for node_index, (left_child, right_child) in enumerate(zip(leftChild, rightChild)):
for child in (left_child, right_child):
if child != -1:
# Add self-loop check
if child == node_index:
return False
# ... rest of the validation
5. Confusing Variable Names
Using n
for both the initial node count and component counter can be confusing. Better practice:
def validateBinaryTreeNodes(self, n: int, leftChild: List[int], rightChild: List[int]) -> bool:
num_nodes = n
component_count = n # Separate variable for clarity
# ... rest of the code
Complete Corrected Solution:
class Solution:
def validateBinaryTreeNodes(self, n: int, leftChild: List[int], rightChild: List[int]) -> bool:
def find_root(node: int) -> int:
if parent[node] != node:
parent[node] = find_root(parent[node])
return parent[node]
parent = list(range(n))
has_parent = [False] * n
component_count = n
for node_index, (left_child, right_child) in enumerate(zip(leftChild, rightChild)):
for child in (left_child, right_child):
if child != -1:
# Check for self-loop
if child == node_index:
return False
# Check for multiple parents or cycle
if has_parent[child] or find_root(node_index) == find_root(child):
return False
# Correct union direction: child's root points to parent's root
parent[find_root(child)] = find_root(node_index)
has_parent[child] = True
component_count -= 1
# Verify exactly one root and one component
root_count = sum(1 for i in range(n) if not has_parent[i])
return root_count == 1 and component_count == 1
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!