2641. Cousins in Binary Tree II
Problem Description
You are given the root of a binary tree. Your task is to replace the value of each node with the sum of all its cousin nodes' values.
What are cousin nodes?
- Two nodes are cousins if they are at the same depth (level) in the tree but have different parents
- The depth of a node is the number of edges from the root to that node
Key Points:
- Sibling nodes (nodes with the same parent) are NOT cousins to each other
- Nodes at the same level but with different parents ARE cousins
- After replacement, each node's value should equal the sum of all its cousins' values
Example Understanding:
If a tree has nodes at depth 2 with values [4, 5, 6, 7]
where:
- Nodes
4
and5
share parent A - Nodes
6
and7
share parent B
Then:
- Node
4
has cousins6
and7
, so it gets replaced with6 + 7 = 13
- Node
5
has cousins6
and7
, so it gets replaced with6 + 7 = 13
- Node
6
has cousins4
and5
, so it gets replaced with4 + 5 = 9
- Node
7
has cousins4
and5
, so it gets replaced with4 + 5 = 9
The function should return the root of the modified tree where all nodes have been updated with their cousin sum values.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph (specifically, a connected acyclic graph where each node has at most two children).
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure where we need to traverse nodes and process them based on their relationships (parent-child and cousin relationships).
DFS
- Yes: We arrive at DFS as the recommended approach.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this problem.
Why DFS is Appropriate
The DFS pattern fits perfectly because:
-
Level-wise Information Gathering: We need to traverse the entire tree to collect sum information at each depth level. DFS allows us to systematically visit all nodes while tracking their depth.
-
Parent-Child Relationship Tracking: To identify cousins (same depth, different parents), we need to traverse the tree while maintaining parent context. DFS naturally maintains this context through its recursive call stack.
-
Two-Pass Strategy:
- First DFS pass: Calculate the sum of all nodes at each depth level
- Second DFS pass: Update each node's value based on cousin relationships (total sum at that level minus sibling values)
-
Tree Structure Navigation: DFS is ideal for binary tree problems where we need to process nodes based on their structural relationships rather than just their values.
Intuition
The key insight is recognizing that cousins are all nodes at the same level except for siblings. This means for any node, its cousin sum equals the total sum of all nodes at that level minus the sum of itself and its sibling.
Let's think through this step by step:
-
What information do we need?
- To calculate cousin sums, we need to know the total sum at each depth level
- We also need to know which nodes are siblings (share the same parent)
-
Breaking down the cousin relationship:
- If nodes A and B are at depth
d
and have the same parent, they are siblings - All other nodes at depth
d
are their cousins - So:
cousin_sum = total_sum_at_depth_d - (A.val + B.val)
- If nodes A and B are at depth
-
Why two passes?
- First pass: We can't calculate cousin sums without knowing the total at each level first. So we traverse the tree once to build an array
s
wheres[depth]
stores the sum of all nodes at that depth. - Second pass: Now that we know level sums, we can update each node. But here's the clever part - instead of updating a node with its own cousin sum, we update its children with their cousin sums. Why? Because when we're at a parent node, we can easily calculate the sum of its children (the siblings), and therefore determine what their cousin sum should be.
- First pass: We can't calculate cousin sums without knowing the total at each level first. So we traverse the tree once to build an array
-
The parent's perspective trick:
- When we're at a parent node during the second DFS, we know:
- The total sum at the children's level (from our first pass)
- The values of both children (direct access)
- This allows us to calculate:
cousin_sum = level_sum - sibling_sum
- We then assign this value to each child
- When we're at a parent node during the second DFS, we know:
-
Edge cases handled naturally:
- The root has no cousins (it's alone at depth 0), so we set it to 0
- If a node has only one child, we still calculate correctly since a missing child contributes 0 to the sibling sum
- Leaf nodes have no children to update, so the recursion naturally terminates
This approach elegantly solves the problem by recognizing that the cousin relationship is best computed from the parent's perspective, where we have easy access to sibling information.
Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.
Solution Approach
The solution implements a two-pass DFS strategy as outlined in the reference approach:
First Pass - Building Level Sums
The first DFS traversal (dfs1
) collects the sum of node values at each depth:
def dfs1(root: Optional[TreeNode], depth: int):
if root is None:
return
if len(s) <= depth:
s.append(0)
s[depth] += root.val
dfs1(root.left, depth + 1)
dfs1(root.right, depth + 1)
Key components:
- Data Structure: A list
s
wheres[depth]
stores the sum of all node values at that depth - Dynamic list growth: If we encounter a new depth level, we extend the list
- Recursive traversal: Visit each node once, adding its value to the appropriate depth sum
- Time complexity: O(n) where n is the number of nodes
Second Pass - Updating Node Values
The second DFS traversal (dfs2
) updates each node's children with their cousin sums:
def dfs2(root: Optional[TreeNode], depth: int):
sub = (root.left.val if root.left else 0) + (
root.right.val if root.right else 0
)
depth += 1
if root.left:
root.left.val = s[depth] - sub
dfs2(root.left, depth)
if root.right:
root.right.val = s[depth] - sub
dfs2(root.right, depth)
The algorithm:
- Calculate
sub
= sum of current node's children (siblings to each other) - For each child that exists:
- Set its value to
s[depth] - sub
(total at that level minus sibling sum) - Recursively process that child's subtree
- Set its value to
Why this works:
- When at a parent node, we know both children (if they exist)
- These children are siblings to each other
- Their cousin sum = (total sum at their level) - (sum of siblings)
- Since we're modifying values as we go, we must calculate the sibling sum before updating
Main Function Flow
s = [] dfs1(root, 0) # Build level sums root.val = 0 # Root has no cousins dfs2(root, 0) # Update all other nodes return root
Special handling:
- The root node is set to 0 since it has no cousins (it's the only node at depth 0)
- The
dfs2
function starts from the root but only updates its children and below
Complexity Analysis:
- Time Complexity: O(n) - we visit each node exactly twice
- Space Complexity: O(h) where h is the height of the tree (for recursion stack) plus O(d) for the level sum array where d is the depth of the tree
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider this binary tree:
5 / \ 4 9 / \ 1 10
Step 1: First DFS Pass - Calculate Level Sums
Starting from the root, we traverse the tree and build our level sums array s
:
- Depth 0: Visit node 5 →
s[0] = 5
- Depth 1: Visit nodes 4 and 9 →
s[1] = 4 + 9 = 13
- Depth 2: Visit nodes 1 and 10 →
s[2] = 1 + 10 = 11
After first pass: s = [5, 13, 11]
Step 2: Update Root
The root has no cousins (it's alone at depth 0), so we set root.val = 0
.
Tree now looks like:
0 / \ 4 9 / \ 1 10
Step 3: Second DFS Pass - Update with Cousin Sums
Starting from the root, we update each node's children:
At root (depth 0):
- Calculate sibling sum:
sub = 4 + 9 = 13
- Update left child (4):
s[1] - sub = 13 - 13 = 0
- Update right child (9):
s[1] - sub = 13 - 13 = 0
Tree becomes:
0 / \ 0 0 / \ 1 10
At node 4 (now 0) at depth 1:
- Calculate sibling sum:
sub = 1 + 10 = 11
- Update left child (1):
s[2] - sub = 11 - 11 = 0
- Update right child (10):
s[2] - sub = 11 - 11 = 0
At node 9 (now 0) at depth 1:
- No children, so nothing to update
Final tree:
0 / \ 0 0 / \ 0 0
Verification:
- Node at depth 0: Has no cousins → value = 0 ✓
- Nodes at depth 1 (originally 4 and 9): They are siblings, not cousins to each other → cousin sum = 0 ✓
- Nodes at depth 2 (originally 1 and 10): They are siblings, not cousins to each other → cousin sum = 0 ✓
This example shows how the algorithm correctly identifies that sibling nodes have no cousins at their level when they are the only nodes at that depth with different parents.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8class Solution:
9 def replaceValueInTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
10 """
11 Replace each node's value with the sum of its cousin nodes' values.
12 Cousins are nodes at the same depth but with different parents.
13 """
14
15 def calculate_level_sums(node: Optional[TreeNode], depth: int) -> None:
16 """
17 First DFS traversal to calculate the sum of values at each depth level.
18
19 Args:
20 node: Current node in the tree
21 depth: Current depth level (0-indexed)
22 """
23 if node is None:
24 return
25
26 # Extend the level_sums list if we encounter a new depth
27 if len(level_sums) <= depth:
28 level_sums.append(0)
29
30 # Add current node's value to the sum for this depth
31 level_sums[depth] += node.val
32
33 # Recursively process left and right subtrees
34 calculate_level_sums(node.left, depth + 1)
35 calculate_level_sums(node.right, depth + 1)
36
37 def update_node_values(parent: Optional[TreeNode], depth: int) -> None:
38 """
39 Second DFS traversal to update each node's value with cousin sum.
40
41 Args:
42 parent: Parent node whose children need to be updated
43 depth: Current depth level of the parent node
44 """
45 # Calculate the sum of sibling values (children of current parent)
46 sibling_sum = (parent.left.val if parent.left else 0) + \
47 (parent.right.val if parent.right else 0)
48
49 # Move to children's depth level
50 child_depth = depth + 1
51
52 # Update left child's value with cousin sum
53 # (total sum at depth minus sibling sum)
54 if parent.left:
55 parent.left.val = level_sums[child_depth] - sibling_sum
56 update_node_values(parent.left, child_depth)
57
58 # Update right child's value with cousin sum
59 if parent.right:
60 parent.right.val = level_sums[child_depth] - sibling_sum
61 update_node_values(parent.right, child_depth)
62
63 # Initialize list to store sum of values at each depth level
64 level_sums = []
65
66 # First pass: Calculate sum of values at each depth
67 calculate_level_sums(root, 0)
68
69 # Root has no cousins, so its value becomes 0
70 root.val = 0
71
72 # Second pass: Update each node's value with cousin sum
73 update_node_values(root, 0)
74
75 return root
76
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 // List to store the sum of node values at each depth level
18 private List<Integer> depthSums = new ArrayList<>();
19
20 /**
21 * Replaces each node's value with the sum of its cousins' values.
22 * Cousins are nodes at the same depth but with different parents.
23 *
24 * @param root The root of the binary tree
25 * @return The modified tree with replaced values
26 */
27 public TreeNode replaceValueInTree(TreeNode root) {
28 // First pass: Calculate sum of all nodes at each depth
29 calculateDepthSums(root, 0);
30
31 // Root has no cousins, so its value becomes 0
32 root.val = 0;
33
34 // Second pass: Replace each node's value with sum of cousins
35 replaceWithCousinSum(root, 0);
36
37 return root;
38 }
39
40 /**
41 * First DFS traversal to calculate the sum of all node values at each depth.
42 *
43 * @param node The current node being processed
44 * @param depth The current depth in the tree
45 */
46 private void calculateDepthSums(TreeNode node, int depth) {
47 if (node == null) {
48 return;
49 }
50
51 // Expand the list if we've reached a new depth level
52 if (depthSums.size() <= depth) {
53 depthSums.add(0);
54 }
55
56 // Add current node's value to the sum at this depth
57 depthSums.set(depth, depthSums.get(depth) + node.val);
58
59 // Recursively process left and right subtrees
60 calculateDepthSums(node.left, depth + 1);
61 calculateDepthSums(node.right, depth + 1);
62 }
63
64 /**
65 * Second DFS traversal to replace each node's value with the sum of its cousins.
66 * The sum of cousins = total sum at depth - sum of siblings.
67 *
68 * @param parent The parent node whose children will be updated
69 * @param depth The current depth of the parent node
70 */
71 private void replaceWithCousinSum(TreeNode parent, int depth) {
72 // Calculate the sum of the parent's children (siblings to each other)
73 int leftValue = (parent.left == null) ? 0 : parent.left.val;
74 int rightValue = (parent.right == null) ? 0 : parent.right.val;
75 int siblingSum = leftValue + rightValue;
76
77 // Move to the next depth level (children's level)
78 depth++;
79
80 // Update left child's value with cousin sum
81 if (parent.left != null) {
82 parent.left.val = depthSums.get(depth) - siblingSum;
83 replaceWithCousinSum(parent.left, depth);
84 }
85
86 // Update right child's value with cousin sum
87 if (parent.right != null) {
88 parent.right.val = depthSums.get(depth) - siblingSum;
89 replaceWithCousinSum(parent.right, depth);
90 }
91 }
92}
93
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 TreeNode* replaceValueInTree(TreeNode* root) {
15 // Initialize level sums array to zero
16 memset(levelSums, 0, sizeof(levelSums));
17
18 // First pass: Calculate sum of all nodes at each depth level
19 calculateLevelSums(root, 0);
20
21 // Root has no cousins, so its value becomes 0
22 root->val = 0;
23
24 // Second pass: Replace each node's value with sum of its cousins
25 replaceWithCousinSum(root, 0);
26
27 return root;
28 }
29
30private:
31 // Array to store sum of node values at each depth level
32 // Maximum depth assumed to be less than 100010
33 int levelSums[100010];
34
35 /**
36 * First DFS traversal to calculate sum of all nodes at each depth level
37 * @param node Current node being processed
38 * @param depth Current depth in the tree (0-indexed from root)
39 */
40 void calculateLevelSums(TreeNode* node, int depth) {
41 // Base case: null node
42 if (!node) {
43 return;
44 }
45
46 // Add current node's value to its depth level sum
47 levelSums[depth] += node->val;
48
49 // Recursively process left and right subtrees
50 calculateLevelSums(node->left, depth + 1);
51 calculateLevelSums(node->right, depth + 1);
52 }
53
54 /**
55 * Second DFS traversal to replace each node's value with cousin sum
56 * Cousin sum = total sum at that depth - sum of siblings (including self)
57 * @param node Current node being processed
58 * @param depth Current depth in the tree
59 */
60 void replaceWithCousinSum(TreeNode* node, int depth) {
61 // Calculate sum of current node's children (siblings to each other)
62 int leftChildValue = node->left ? node->left->val : 0;
63 int rightChildValue = node->right ? node->right->val : 0;
64 int siblingSum = leftChildValue + rightChildValue;
65
66 // Move to next depth level for children
67 ++depth;
68
69 // Update left child's value to cousin sum
70 if (node->left) {
71 // Cousin sum = level sum - sibling sum
72 node->left->val = levelSums[depth] - siblingSum;
73 // Recursively process left subtree
74 replaceWithCousinSum(node->left, depth);
75 }
76
77 // Update right child's value to cousin sum
78 if (node->right) {
79 // Cousin sum = level sum - sibling sum
80 node->right->val = levelSums[depth] - siblingSum;
81 // Recursively process right subtree
82 replaceWithCousinSum(node->right, depth);
83 }
84 }
85};
86
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Replaces each node's value in the tree with the sum of all its cousins' values.
17 * Cousins are nodes at the same depth but with different parents.
18 * @param root - The root of the binary tree
19 * @returns The modified tree with replaced values
20 */
21function replaceValueInTree(root: TreeNode | null): TreeNode | null {
22 // Array to store the sum of node values at each depth level
23 const depthSums: number[] = [];
24
25 /**
26 * First DFS: Calculate the sum of all node values at each depth
27 * @param node - Current node being processed
28 * @param depth - Current depth in the tree
29 */
30 const calculateDepthSums = (node: TreeNode | null, depth: number): void => {
31 if (!node) {
32 return;
33 }
34
35 // Initialize sum for this depth if not exists
36 if (depthSums.length <= depth) {
37 depthSums.push(0);
38 }
39
40 // Add current node's value to the sum at this depth
41 depthSums[depth] += node.val;
42
43 // Recursively process left and right subtrees
44 calculateDepthSums(node.left, depth + 1);
45 calculateDepthSums(node.right, depth + 1);
46 };
47
48 /**
49 * Second DFS: Replace each node's value with the sum of its cousins
50 * @param node - Current node whose children need to be updated
51 * @param depth - Current depth in the tree
52 */
53 const replaceWithCousinSum = (node: TreeNode | null, depth: number): void => {
54 // Calculate the sum of current node's children (siblings to each other)
55 const siblingsSum = (node.left?.val || 0) + (node.right?.val || 0);
56
57 // Move to the next depth level
58 depth++;
59
60 // Update left child's value with cousins' sum (total at depth minus siblings)
61 if (node.left) {
62 node.left.val = depthSums[depth] - siblingsSum;
63 replaceWithCousinSum(node.left, depth);
64 }
65
66 // Update right child's value with cousins' sum (total at depth minus siblings)
67 if (node.right) {
68 node.right.val = depthSums[depth] - siblingsSum;
69 replaceWithCousinSum(node.right, depth);
70 }
71 };
72
73 // First pass: Calculate sum at each depth
74 calculateDepthSums(root, 0);
75
76 // Root has no cousins, so set its value to 0
77 if (root) {
78 root.val = 0;
79 }
80
81 // Second pass: Replace values with cousin sums
82 if (root) {
83 replaceWithCousinSum(root, 0);
84 }
85
86 return root;
87}
88
Time and Space Complexity
Time Complexity: O(n)
The algorithm consists of two depth-first search (DFS) traversals:
dfs1
: Traverses the entire tree once to calculate the sum of node values at each depth level. Each node is visited exactly once, resulting inO(n)
time.dfs2
: Traverses the entire tree once more to replace each node's value with the sum of its cousins (nodes at the same depth but with different parents). Each node is visited exactly once, resulting inO(n)
time.
Since we perform two sequential traversals of the tree, the total time complexity is O(n) + O(n) = O(n)
.
Space Complexity: O(n)
The space complexity comes from multiple sources:
- Recursion call stack: In the worst case (a skewed tree), the recursion depth can be
O(n)
, requiringO(n)
space for the call stack. - Array
s
: This array stores the sum of values at each depth level. In the worst case (a skewed tree), there can ben
depth levels, requiringO(n)
space. - Input tree structure: The tree itself occupies
O(n)
space, but since we're modifying the existing tree in-place rather than creating a new one, this doesn't add to the auxiliary space complexity.
The dominant factor is the maximum between the recursion stack and the array storage, both of which are O(n)
in the worst case. Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Modifying Values Before Reading Siblings
The Pitfall:
A common mistake is updating node values during traversal without first saving the original sibling values. Since we need to calculate sibling_sum
to determine cousin sums, if we modify a node's value before reading its sibling, the calculation becomes incorrect.
Incorrect Approach:
def update_node_values(parent: Optional[TreeNode], depth: int) -> None:
child_depth = depth + 1
# WRONG: Updating left child before reading right child's value
if parent.left:
parent.left.val = level_sums[child_depth] - parent.left.val
update_node_values(parent.left, child_depth)
# Now the sibling sum calculation would be wrong!
if parent.right:
# This uses the already modified left.val, not the original
sibling_sum = parent.left.val + parent.right.val
parent.right.val = level_sums[child_depth] - sibling_sum
The Solution: Always calculate and store the sibling sum BEFORE modifying any values:
# Calculate sibling sum first with original values sibling_sum = (parent.left.val if parent.left else 0) + \ (parent.right.val if parent.right else 0) # Then update both children using the same sibling_sum if parent.left: parent.left.val = level_sums[child_depth] - sibling_sum if parent.right: parent.right.val = level_sums[child_depth] - sibling_sum
2. Attempting Bottom-Up Value Updates
The Pitfall: Trying to update a node's value when visiting that node directly, rather than updating it through its parent. This approach fails because you can't determine siblings without access to the parent.
Incorrect Approach:
def update_values(node: Optional[TreeNode], depth: int, parent: Optional[TreeNode]):
if node is None:
return
# WRONG: How do we find siblings without parent reference?
# We'd need to pass extra information about siblings
node.val = level_sums[depth] - get_sibling_sum(node, parent) # Complex!
update_values(node.left, depth + 1, node)
update_values(node.right, depth + 1, node)
The Solution: Update children from their parent node, where both siblings are directly accessible:
def update_node_values(parent: Optional[TreeNode], depth: int):
# Parent knows both children - can easily calculate sibling sum
sibling_sum = (parent.left.val if parent.left else 0) + \
(parent.right.val if parent.right else 0)
# Update children from parent's perspective
if parent.left:
parent.left.val = level_sums[depth + 1] - sibling_sum
if parent.right:
parent.right.val = level_sums[depth + 1] - sibling_sum
3. Forgetting Edge Cases
The Pitfall: Not handling special cases properly:
- Root node (has no cousins)
- Single child nodes (only one sibling exists)
- Leaf nodes (have no children to update)
Incorrect Handling:
# WRONG: Forgetting root node special case
def replaceValueInTree(root):
calculate_level_sums(root, 0)
update_node_values(root, 0) # Root value never gets set to 0!
return root
# WRONG: Not handling single child case
sibling_sum = parent.left.val + parent.right.val # NullPointerError if one is None!
The Solution: Handle all edge cases explicitly:
# Root has no cousins root.val = 0 # Safe sibling sum calculation sibling_sum = (parent.left.val if parent.left else 0) + \ (parent.right.val if parent.right else 0) # Leaf nodes naturally handled - the function just returns without updating anything
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
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!