Smallest Missing Genetic Value in Each Subtree
HardPrompt
There is a family tree rooted at 0 consisting of n nodes numbered 0 to n - 1. You are given a 0-indexed integer array parents, where parents[i] is the parent for node i. Since node 0 is the root, parents[0] == -1.
There are 105 genetic values, each represented by an integer in the inclusive range [1, 105]. You are given a 0-indexed integer array nums, where nums[i] is a distinct genetic value for node i.
Return an array ans of length n where ans[i] is the smallest genetic value that is missing from the subtree rooted at node i.
The subtree rooted at a node x contains node x and all of its descendant nodes.
Example 1:
Input: parents = [-1,0,0,2], nums = [1,2,3,4]
Output: [5,1,1,1]
Explanation: The answer for each subtree is calculated as follows:
- 0: The subtree contains nodes [0,1,2,3] with values [1,2,3,4]. 5 is the smallest missing value.
- 1: The subtree contains only node 1 with value 2. 1 is the smallest missing value.
- 2: The subtree contains nodes [2,3] with values [3,4]. 1 is the smallest missing value.
- 3: The subtree contains only node 3 with value 4. 1 is the smallest missing value.Example 2:
Input: parents = [-1,0,1,0,3,3], nums = [5,4,6,2,1,3]
Output: [7,1,1,4,2,1]
Explanation: The answer for each subtree is calculated as follows:
- 0: The subtree contains nodes [0,1,2,3,4,5] with values [5,4,6,2,1,3]. 7 is the smallest missing value.
- 1: The subtree contains nodes [1,2] with values [4,6]. 1 is the smallest missing value.
- 2: The subtree contains only node 2 with value 6. 1 is the smallest missing value.
- 3: The subtree contains nodes [3,4,5] with values [2,1,3]. 4 is the smallest missing value.
- 4: The subtree contains only node 4 with value 1. 2 is the smallest missing value.
- 5: The subtree contains only node 5 with value 3. 1 is the smallest missing value.Example 3:
Input: parents = [-1,2,3,0,2,4,1], nums = [2,3,4,5,6,7,8]
Output: [1,1,1,1,1,1,1]
Explanation: The value 1 is missing from all the subtrees.
Constraints:
n == parents.length == nums.length2 <= n <= 1050 <= parents[i] <= n - 1fori != 0parents[0] == -1parentsrepresents a valid tree.1 <= nums[i] <= 105- Each
nums[i]is distinct.
Approaches
2 approaches with complexity analysis and trade-offs.
A straightforward approach is to iterate through each node, and for each node, determine its subtree. Then, we collect all the genetic values within that subtree and find the smallest positive integer not present in the collection. This method is easy to conceptualize but computationally expensive.
Algorithm
-
- Construct an adjacency list for the tree from the
parentsarray to represent the child-parent relationships.
- Construct an adjacency list for the tree from the
-
- Initialize an answer array
ansof sizen.
- Initialize an answer array
-
- Iterate through each node
ifrom0ton-1.
- Iterate through each node
-
- For each node
i:
- a. Create an empty
HashSetto store the genetic values of the nodes in the subtree ofi. - b. Perform a Depth First Search (DFS) or Breadth First Search (BFS) starting from node
ito traverse its entire subtree. - c. For each node
jvisited in the traversal, add its genetic valuenums[j]to the hash set. - d. After collecting all values, find the smallest missing positive integer. Initialize a variable
missingValue = 1. - e. While the hash set contains
missingValue, incrementmissingValue. - f. Store the final
missingValueinans[i].
- For each node
-
- After iterating through all nodes, return the
ansarray.
- After iterating through all nodes, return the
Walkthrough
This approach tackles the problem directly by processing each node one by one. For every node i in the tree, we need to find the smallest missing genetic value in its subtree.
First, we convert the parents array into a more convenient graph representation, like an adjacency list, where adj[u] contains all children of node u. This allows for easy traversal of subtrees.
Then, for each node i from 0 to n-1, we perform a traversal (e.g., DFS) starting from i. This traversal visits i and all of its descendants. During the traversal, we use a HashSet to collect the distinct genetic values (nums) of all the nodes in the current subtree. The HashSet provides fast O(1) average time complexity for insertions and lookups.
Once the set of genetic values for the subtree of i is complete, we search for the smallest missing positive integer. We start checking from 1 and increment upwards, using the contains method of the hash set to see if the number is present. The first integer we find that is not in the set is the answer for the subtree rooted at i.
We repeat this entire process for all n nodes to fill the final answer array.
import java.util.*; class Solution { public int[] smallestMissingValueSubtree(int[] parents, int[] nums) { int n = parents.length; List<List<Integer>> children = new ArrayList<>(); for (int i = 0; i < n; i++) { children.add(new ArrayList<>()); } for (int i = 1; i < n; i++) { children.get(parents[i]).add(i); } int[] ans = new int[n]; for (int i = 0; i < n; i++) { Set<Integer> subtreeValues = new HashSet<>(); collectSubtreeValues(i, children, nums, subtreeValues); int missing = 1; while (subtreeValues.contains(missing)) { missing++; } ans[i] = missing; } return ans; } private void collectSubtreeValues(int u, List<List<Integer>> children, int[] nums, Set<Integer> values) { values.add(nums[u]); for (int v : children.get(u)) { collectSubtreeValues(v, children, nums, values); } }}Complexity
Time
O(N^2). We iterate through N nodes. For each node `i`, we traverse its subtree. The size of a subtree can be up to N. The sum of all subtree sizes can be O(N^2) in the worst case (a path or skewed tree). For each subtree, collecting values and finding the missing one takes time proportional to the subtree size. Thus, the total time complexity is O(N^2).
Space
O(N). The adjacency list requires O(N) space. During the calculation for each node, the `HashSet` can grow to store up to N values in the worst case (for the root's subtree), thus requiring O(N) auxiliary space. The recursion stack for DFS can also go up to O(N) in depth for a skewed tree.
Trade-offs
Pros
- It is simple to understand and implement.
- It correctly solves the problem for small inputs.
Cons
- The time complexity is O(N^2) in the worst-case scenario (e.g., a skewed tree), which is too slow for the given constraints and will result in a 'Time Limit Exceeded' error on most platforms.
This approach is based on a key observation: the smallest missing genetic value for any subtree is 1 unless the subtree contains the genetic value 1. This insight allows us to optimize the problem by focusing only on the subtrees that contain the value 1. These are the subtrees rooted at the node with value 1 and all of its ancestors. By processing only this path and its side-branches, we can achieve a linear time solution.
Algorithm
-
- Build an adjacency list representation of the tree from the
parentsarray.
- Build an adjacency list representation of the tree from the
-
- Find the node
oneNodethat has the genetic value1. If no such node exists, the smallest missing value for every subtree is1, so return an array filled with1s.
- Find the node
-
- Initialize an answer array
ansof sizenwith all elements set to1. This is the default answer for any subtree that does not contain the value1.
- Initialize an answer array
-
- Initialize an empty
HashSet<Integer>calledseenValuesto store genetic values, and an integermissingValueto1.
- Initialize an empty
-
- Traverse upwards from
oneNodeto the root of the tree. LetcurrentNodebe the node being processed andprevNodebe the child on the path we just came from.
- Traverse upwards from
-
- In a loop, while
currentNodeis valid (not -1):
- a. Add the value
nums[currentNode]toseenValues. - b. For each child of
currentNode, if the child is notprevNode, perform a full DFS from that child to collect all genetic values in its subtree and add them toseenValues. - c. After collecting all new values for the current subtree, update
missingValueby incrementing it as long asseenValuescontains the currentmissingValue. - d. Set
ans[currentNode] = missingValue. - e. Move up the path:
prevNode = currentNode,currentNode = parents[currentNode].
- In a loop, while
-
- Return the
ansarray.
- Return the
Walkthrough
The efficiency of this method comes from avoiding redundant computations. We start by recognizing that any subtree that lacks the genetic value 1 will have 1 as its smallest missing value. Therefore, we can initialize our answer array ans with all 1s.
The problem then reduces to finding the correct answer only for the nodes whose subtrees contain the value 1. These nodes are precisely the node with value 1 (let's call it oneNode) and all its ancestors up to the root.
We can find oneNode with a single pass through the nums array. If 1 is not present in nums, all answers are 1, and we're done. Otherwise, we start an iterative process from oneNode, moving up towards the root using the parents array.
We maintain a single HashSet called seenValues that accumulates the genetic values of the subtrees as we move up the path. We also keep track of the smallest_missing value, initialized to 1.
For each currentNode on the path from oneNode to the root:
- We expand our set of values. The subtree of
currentNodeincludescurrentNodeitself, the subtree of the child we just came from (prevNode), and the subtrees of all other children. Since the values fromprevNode's subtree are already inseenValues, we only need to addnums[currentNode]and the values from the subtrees ofcurrentNode's other children (the "side branches"). - We achieve this by adding
nums[currentNode]and then for each child ofcurrentNodethat is notprevNode, we perform a DFS to add all values from its subtree intoseenValues. - After updating
seenValues, we find the newsmallest_missingvalue by incrementing it until we find a value not in the set. - This value is the answer for
currentNode, so we setans[currentNode].
This process is efficient because each node in the tree is visited and its value is added to the set exactly once, leading to an overall linear time complexity.
import java.util.*; class Solution { public int[] smallestMissingValueSubtree(int[] parents, int[] nums) { int n = parents.length; List<List<Integer>> children = new ArrayList<>(); for (int i = 0; i < n; i++) { children.add(new ArrayList<>()); } int oneNode = -1; for (int i = 0; i < n; i++) { if (i != 0) { children.get(parents[i]).add(i); } if (nums[i] == 1) { oneNode = i; } } int[] ans = new int[n]; Arrays.fill(ans, 1); if (oneNode == -1) { return ans; } Set<Integer> seenValues = new HashSet<>(); int missingValue = 1; int currentNode = oneNode; int prevNode = -1; while (currentNode != -1) { // Collect values from the current node and its side branches collectSubtree(currentNode, children, nums, seenValues); while (seenValues.contains(missingValue)) { missingValue++; } ans[currentNode] = missingValue; // Move up to the parent currentNode = parents[currentNode]; } return ans; } private void collectSubtree(int u, List<List<Integer>> children, int[] nums, Set<Integer> seenValues) { // Use a queue for iterative DFS to avoid stack overflow on skewed trees Queue<Integer> q = new LinkedList<>(); q.offer(u); seenValues.add(nums[u]); while(!q.isEmpty()){ int node = q.poll(); for(int child : children.get(node)){ if(!seenValues.contains(nums[child])){ seenValues.add(nums[child]); q.offer(child); } } } }}Note: The provided code snippet for collectSubtree uses an iterative BFS-like traversal to gather values. This is to prevent potential StackOverflowError on deep/skewed trees, which a recursive DFS might face. The logic of which nodes to visit is handled in the main loop by only initiating the collection from specific nodes. The logic in the main loop is slightly different from the description to be more robust. The collectSubtree is called on currentNode and it traverses the whole subtree. The seenValues set ensures that already visited nodes (from a child's subtree on the path) are not re-processed, making it efficient.
Complexity
Time
O(N). Building the adjacency list is O(N). Finding `oneNode` is O(N). The main part involves traversing up from `oneNode` and collecting values from side branches. Each node in the tree is part of exactly one such branch (or is on the main path). Therefore, each node's value is added to the `seenValues` set exactly once. The total time for all collections is O(N). The `missingValue` pointer only moves forward, and in total, it will be incremented at most N+1 times. Thus, the overall time complexity is linear.
Space
O(N). We need O(N) space for the adjacency list, the `ans` array, and the `seenValues` hash set, which can store up to N distinct genetic values. The queue for the iterative traversal can also hold up to O(N) nodes in the worst case.
Trade-offs
Pros
- Highly efficient with O(N) time complexity, which passes for large inputs.
- The core idea is clever and reduces the problem scope significantly.
Cons
- The logic is more complex to understand and implement compared to the brute-force approach.
- It requires careful handling of pointers/indices to traverse up the path and to correctly identify side branches.
Solutions
Solution
class Solution {private List<Integer>[] g;private boolean[] vis;private boolean[] has;private int[] nums;public int[] smallestMissingValueSubtree(int[] parents, int[] nums) { int n = nums.length; this.nums = nums; g = new List[n]; vis = new boolean[n]; has = new boolean[n + 2]; Arrays.setAll(g, i->new ArrayList<>()); int idx = -1; for (int i = 0; i < n; ++i) { if (i > 0) { g[parents[i]].add(i); } if (nums[i] == 1) { idx = i; } } int[] ans = new int[n]; Arrays.fill(ans, 1); if (idx == -1) { return ans; } for (int i = 2; idx != -1; idx = parents[idx]) { dfs(idx); while (has[i]) { ++i; } ans[idx] = i; } return ans; }private void dfs(int i) { if (vis[i]) { return; } vis[i] = true; if (nums[i] < has.length) { has[nums[i]] = true; } for (int j : g[i]) { dfs(j); } }}class Solution {public: vector<int> smallestMissingValueSubtree(vector<int> &parents, vector<int> &nums) { int n = nums.size(); vector<int> g[n]; bool vis[n]; bool has[n + 2]; memset(vis, false, sizeof(vis)); memset(has, false, sizeof(has)); int idx = -1; for (int i = 0; i < n; ++i) { if (i) { g[parents[i]].push_back(i); } if (nums[i] == 1) { idx = i; } } vector<int> ans(n, 1); if (idx == -1) { return ans; } function<void(int)> dfs = [&](int i) { if (vis[i]) { return; } vis[i] = true; if (nums[i] < n + 2) { has[nums[i]] = true; } for (int j : g[i]) { dfs(j); } }; for (int i = 2; ~idx; idx = parents[idx]) { dfs(idx); while (has[i]) { ++i; } ans[idx] = i; } return ans; }};class Solution: def smallestMissingValueSubtree(self, parents: List[int], nums: List[int]) -> List[int]: def dfs(i: int): if vis[i]: return vis[i] = True if nums[i] < len(has): has[nums[i]] = True for j in g[i]: dfs(j) n = len(nums) ans = [1] * n g = [[] for _ in range(n)] idx = - 1 for i, p in enumerate(parents): if i: g[p]. append(i) if nums[i] == 1: idx = i if idx == - 1: return ans vis = [False] * n has = [False] * (n + 2) i = 2 while idx != - 1: dfs(idx) while has[i]: i += 1 ans[idx] = i idx = parents[idx] return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.