Facebook Pixel

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:

  1. Exactly one root: There must be exactly one node that has no parent (not referenced as anyone's child)
  2. No multiple parents: Each non-root node must have exactly one parent
  3. No cycles: The structure must not contain any cycles
  4. 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:

  1. Tree Traversal: Starting from the root, we can traverse the entire tree using DFS to verify all nodes are connected
  2. Cycle Detection: During DFS traversal, if we encounter a node we've already visited (except for the parent), we've found a cycle
  3. Connectivity Check: After DFS completes, we can verify that all n nodes were visited exactly once
  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we think about what makes a valid binary tree, we need to ensure several key properties:

  1. One and only one root: Exactly one node should have no parent
  2. No node has multiple parents: Each child appears in at most one parent's left or right child array
  3. No cycles exist: Following parent-child relationships shouldn't lead us back to where we started
  4. 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

  1. Parent array p: Initially p = [0, 1, 2, ..., n-1], where each node is its own parent (representing n separate components)
  2. Visited array vis: Tracks whether a node has already been assigned as someone's child (to detect multiple parents)
  3. 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:

  1. Check for multiple parents: If vis[j] is True, node j already has a parent

    if vis[j]:
        return False  # Multiple parents detected
  2. Check for cycles: If find(i) == find(j), parent and child are already in the same component

    if 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
  • 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 Evaluator

Example 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] becomes True
  • 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 of O(α(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 size n: O(n)
  • The visited array vis of size n: 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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More