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 node2 * i
and its right child is node2 * 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.
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:
- Calculate the cost difference between left and right children
- Add this difference to our answer
- 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:
-
Start from the last non-leaf nodes: We iterate from node
n >> 1
(which isn // 2
) down to node1
. In a perfect binary tree withn
nodes, nodes from1
ton // 2
are non-leaf nodes, while nodes fromn // 2 + 1
ton
are leaf nodes. -
For each non-leaf node
i
:- Calculate its children indices:
- Left child:
l = i << 1
(which isi * 2
) - Right child:
r = i << 1 | 1
(which isi * 2 + 1
)
- Left child:
- Calculate its children indices:
-
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])
- Find the cost difference:
-
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
ton
, but the cost array is 0-indexed, we usecost[i - 1]
to access the cost of nodei
. -
Bit operations:
n >> 1
efficiently computesn // 2
i << 1
efficiently computesi * 2
i << 1 | 1
efficiently computesi * 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 EvaluatorExample 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).
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!