Facebook Pixel

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 and 5 share parent A
  • Nodes 6 and 7 share parent B

Then:

  • Node 4 has cousins 6 and 7, so it gets replaced with 6 + 7 = 13
  • Node 5 has cousins 6 and 7, so it gets replaced with 6 + 7 = 13
  • Node 6 has cousins 4 and 5, so it gets replaced with 4 + 5 = 9
  • Node 7 has cousins 4 and 5, so it gets replaced with 4 + 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:

  1. 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.

  2. 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.

  3. 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)
  4. 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.

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

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:

  1. 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)
  2. 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)
  3. 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 where s[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.
  4. 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
  5. 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 where s[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:

  1. Calculate sub = sum of current node's children (siblings to each other)
  2. 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

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 Evaluator

Example 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 in O(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 in O(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), requiring O(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 be n depth levels, requiring O(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More