Facebook Pixel

2673. Make Costs of Paths Equal in a Binary Tree

Problem Description

You have a perfect binary tree with n nodes, numbered from 1 to n. The tree structure follows these rules:

  • Node 1 is the root
  • For any node i, its left child is node 2 * i and its right child is node 2 * i + 1

Each node has a cost value given in a 0-indexed array cost, where cost[i] represents the cost of node i + 1.

Your task is to make all paths from the root to every leaf node have equal total costs. You can achieve this by incrementing the cost of any node by 1, as many times as needed.

Key Definitions:

  • A perfect binary tree is a tree where every non-leaf node has exactly 2 children
  • The cost of a path is the sum of all node costs along that path
  • A leaf node is a node with no children

Goal: Find the minimum number of increments needed to equalize all root-to-leaf path costs.

Example Understanding: If you have a tree with nodes and their costs, you need to strategically increment certain node values so that when you sum up the costs from root to any leaf, you get the same total. The challenge is to do this with the minimum number of increment operations.

The solution works from bottom to top, ensuring that at each level, the subtrees have equal maximum path values. For each parent node with two children, it calculates the difference in path costs and increments the smaller one to match the larger, accumulating these increments as the answer.

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

Intuition

Think about this problem from a different angle: instead of looking at the entire tree at once, let's focus on smaller subtrees first.

Consider any parent node with its two children. For the paths passing through this parent to have equal costs when they reach the leaves, the maximum path cost from the left child to its leaves must equal the maximum path cost from the right child to its leaves. Why? Because the parent node adds the same value to both paths, so if the subtree paths are unequal, the total paths will also be unequal.

This observation leads to a key insight: we need to work from the bottom up. At each level, we ensure that both children of a parent node have equal maximum path costs to their respective leaves.

Here's the clever part: when we find that the left and right subtrees have different maximum path costs, we don't need to increment nodes deep in the tree. Instead, we can simply increment the root of the subtree that has the smaller maximum path cost. The number of increments needed is exactly the difference between the two maximum path costs.

For example, if the left subtree has a maximum path cost of 10 and the right subtree has 7, we need 3 increments to the right child to balance them. After this operation, both subtrees contribute 10 to their parent.

By processing nodes from the leaves upward (starting from the last non-leaf nodes), we can:

  1. Calculate the cost difference between left and right children
  2. Add this difference to our answer
  3. Update the parent's value to include the maximum of its children's path costs

This greedy approach works because balancing at each level ensures all paths are eventually equal, and doing it bottom-up ensures we make the minimum number of increments since we're always choosing to increment at the highest possible level in each subtree.

Learn more about Greedy, Tree, Dynamic Programming and Binary Tree patterns.

Solution Approach

The solution implements a bottom-up greedy algorithm that processes nodes level by level from the leaves toward the root.

