1361. Validate Binary Tree Nodes
Problem Description
You are given a number n
, which represents the total number of binary tree nodes. Each node is uniquely numbered from 0
to n-1
. Along with this, you are provided two lists, leftChild
and rightChild
, which represent the indices of the left and right children for each node. If a node does not have a left or right child, its corresponding value in the list will be -1
. Your task is to determine if all these nodes can form exactly one valid binary tree.
To qualify as a valid binary tree, there must be exactly one root node (a node without a parent), and each node must have exactly one parent, except for the root. Furthermore, there must be no cycles within the tree structure—no node can be an ancestor of itself.
Flowchart Walkthrough
Let's navigate through the decision-making process using the Flowchart to determine the appropriate algorithm for solving LeetCode problem 1361. Validate Binary Tree Nodes. We'll decide step-by-step:
Is it a graph?
- Yes: The problem involves validating if a collection of nodes forms a valid tree, which can be visualized as a graph where each node potentially has connections (edges).
Is it a tree?
- Yes: The entire goal is to verify if the given structure is a valid binary tree.
Therefore, based on the flowchart, after confirming that we are working with a tree, the appropriate algorithm to use is:
DFS (Depth-First Search)
Conclusion: The flowchart indicates that DFS is suitable for this scenario, as we need to traverse each node and ensure there are no cycles, and that all nodes (except the root) have exactly one parent. These properties are efficiently checked using DFS in structural tree validation problems.
Intuition
The approach to solve this problem involves using a union-find data structure, which is perfect for checking cycles and connections between nodes. The primary idea behind union-find is to group nodes into sets. Each set is identified by a representative, which is one of the nodes in the set (initially, each node is its own representative).
The solution follows these steps:
-
Initialization: Create a union-find structure (
p
) where each node is initially part of its own set, and a visited array (vis
) to keep track of whether a node has already been linked as a child to another node. -
Iterative Check: Go through each node's children (left and right). If a child node is already visited (
vis[j]
istrue
), it means that child has more than one parent, which invalidates the tree structure. If the parent of the node is the same as the parent of its child (find(i) == find(j)
), it means that a cycle is detected, which again invalidates the tree. -
Union Operations: If a child has not been visited and it doesn't create a cycle, we perform a union operation. This involves setting the parent of the child's root to be the root of the current node, and marking the child as visited.
-
Validate Tree: Finally, after iterating through all nodes, we check if there is exactly one node left without a parent (one root), which means that
n == 1
at the end of the process after decrementing the count when nodes are linked. Ifn == 1
, it indicates that we have exactly one tree and it is valid, so we returntrue
. Any other value indicates an invalid tree.
In summary, the union-find helps efficiently manage the connectivity between nodes while making it easy to check for cycles and multiple parents, ensuring we end up with a valid binary tree if possible.
Learn more about Tree, Depth-First Search, Breadth-First Search, Union Find, Graph and Binary Tree patterns.
Solution Approach
The implementation of the solution revolves around the union-find algorithm, which is a well-known algorithm used in situations where we need to find and manage connected components or sets, especially when the union and find operations are needed frequently and must perform efficiently.
Here is an explanation of each part of the implementation:
-
Initialization: The lists
p
andvis
are initialized. The listp
represents the parent of each node, and it's initialized such that each node is its own parent. The listvis
is a boolean array initialized toFalse
, indicating that no nodes have been visited initially.p = list(range(n)) vis = [False] * n
-
Find Function: The
find
function is a recursive function that traverses thep
array to find the root of the set that contains nodex
. If the current nodex
is not the root (i.e.,p[x] != x
), it performs path compression by settingp[x]
to the result of a recursive call tofind(p[x])
. Path compression is a technique that flattens the structure of the sets for faster future lookup.def find(x: int) -> int: if p[x] != x: p[x] = find(p[x]) return p[x]
-
Iterate Over Nodes: We loop over all nodes, and for each node, we loop over its left and right children.
for i, (a, b) in enumerate(zip(leftChild, rightChild)):
-
Check and Union: Inside the loop, we check if child node
j
is not-1
, which means the current node has a child. There are two important conditions to check for here:- If
vis[j]
isTrue
, it means the child nodej
is already connected to a parent, indicating an invalid tree because a node has more than one parent. - If
find(i) == find(j)
, it means the parent of nodei
is in the same set as childj
, indicating a cycle in the tree, which invalidates the binary tree property.
If neither condition is met, we perform a union operation by setting the parent of
j
's root to bei
's root, markingj
as visited, and decrementingn
by 1, because we have linkedj
under a parent.for j in (a, b): if j != -1: if vis[j] or find(i) == find(j): return False p[find(i)] = find(j) vis[j] = True n -= 1
- If
-
Validate by Single Root: After the loop, the last validation step is to check whether
n == 1
. This validation ensures that there is exactly one root node left, implying that the remaining unvisited node is the root of the entire binary tree. If this condition holds, it means all nodes have been connected properly and exactly one valid binary tree has been formed.return n == 1
In conclusion, by utilizing the union-find algorithm and taking into account the unique properties of a binary tree, we can determine whether the given nodes can form exactly one valid binary tree.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach provided.
- Suppose we have
n = 4
nodes, which are numbered0
to3
. - Let's say the
leftChild
list is[1, -1, -1, -1]
and therightChild
list is[2, 3, -1, -1]
. This implies that:- Node
0
has1
as its left child and2
as its right child. - Node
1
does not have any children, but it is the right child of Node0
. - Node
2
is the left child of Node0
and does not have any children. - Node
3
does not have any children but is a right child of Node1
.
- Node
Using the solution approach:
-
Initialization: We initialize parent array
p
to[0, 1, 2, 3]
and visited arrayvis
to[False, False, False, False]
. -
Find Function: The
find
function will be used later to determine the roots of the nodes and apply path compression. -
Iterate Over Nodes:
- For Node
0
,leftChild[0] = 1
andrightChild[0] = 2
. - For Node
1
,leftChild[1] = -1
(no left child) andrightChild[1] = 3
.
- For Node
-
Check and Union:
First, iterate for Node
0
:- For
leftChild[0] = 1
:vis[1]
isFalse
, so Node1
has no parent yet.find(0)
is0
andfind(1)
is1
. They are not the same, no cycle here.- Perform union: set
p[1]
to0
(p[find(1)] = find(0)
), markvis[1]
asTrue
, decrementn
to3
.
- For
rightChild[0] = 2
:vis[2]
isFalse
. Node2
has no parent yet.find(0)
is0
andfind(2)
is2
. They are not the same, no cycle here.- Perform union: set
p[2]
to0
(p[find(2)] = find(0)
), markvis[2]
asTrue
, decrementn
to2
.
- For
Then, iterate for Node 1
:
- For
rightChild[1] = 3
:vis[3]
isFalse
. Node3
has no parent yet.find(1)
is0
andfind(3)
is3
. They are not the same, no cycle here.- Perform union: set
p[3]
to0
(p[find(3)] = find(1)
), markvis[3]
asTrue
, decrementn
to1
.
- Validate by Single Root: After looping through all nodes, we check if
n == 1
. Sincen
has been decremented correctly to1
, we have exactly one node left without a parent. This indicates there is exactly one root in the binary tree, and the tree is valid.
If we visualize the final binary tree formed, it looks like this:
0 / \ 1 2 \ 3
The tree formed is indeed a valid binary tree with Node 0
as the root, and each node has exactly one parent, fulfilling the requirements of the problem. Therefore, the implemented solution would return True
.
Solution Implementation
1from typing import List
2
3class Solution:
4 def validateBinaryTreeNodes(
5 self, node_count: int, left_children: List[int], right_children: List[int]
6 ) -> bool:
7 # Helper function to find the root of the set in which element x is.
8 def find_root(x: int) -> int:
9 if parents[x] != x:
10 # Path compression: Directly attaching x to its root to optimize further queries
11 parents[x] = find_root(parents[x])
12 return parents[x]
13
14 # Initialize each node to be its own parent (self loop).
15 # This also sets up the forest for the union-find algorithm.
16 parents = list(range(node_count))
17
18 # Visited array to keep track if a node has already been assigned a parent.
19 visited = [False] * node_count
20
21 # Iterate over each node and their respective left and right children
22 for i, (left_child, right_child) in enumerate(zip(left_children, right_children)):
23 # Iterate over both children of the current node
24 for child in (left_child, right_child):
25 # If the child is not a placeholder value (-1 indicates no child)
26 if child != -1:
27 # If the child has been visited, or if parent of current node and child node are the same
28 # then the tree is not valid.
29 if visited[child] or find_root(i) == find_root(child):
30 return False
31 # Union operation: set the parent of the current node's parent to the child's parent
32 # effectively linking the trees of the current node and child.
33 parents[find_root(i)] = find_root(child)
34 # Mark the child as visited
35 visited[child] = True
36 # Decrement the node count, aiming to validate if only one root exists at the end
37 node_count -= 1
38
39 # The tree is valid if it has exactly one node that is not a child of any other node (hence one root).
40 return node_count == 1
41
1class Solution {
2 // Parents array to keep track of the roots of each node
3 private int[] parents;
4
5 /**
6 * This function checks if the given nodes form a valid binary tree.
7 *
8 * @param n the number of nodes.
9 * @param leftChild array containing the indices of left children.
10 * @param rightChild array containing the indices of right children.
11 * @return boolean whether the input forms a valid binary tree.
12 */
13 public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) {
14 // Initialize parents array where each node is its own parent at the start
15 parents = new int[n];
16 for (int i = 0; i < n; i++) {
17 parents[i] = i;
18 }
19
20 // Visited array to check if a node has already been assigned a parent
21 boolean[] visited = new boolean[n];
22
23 // Iterate over all nodes
24 for (int nodeIndex = 0; nodeIndex < n; nodeIndex++) {
25 // Check both children of the current node
26 for (int child : new int[]{leftChild[nodeIndex], rightChild[nodeIndex]}) {
27 if (child != -1) { // If the child is not a null node (-1)
28 // If either the child has already been visited (has another parent) or the child and parent are part of the same subtree,
29 // the tree is invalid
30 if (visited[child] || find(nodeIndex) == find(child)) {
31 return false;
32 }
33 // Union the current node and the child
34 parents[find(nodeIndex)] = find(child);
35
36 // Mark the child as visited
37 visited[child] = true;
38
39 // Decrement the remaining nodes count
40 --n;
41 }
42 }
43 }
44
45 // Check if exactly one root is remaining, which implies a valid binary tree
46 return n == 1;
47 }
48
49 /**
50 * Helper method to find the root of the given node using path compression.
51 *
52 * @param node the node to find the root for.
53 * @return the root of the given node.
54 */
55 private int find(int node) {
56 // Recursive path compression: nodes on the path directly point to root
57 if (parents[node] != node) {
58 parents[node] = find(parents[node]);
59 }
60 return parents[node];
61 }
62}
63
1#include <vector>
2#include <numeric>
3#include <cstring>
4#include <functional>
5
6class Solution {
7public:
8 bool validateBinaryTreeNodes(int nodeCount, vector<int>& leftChildren, vector<int>& rightChildren) {
9 // Array 'parents' keeps track of the root of each node's disjoint set
10 int parents[nodeCount];
11 // Initialize each node as its own parent to signify its own set
12 iota(parents, parents + nodeCount, 0);
13
14 // Array 'visited' indicates whether a node has been added to the tree
15 bool visited[nodeCount];
16 // Initialize all nodes as unvisited
17 memset(visited, 0, sizeof(visited));
18
19 // Find function using path compression for disjoint set data structure
20 function<int(int)> find = [&](int node) {
21 // Traverse up the tree until the root is found, and compress path
22 return parents[node] == node ? node : parents[node] = find(parents[node]);
23 };
24
25 // Iterate over the nodes to build the tree and check for cycles
26 for (int i = 0; i < nodeCount; ++i) {
27 for (int child : {leftChildren[i], rightChildren[i]}) {
28 // If the child exists (-1 represents a non-existent child)
29 if (child != -1) {
30 // If the child has already been visited or is part of the same set, it's not a valid tree
31 if (visited[child] || find(i) == find(child)) {
32 return false;
33 }
34 // Union: merge the set that contains the current node with the set that contains the child
35 parents[find(i)] = find(child);
36 // Mark the child as visited
37 visited[child] = true;
38 // Decrease the number of roots by one - as one more node is connected to the tree
39 --nodeCount;
40 }
41 }
42 }
43 // There should be exactly one root remaining for a valid binary tree
44 return nodeCount == 1;
45 }
46};
47
1function validateBinaryTreeNodes(nodeCount: number, leftChildren: Array<number>, rightChildren: Array<number>): boolean {
2 // Array 'parents' keeps track of the root of each node's disjoint set
3 let parents = Array.from({length: nodeCount}, (_, i) => i);
4
5 // Array 'visited' indicates whether a node has been added to the tree
6 let visited = new Array<boolean>(nodeCount).fill(false);
7
8 // Find function using path compression for disjoint set data structure
9 function find(node: number): number {
10 // Traverse up the tree until the root is found, and compress path
11 if (parents[node] !== node) {
12 parents[node] = find(parents[node]);
13 }
14 return parents[node];
15 }
16
17 // Iterate over the nodes to build the tree and check for cycles
18 for (let i = 0; i < nodeCount; ++i) {
19 [leftChildren[i], rightChildren[i]].forEach(child => {
20 // If the child exists (-1 represents a non-existent child)
21 if (child !== -1) {
22 // If the child has already been visited or is part of the same set, it's not a valid tree
23 if (visited[child] || find(i) === find(child)) {
24 return false;
25 }
26 // Union: merge the set that contains the current node with the set that contains the child
27 parents[find(i)] = find(child);
28 // Mark the child as visited
29 visited[child] = true;
30 // Decrease the number of roots by one - as one more node is connected to the tree
31 --nodeCount;
32 }
33 });
34
35 // After checking both children, if the return false condition was met, we should terminate early
36 if (nodeCount < 1) {
37 return false;
38 }
39 }
40 // There should be exactly one root remaining for a valid binary tree
41 return nodeCount === 1;
42}
43
Time and Space Complexity
The given code snippet is a Python method designed to validate a binary tree given the number of nodes n
and two lists denoting the indices of left and right children. The function utilizes the Union-Find algorithm, also known as the Disjoint Set Union (DSU) algorithm, to determine if the given input constitutes a valid tree structure.
Time Complexity
The time complexity of this algorithm is primarily determined by two factors: the traversals over the leftChild
and rightChild
lists, and the union-find operations.
-
Iterating over the
leftChild
andrightChild
lists occurs once for each node in the binary tree. This operation has a linear time complexity ofO(n)
because each node's children are checked exactly once. -
The union-find operations consist of the
find
function, which includes path compression. While a naïve implementation of union-find has a time complexity approachingO(n)
for eachfind
operation, path compression optimizes this to an amortized time complexity of approximatelyO(α(n))
, whereα(n)
is the inverse Ackermann function. The inverse Ackermann function grows extremely slowly and, for all practical purposes, can be considered almost constant.
Considering these factors, the overall time complexity of the union-find operations is O(n * α(n))
. However, due to the extremely slow growth of the α(n)
term, the time complexity is effectively O(n)
. Therefore, the total time complexity of the validateBinaryTreeNodes
function is O(n)
.
Space Complexity
The space complexity is determined by the space required to maintain the p
array and the vis
array, used to track the parents in the union-find structure and the visited state of nodes, respectively.
-
The
p
array is initialized with a size ofn
, resulting in a space complexity ofO(n)
. -
The
vis
array is also of sizen
, contributing anotherO(n)
to the space complexity.
These are the only significant sources of space usage in the algorithm. Since they are linear with respect to the number of nodes n
, the overall space complexity is also O(n)
.
In summary, for the provided validateBinaryTreeNodes
function:
- The time complexity is
O(n)
. - The space complexity is
O(n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
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 algomonster s3 us east 2 amazonaws com 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
https algomonster s3 us east 2 amazonaws com 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
Want a Structured Path to Master System Design Too? Don’t Miss This!