1273. Delete Tree Nodes 🔒
Problem Description
You are given a tree with nodes
number of nodes, where the root is at node 0. The tree is represented using three pieces of information:
- The total number of nodes is
nodes
- Each node
i
has a value given byvalue[i]
- Each node
i
(except the root) has a parent given byparent[i]
Your task is to remove every subtree whose sum of node values equals zero. After removing all such subtrees, return the count of nodes that remain in the tree.
A subtree includes a node and all of its descendants. When a subtree's total sum equals zero, the entire subtree (including the root of that subtree and all its descendants) should be removed from the tree.
For example, if a node and all its descendants sum to zero, that entire branch is deleted. The removal process should be applied recursively - if removing some subtrees causes a parent subtree to now sum to zero, that parent subtree should also be removed.
The solution uses DFS to traverse the tree from bottom to top, calculating the sum and node count for each subtree. When a subtree's sum equals zero, its node count is set to zero, effectively removing it from the final count.
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 a tree structure where nodes are connected through parent-child relationships. A tree is a special type of graph (specifically, a connected acyclic graph).
Is it a tree?
- Yes: The problem explicitly states we have a tree rooted at node 0, with parent-child relationships defined by the
parent
array.
DFS
- Yes: Since we identified this as a tree problem, the flowchart leads us directly to DFS as the solution approach.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this tree problem.
This makes perfect sense for Delete Tree Nodes because:
- We need to traverse the entire tree to calculate subtree sums
- We need to process children before parents (post-order traversal) to determine which subtrees sum to zero
- DFS naturally handles the recursive nature of checking each subtree and propagating results upward
- The problem requires exploring all paths from leaves to root to correctly identify and remove zero-sum subtrees
The DFS approach allows us to efficiently calculate subtree sums bottom-up and mark subtrees for removal when their sum equals zero.
Intuition
The key insight is that we need to know the sum of each subtree before deciding whether to keep or remove it. This naturally suggests a bottom-up approach where we process children before their parents.
Think about it this way: if we start from the root and go downward, we won't know whether to keep a node until we've explored all its descendants. But if we start from the leaves and work our way up, we can calculate each subtree's sum and immediately know if it should be removed.
When we visit a node during our traversal, we need to track two pieces of information:
- The sum of all values in the subtree rooted at this node
- The count of nodes that remain in this subtree after removal
Here's the clever part: when a subtree sums to zero, we don't actually delete it from the tree structure. Instead, we simply mark it as having 0 remaining nodes. This way, when the parent node asks "how many nodes are in my child's subtree?", it gets 0, effectively ignoring the removed subtree.
The process works like this:
- Start at a leaf node: its sum is just its own value, and it has 1 node
- Move to a parent: accumulate the sums and node counts from all children
- If the total sum equals zero, set the node count to 0 (marking the entire subtree for removal)
- Continue upward until we reach the root
By the time we finish processing the root, we have the total count of nodes that remain in the tree after all zero-sum subtrees have been "removed."
This approach elegantly handles cascading removals - if removing some child subtrees causes a parent subtree to sum to zero, that parent subtree will also be marked with 0 nodes, effectively removing it as well.
Learn more about Tree, Depth-First Search and Breadth-First Search patterns.
Solution Approach
The implementation follows the DFS pattern we identified, with a few key components:
Building the Tree Structure:
First, we convert the parent array into an adjacency list representation using a defaultdict(list)
. For each node i
(starting from 1, since 0 is the root with no parent), we add it to its parent's children list: g[parent[i]].append(i)
. This gives us easy access to all children of any node.
The DFS Function:
We define a recursive function dfs(i)
that processes node i
and returns a tuple (sum, count)
where:
sum
is the total sum of values in the subtree rooted at nodei
count
is the number of nodes remaining in this subtree after removing zero-sum subtrees
Processing Each Node:
For each node i
, we:
- Initialize
s = value[i]
(the sum starts with the node's own value) - Initialize
m = 1
(count starts at 1 for the node itself) - Recursively process all children in
g[i]
:- Call
dfs(j)
for each childj
- Add the child's subtree sum to our running total:
s += t
- Add the child's remaining node count to our count:
m += n
- Call
Handling Zero-Sum Subtrees:
After processing all children, we check if the total sum s
equals 0. If it does, we set m = 0
, effectively marking this entire subtree as removed. This is the crucial step - we don't actually delete nodes, we just report that this subtree has 0 remaining nodes.
Getting the Final Answer:
We call dfs(0)
to process the entire tree starting from the root. The function returns a tuple, and we extract the second element [1]
which gives us the total count of remaining nodes.
The beauty of this approach is its simplicity - by returning both the sum and count in each recursive call, parent nodes can easily aggregate information from their children and make decisions about removal. The post-order nature of the traversal ensures that all descendants are processed before making decisions about ancestors, handling cascading removals naturally.
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 concrete example to see how the solution works.
Consider a tree with 5 nodes:
- Node values:
[1, -1, 3, -3, 2]
- Parent array:
[-1, 0, 0, 2, 2]
This creates the following tree structure:
0(1) / \ 1(-1) 2(3) / \ 3(-3) 4(2)
Step 1: Build the adjacency list From the parent array, we create:
g[0] = [1, 2]
(node 0 has children 1 and 2)g[2] = [3, 4]
(node 2 has children 3 and 4)
Step 2: Start DFS from root (node 0)
The DFS processes nodes in post-order (children before parents):
Processing node 1 (leaf):
- No children to process
- Sum = -1 (its own value)
- Count = 1 (just itself)
- Sum ≠0, so keep the count as 1
- Returns:
(-1, 1)
Processing node 3 (leaf):
- No children to process
- Sum = -3
- Count = 1
- Sum ≠0, so keep the count as 1
- Returns:
(-3, 1)
Processing node 4 (leaf):
- No children to process
- Sum = 2
- Count = 1
- Sum ≠0, so keep the count as 1
- Returns:
(2, 1)
Processing node 2:
- Start with sum = 3 (its value), count = 1 (itself)
- Process child 3: adds (-3, 1) → sum becomes 3 + (-3) = 0, count becomes 1 + 1 = 2
- Process child 4: adds (2, 1) → sum becomes 0 + 2 = 2, count becomes 2 + 1 = 3
- Final sum = 2, count = 3
- Sum ≠0, so keep the count as 3
- Returns:
(2, 3)
Processing node 0 (root):
- Start with sum = 1 (its value), count = 1 (itself)
- Process child 1: adds (-1, 1) → sum becomes 1 + (-1) = 0, count becomes 1 + 1 = 2
- Process child 2: adds (2, 3) → sum becomes 0 + 2 = 2, count becomes 2 + 3 = 5
- Final sum = 2, count = 5
- Sum ≠0, so keep the count as 5
- Returns:
(2, 5)
Result: The function returns 5, meaning all 5 nodes remain in the tree.
Now let's see what happens if we modify node 4's value to be -2 instead of 2:
Modified tree:
0(1) / \ 1(-1) 2(3) / \ 3(-3) 4(-2)
When we process node 2 now:
- Start with sum = 3, count = 1
- Add child 3: sum = 3 + (-3) = 0, count = 1 + 1 = 2
- Add child 4: sum = 0 + (-2) = -2, count = 2 + 1 = 3
- Final sum = -2, count = 3
- Returns:
(-2, 3)
When we process node 0:
- Start with sum = 1, count = 1
- Add child 1: sum = 1 + (-1) = 0, count = 1 + 1 = 2
- Add child 2: sum = 0 + (-2) = -2, count = 2 + 3 = 5
- Final sum = -2, count = 5
- Returns:
(-2, 5)
But if node 2's subtree had summed to 0:
- When processing node 2, if the sum equaled 0, we'd set count = 0
- This would return
(0, 0)
to node 0 - Node 0 would then only count itself and node 1, giving a final count of 2
This demonstrates how the algorithm elegantly handles subtree removal by simply setting the count to 0 when a subtree sums to zero, allowing parent nodes to naturally exclude removed subtrees from their counts.
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def deleteTreeNodes(self, nodes: int, parent: List[int], value: List[int]) -> int:
6 """
7 Delete nodes from a tree whose subtree sum equals zero.
8
9 Args:
10 nodes: Total number of nodes in the tree
11 parent: Parent array where parent[i] is the parent of node i
12 value: Value array where value[i] is the value of node i
13
14 Returns:
15 Number of remaining nodes after deletion
16 """
17
18 def dfs(node_idx: int) -> tuple[int, int]:
19 """
20 Perform DFS to calculate subtree sum and count of nodes.
21
22 Args:
23 node_idx: Current node index
24
25 Returns:
26 Tuple of (subtree_sum, node_count)
27 """
28 # Initialize with current node's value and count of 1
29 subtree_sum = value[node_idx]
30 node_count = 1
31
32 # Process all children of the current node
33 for child_idx in adjacency_list[node_idx]:
34 # Recursively get child's subtree sum and count
35 child_sum, child_count = dfs(child_idx)
36
37 # Add child's contribution to current subtree
38 subtree_sum += child_sum
39 node_count += child_count
40
41 # If subtree sum is 0, mark entire subtree for deletion
42 if subtree_sum == 0:
43 node_count = 0
44
45 return (subtree_sum, node_count)
46
47 # Build adjacency list representation of the tree
48 adjacency_list = defaultdict(list)
49 for i in range(1, nodes):
50 adjacency_list[parent[i]].append(i)
51
52 # Start DFS from root (node 0) and return the node count
53 return dfs(0)[1]
54
1class Solution {
2 // Adjacency list to represent the tree structure
3 private List<Integer>[] adjacencyList;
4 // Array to store node values
5 private int[] nodeValues;
6
7 public int deleteTreeNodes(int nodes, int[] parent, int[] value) {
8 // Initialize the adjacency list for the tree
9 adjacencyList = new List[nodes];
10 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
11
12 // Build the tree structure by adding children to their parents
13 // Start from node 1 since node 0 is the root (has no parent)
14 for (int i = 1; i < nodes; ++i) {
15 adjacencyList[parent[i]].add(i);
16 }
17
18 // Store the node values for use in DFS
19 this.nodeValues = value;
20
21 // Perform DFS from root (node 0) and return the count of remaining nodes
22 // dfs returns [sum, count], we need the count (index 1)
23 return dfs(0)[1];
24 }
25
26 /**
27 * Performs depth-first search to calculate subtree sum and node count
28 * @param currentNode The current node being processed
29 * @return An array where [0] = subtree sum, [1] = number of nodes in subtree
30 */
31 private int[] dfs(int currentNode) {
32 // Initialize result with current node's value and count of 1
33 int[] result = new int[] {nodeValues[currentNode], 1};
34
35 // Process all children of the current node
36 for (int childNode : adjacencyList[currentNode]) {
37 // Recursively get the sum and count for child's subtree
38 int[] childResult = dfs(childNode);
39
40 // Add child's subtree sum to current subtree sum
41 result[0] += childResult[0];
42 // Add child's node count to current subtree count
43 result[1] += childResult[1];
44 }
45
46 // If the subtree sum is 0, delete the entire subtree
47 // by setting its node count to 0
48 if (result[0] == 0) {
49 result[1] = 0;
50 }
51
52 return result;
53 }
54}
55
1class Solution {
2public:
3 int deleteTreeNodes(int nodes, vector<int>& parent, vector<int>& value) {
4 // Build adjacency list representation of the tree
5 // graph[i] contains all children of node i
6 vector<vector<int>> graph(nodes);
7 for (int i = 1; i < nodes; ++i) {
8 graph[parent[i]].emplace_back(i);
9 }
10
11 // DFS function to calculate subtree sum and count of remaining nodes
12 // Returns: {subtree_sum, remaining_node_count}
13 function<pair<int, int>(int)> dfs = [&](int currentNode) -> pair<int, int> {
14 // Initialize sum with current node's value and count as 1
15 int subtreeSum = value[currentNode];
16 int nodeCount = 1;
17
18 // Process all children of current node
19 for (int childNode : graph[currentNode]) {
20 // Recursively get sum and count from child subtree
21 auto [childSum, childCount] = dfs(childNode);
22
23 // Add child's contribution to current subtree
24 subtreeSum += childSum;
25 nodeCount += childCount;
26 }
27
28 // If subtree sum is 0, delete entire subtree (set count to 0)
29 if (subtreeSum == 0) {
30 nodeCount = 0;
31 }
32
33 return {subtreeSum, nodeCount};
34 };
35
36 // Start DFS from root (node 0) and return the count of remaining nodes
37 return dfs(0).second;
38 }
39};
40
1function deleteTreeNodes(nodes: number, parent: number[], value: number[]): number {
2 // Build adjacency list representation of the tree
3 // graph[i] contains all children of node i
4 const graph: number[][] = Array.from({ length: nodes }, () => []);
5 for (let i = 1; i < nodes; i++) {
6 graph[parent[i]].push(i);
7 }
8
9 // DFS function to calculate subtree sum and count of remaining nodes
10 // Returns: [subtreeSum, remainingNodeCount]
11 const dfs = (currentNode: number): [number, number] => {
12 // Initialize sum with current node's value and count as 1
13 let subtreeSum = value[currentNode];
14 let nodeCount = 1;
15
16 // Process all children of current node
17 for (const childNode of graph[currentNode]) {
18 // Recursively get sum and count from child subtree
19 const [childSum, childCount] = dfs(childNode);
20
21 // Add child's contribution to current subtree
22 subtreeSum += childSum;
23 nodeCount += childCount;
24 }
25
26 // If subtree sum is 0, delete entire subtree (set count to 0)
27 if (subtreeSum === 0) {
28 nodeCount = 0;
29 }
30
31 return [subtreeSum, nodeCount];
32 };
33
34 // Start DFS from root (node 0) and return the count of remaining nodes
35 return dfs(0)[1];
36}
37
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the tree. Each node is visited exactly once during the DFS traversal. For each node, the operations performed are:
- Accessing the node's value:
O(1)
- Iterating through its children:
O(number of children)
- Performing constant-time arithmetic operations:
O(1)
Since the total number of edges in a tree is n-1
(where n
is the number of nodes), and each edge is traversed once when iterating through children across all nodes, the total time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of:
- The adjacency list
g
(defaultdict): This stores all parent-child relationships. In the worst case, it storesn-1
edges, requiringO(n)
space. - The recursion call stack: In the worst case (a skewed tree), the maximum depth of recursion can be
n
, requiringO(n)
space for the call stack. - Local variables in each recursive call (
s
,m
,t
,n
): These are constant space per call, contributingO(n)
in total across all recursive calls.
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Order of Processing - Not Using Post-Order Traversal
A common mistake is trying to process nodes in the wrong order, such as using pre-order traversal or attempting to delete nodes while traversing. This can lead to incorrect results because parent nodes need information from all their descendants before deciding whether to delete themselves.
Wrong Approach:
def dfs(node_idx):
# WRONG: Checking deletion before processing children
if value[node_idx] == 0:
return (0, 0)
subtree_sum = value[node_idx]
node_count = 1
for child_idx in adjacency_list[node_idx]:
child_sum, child_count = dfs(child_idx)
subtree_sum += child_sum
node_count += child_count
return (subtree_sum, node_count)
Why it fails: This approach only checks if the current node's value is 0, not the entire subtree sum. It would miss cases where a subtree sums to 0 due to positive and negative values canceling out.
2. Not Propagating Zero-Count Correctly
Another pitfall is forgetting that when a child subtree is deleted (count = 0), its sum should still be included in the parent's calculation, but its count should not affect whether the parent gets deleted.
Wrong Approach:
def dfs(node_idx):
subtree_sum = value[node_idx]
node_count = 1
for child_idx in adjacency_list[node_idx]:
child_sum, child_count = dfs(child_idx)
# WRONG: Not adding child_sum if child was deleted
if child_count > 0:
subtree_sum += child_sum
node_count += child_count
if subtree_sum == 0:
node_count = 0
return (subtree_sum, node_count)
Why it fails: Even if a child subtree is deleted (count = 0), its sum value should still contribute to the parent's sum calculation. The parent needs the complete sum of all its descendants to determine if it should also be deleted.
3. Modifying Tree Structure During Traversal
Attempting to actually remove nodes from the adjacency list during traversal can cause issues:
Wrong Approach:
def dfs(node_idx):
subtree_sum = value[node_idx]
node_count = 1
children_to_remove = []
for child_idx in adjacency_list[node_idx]:
child_sum, child_count = dfs(child_idx)
if child_count == 0:
# WRONG: Trying to modify structure during traversal
children_to_remove.append(child_idx)
subtree_sum += child_sum
node_count += child_count
# WRONG: Modifying the adjacency list
for child in children_to_remove:
adjacency_list[node_idx].remove(child)
if subtree_sum == 0:
node_count = 0
return (subtree_sum, node_count)
Why it fails: There's no need to actually modify the tree structure. The algorithm works by tracking counts, not by physically removing nodes. Modifying the structure adds unnecessary complexity and doesn't improve the solution.
4. Edge Case: Single Node with Zero Value
Not handling the case where the root itself has value 0 and no children:
Correct Solution Handles This: The provided solution correctly handles this case because it checks the sum after processing all children (even if there are none), so a root with value 0 will correctly return count = 0.
Which data structure is used in a depth first search?
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!