Algorithm Steps:

  1. Start from the last non-leaf nodes: We iterate from node n >> 1 (which is n // 2) down to node 1. In a perfect binary tree with n nodes, nodes from 1 to n // 2 are non-leaf nodes, while nodes from n // 2 + 1 to n are leaf nodes.

  2. For each non-leaf node i:

    • Calculate its children indices:
      • Left child: l = i << 1 (which is i * 2)
      • Right child: r = i << 1 | 1 (which is i * 2 + 1)
  3. Balance the children's path costs:

    • Find the cost difference: abs(cost[l - 1] - cost[r - 1])
    • This difference represents how many increments we need to make the smaller child's path equal to the larger one
    • Add this difference to our answer: ans += abs(cost[l - 1] - cost[r - 1])
  4. Update the parent's effective cost:

    • The parent node now needs to account for the maximum path cost through its children
    • Update: cost[i - 1] += max(cost[l - 1], cost[r - 1])
    • This ensures that when this node becomes a child in the next iteration, it carries the correct accumulated path cost

Key Implementation Details:

  • Array indexing: Since the tree nodes are numbered from 1 to n, but the cost array is 0-indexed, we use cost[i - 1] to access the cost of node i.

  • Bit operations:

    • n >> 1 efficiently computes n // 2
    • i << 1 efficiently computes i * 2
    • i << 1 | 1 efficiently computes i * 2 + 1
  • In-place modification: The algorithm modifies the cost array in place to store accumulated path costs, avoiding extra space usage.

Why this works:

By processing from bottom to top, we ensure that when we reach any parent node, its children already represent the maximum path costs in their respective subtrees. The greedy choice of incrementing to match the larger child's cost at each step guarantees the minimum total increments needed, as any alternative would require more increments at lower levels of the tree.

The time complexity is O(n) as we visit each non-leaf node once, and the space complexity is O(1) as we only use a constant amount of extra space.

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 with a perfect binary tree of 7 nodes.

Initial Setup:

  • Tree structure (nodes 1-7):
        1
       / \
      2   3
     / \ / \
    4  5 6  7
  • Initial costs: cost = [1, 5, 2, 2, 3, 3, 1]
    • Node 1 has cost 1
    • Node 2 has cost 5
    • Node 3 has cost 2
    • Node 4 has cost 2
    • Node 5 has cost 3
    • Node 6 has cost 3
    • Node 7 has cost 1

Step 1: Process node 3 (rightmost non-leaf)

  • Children: node 6 (cost=3) and node 7 (cost=1)
  • Difference: |3 - 1| = 2
  • Add 2 to answer: ans = 2
  • Update node 3's cost: 2 + max(3, 1) = 2 + 3 = 5
  • Updated costs: [1, 5, 5, 2, 3, 3, 1]

Step 2: Process node 2

  • Children: node 4 (cost=2) and node 5 (cost=3)
  • Difference: |2 - 3| = 1
  • Add 1 to answer: ans = 3
  • Update node 2's cost: 5 + max(2, 3) = 5 + 3 = 8
  • Updated costs: [1, 8, 5, 2, 3, 3, 1]

Step 3: Process node 1 (root)

  • Children: node 2 (cost=8) and node 3 (cost=5)
  • Difference: |8 - 5| = 3
  • Add 3 to answer: ans = 6
  • Update node 1's cost: 1 + max(8, 5) = 1 + 8 = 9
  • Updated costs: [9, 8, 5, 2, 3, 3, 1]

Result: Minimum increments needed = 6

Verification of equal paths: After making the increments:

  • Path 1→2→4: 1 + 5 + 2 = 8 → becomes 1 + 5 + 3 = 9
  • Path 1→2→5: 1 + 5 + 3 = 9 → stays 9
  • Path 1→3→6: 1 + 2 + 3 = 6 → becomes 1 + 5 + 3 = 9
  • Path 1→3→7: 1 + 2 + 1 = 4 → becomes 1 + 5 + 3 = 9

All paths now sum to 9, achieved with 6 total increments.

Solution Implementation

1class Solution:
2    def minIncrements(self, n: int, cost: List[int]) -> int:
3        # Initialize total increments needed
4        total_increments = 0
5      
6        # Process nodes from bottom to top (from last non-leaf to root)
7        # n >> 1 gives us n // 2, which is the last non-leaf node index
8        for node_index in range(n // 2, 0, -1):
9            # Calculate left and right child indices (1-indexed)
10            # Left child = 2 * parent, Right child = 2 * parent + 1
11            left_child = node_index * 2
12            right_child = node_index * 2 + 1
13          
14            # Add the difference between left and right child values to total
15            # This represents the increments needed to balance these siblings
16            total_increments += abs(cost[left_child - 1] - cost[right_child - 1])
17          
18            # Update parent value to include the maximum path sum from its subtree
19            # Convert to 0-indexed for array access
20            cost[node_index - 1] += max(cost[left_child - 1], cost[right_child - 1])
21      
22        return total_increments
23
1class Solution {
2    public int minIncrements(int n, int[] cost) {
3        int totalIncrements = 0;
4      
5        // Process nodes from bottom to top, starting from the last non-leaf node
6        // In a complete binary tree with n nodes, the last non-leaf node is at index n/2
7        for (int nodeIndex = n / 2; nodeIndex > 0; nodeIndex--) {
8            // Calculate indices of left and right children
9            // For 1-indexed tree: left child = 2*i, right child = 2*i + 1
10            int leftChildIndex = nodeIndex * 2;
11            int rightChildIndex = nodeIndex * 2 + 1;
12          
13            // Add the absolute difference between left and right child values to the result
14            // This represents the minimum increments needed to make these siblings equal
15            totalIncrements += Math.abs(cost[leftChildIndex - 1] - cost[rightChildIndex - 1]);
16          
17            // Update current node's value by adding the maximum of its children
18            // This propagates the path sum upward in the tree
19            cost[nodeIndex - 1] += Math.max(cost[leftChildIndex - 1], cost[rightChildIndex - 1]);
20        }
21      
22        return totalIncrements;
23    }
24}
25
1class Solution {
2public:
3    int minIncrements(int n, vector<int>& cost) {
4        int totalIncrements = 0;
5      
6        // Process nodes from bottom to top, starting from the last non-leaf node
7        // In a complete binary tree with n nodes, the last non-leaf node is at index n/2
8        for (int nodeIndex = n / 2; nodeIndex > 0; --nodeIndex) {
9            // Calculate indices of left and right children
10            // For 1-indexed node i: left child = 2*i, right child = 2*i + 1
11            int leftChildIndex = nodeIndex * 2;
12            int rightChildIndex = nodeIndex * 2 + 1;
13          
14            // Add the difference between left and right subtree values to total increments
15            // This represents the cost to equalize the two subtrees
16            totalIncrements += abs(cost[leftChildIndex - 1] - cost[rightChildIndex - 1]);
17          
18            // Update current node's value by adding the maximum of its children's values
19            // This propagates the path sum up the tree
20            cost[nodeIndex - 1] += max(cost[leftChildIndex - 1], cost[rightChildIndex - 1]);
21        }
22      
23        return totalIncrements;
24    }
25};
26
1/**
2 * Calculates the minimum number of increments needed to make all root-to-leaf paths have equal sum
3 * in a perfect binary tree.
4 * 
5 * @param n - The number of nodes in the perfect binary tree
6 * @param cost - Array of costs for each node (0-indexed)
7 * @returns The minimum number of increments required
8 */
9function minIncrements(n: number, cost: number[]): number {
10    let totalIncrements: number = 0;
11  
12    // Process nodes from bottom to top, starting from the last non-leaf node
13    // In a perfect binary tree, the last non-leaf node is at index n/2
14    for (let nodeIndex: number = n >> 1; nodeIndex > 0; nodeIndex--) {
15        // Calculate indices of left and right children (1-indexed in formula)
16        const leftChildIndex: number = nodeIndex << 1;      // Left child: 2 * nodeIndex
17        const rightChildIndex: number = (nodeIndex << 1) | 1; // Right child: 2 * nodeIndex + 1
18      
19        // Convert to 0-indexed for array access
20        const leftChildCost: number = cost[leftChildIndex - 1];
21        const rightChildCost: number = cost[rightChildIndex - 1];
22      
23        // Add the difference between left and right subtree costs to balance them
24        totalIncrements += Math.abs(leftChildCost - rightChildCost);
25      
26        // Update current node's cost to include the maximum cost from its subtrees
27        // This represents the total cost of the path through this node
28        cost[nodeIndex - 1] += Math.max(leftChildCost, rightChildCost);
29    }
30  
31    return totalIncrements;
32}
33

Time and Space Complexity

Time Complexity: O(n)

The algorithm iterates through nodes from n >> 1 (which is n/2) down to 1, processing each internal node exactly once. Since a complete binary tree with n nodes has (n-1)/2 internal nodes, and we visit each internal node once with constant-time operations (calculating indices, accessing array elements, performing arithmetic operations), the total time complexity is O(n/2) which simplifies to O(n).

Space Complexity: O(1)

The algorithm only uses a constant amount of extra space for variables ans, i, l, and r. Although the input array cost is modified in-place to store intermediate results (accumulated path sums from children), this doesn't count as additional space since we're reusing the existing array. No additional data structures are created that scale with the input size, resulting in O(1) space complexity.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Confusion Between 1-indexed Tree and 0-indexed Array

The most common pitfall is mixing up the indexing systems. The tree nodes are numbered from 1 to n, but the cost array is 0-indexed (from 0 to n-1).

Incorrect Implementation:

# Wrong: Forgetting to adjust for 0-indexed array
cost[node_index] += max(cost[left_child], cost[right_child])

Correct Implementation:

# Correct: Properly converting between 1-indexed nodes and 0-indexed array
cost[node_index - 1] += max(cost[left_child - 1], cost[right_child - 1])

2. Processing Nodes in Wrong Order

Processing nodes from top to bottom instead of bottom to top will give incorrect results because parent nodes need to know the accumulated path costs of their children first.

Incorrect Implementation:

# Wrong: Processing from root to leaves
for node_index in range(1, n // 2 + 1):
    # This won't work because children haven't been processed yet

Correct Implementation:

# Correct: Processing from leaves toward root
for node_index in range(n // 2, 0, -1):
    # Children are processed before their parents

3. Not Accumulating Path Costs Properly

Forgetting to update the parent's cost with the maximum child path cost will break the algorithm for trees with more than 2 levels.

Incorrect Implementation:

# Wrong: Only counting immediate increments without updating parent
total_increments += abs(cost[left_child - 1] - cost[right_child - 1])
# Missing: cost[node_index - 1] += max(cost[left_child - 1], cost[right_child - 1])

Correct Implementation:

# Correct: Both counting increments AND updating parent's accumulated cost
total_increments += abs(cost[left_child - 1] - cost[right_child - 1])
cost[node_index - 1] += max(cost[left_child - 1], cost[right_child - 1])

4. Incorrect Child Index Calculation

Using wrong formulas for calculating child indices, especially with bit operations.

Incorrect Implementation:

# Wrong: Incorrect child index calculations
left_child = node_index * 2 - 1  # This is wrong
right_child = node_index * 2      # This is wrong

Correct Implementation:

# Correct: Proper child index calculations for 1-indexed tree
left_child = node_index * 2
right_child = node_index * 2 + 1
# Or using bit operations:
left_child = node_index << 1
right_child = (node_index << 1) | 1

5. Edge Case: Single Node Tree

Not handling the case where n = 1 (tree with only root node). While the algorithm naturally handles this by not entering the loop, it's worth being aware of.

Solution: The loop range(n // 2, 0, -1) becomes range(0, 0, -1) when n = 1, which creates an empty range and returns 0 correctly (no increments needed for a single node).

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

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

Load More