Make Costs of Paths Equal in a Binary Tree
MedPrompt
You are given an integer n representing the number of nodes in a perfect binary tree consisting of nodes numbered from 1 to n. The root of the tree is node 1 and each node i in the tree has two children where the left child is the node 2 * i and the right child is 2 * i + 1.
Each node in the tree also has a cost represented by a given 0-indexed integer array cost of size n where cost[i] is the cost of node i + 1. You are allowed to increment the cost of any node by 1 any number of times.
Return the minimum number of increments you need to make the cost of paths from the root to each leaf node equal.
Note:
- A perfect binary tree is a tree where each node, except the leaf nodes, has exactly 2 children.
- The cost of a path is the sum of costs of nodes in the path.
Example 1:
Input: n = 7, cost = [1,5,2,2,3,3,1]
Output: 6
Explanation: We can do the following increments:
- Increase the cost of node 4 one time.
- Increase the cost of node 3 three times.
- Increase the cost of node 7 two times.
Each path from the root to a leaf will have a total cost of 9.
The total increments we did is 1 + 3 + 2 = 6.
It can be shown that this is the minimum answer we can achieve.Example 2:
Input: n = 3, cost = [5,3,3]
Output: 0
Explanation: The two paths already have equal total costs, so no increments are needed.
Constraints:
3 <= n <= 105n + 1is a power of2cost.length == n1 <= cost[i] <= 104
Approaches
2 approaches with complexity analysis and trade-offs.
This approach utilizes a Depth First Search (DFS) to traverse the tree. It works in a post-order fashion, starting from the leaves and moving up to the root. For each internal node, it recursively calculates the costs of paths in its left and right subtrees, makes them equal by adding the difference to a running total, and then propagates the new, higher path cost up to its parent.
Algorithm
- Create a member variable
totalIncrementsinitialized to 0 to accumulate the total increments. - Define a recursive function
dfs(nodeIndex, n, cost)that returns the total cost of a path fromnodeIndexto a leaf in its subtree after equalization. - In
dfs(nodeIndex):- Base Case: If
nodeIndexrepresents a leaf node (i.e.,2 * nodeIndex > n), it has no children. The path cost from itself is just its own cost, so returncost[nodeIndex - 1]. - Recursive Step: If
nodeIndexis an internal node:- Recursively call
dfsfor the left child (2 * nodeIndex) and right child (2 * nodeIndex + 1) to get their respective equalized path costs,leftPathCostandrightPathCost. - The cost to make the two subtrees equal is the absolute difference between their path costs. Add this difference,
Math.abs(leftPathCost - rightPathCost), to thetotalIncrements. - The new, unified path cost from
nodeIndexdownwards is its own cost plus the maximum of its children's path costs. Returncost[nodeIndex - 1] + Math.max(leftPathCost, rightPathCost).
- Recursively call
- Base Case: If
- To start the process, call
dfs(1, n, cost)from the main function. - The final answer is the accumulated value in
totalIncrements.
Walkthrough
The problem asks for the minimum increments to make all root-to-leaf path costs equal. This can be achieved by ensuring that for any node, the path costs to any leaf in its left subtree are equal to the path costs to any leaf in its right subtree.
This suggests a bottom-up strategy. We can implement this using a recursive post-order traversal. We define a recursive function, say dfs(nodeIndex), which calculates two things implicitly: the minimum increments needed for the subtree at nodeIndex and the cost of a path from nodeIndex to a leaf after equalization.
The function dfs(nodeIndex) will return the equalized path cost from nodeIndex to a leaf. A global or member variable will keep track of the total increments.
- Base Case: For a leaf node, no increments are needed within its subtree, and the path cost is its own cost.
- Recursive Step: For an internal node
p, we first calldfson its childrenlandr. This gives us the equalized path costs fromlandrdownwards, let's call themcost_landcost_r. To make the paths throughpequal, we must havecost[p-1] + cost_lequal tocost[p-1] + cost_rafter some increments. This meanscost_landcost_rmust be equalized. The minimum increment to do this isabs(cost_l - cost_r). We add this to our total. The new path cost frompiscost[p-1] + max(cost_l, cost_r). This value is returned to the caller (p's parent).
The initial call is dfs(1) for the root. The final answer is the total accumulated increments.
class Solution { private int totalIncrements = 0; public int minIncrements(int n, int[] cost) { dfs(1, n, cost); return totalIncrements; } /** * Performs a post-order traversal to calculate path costs and increments. * @param i The current node index (1-based). * @param n The total number of nodes. * @param cost The array of costs. * @return The cost of the path from node i to a leaf after equalization. */ private int dfs(int i, int n, int[] cost) { // Base case: if it's a leaf node (nodes from n/2 + 1 to n are leaves). // The children would be 2*i and 2*i+1. If 2*i > n, it's a leaf. if (i > n / 2) { return cost[i - 1]; } // Children indices are 1-based. int leftChildIndex = 2 * i; int rightChildIndex = 2 * i + 1; // Recursively find the path costs for children subtrees. int leftPathCost = dfs(leftChildIndex, n, cost); int rightPathCost = dfs(rightChildIndex, n, cost); // The number of increments needed at this level is the absolute // difference between the path costs of the two children subtrees. totalIncrements += Math.abs(leftPathCost - rightPathCost); // The new path cost from this node 'i' is its own cost plus the // maximum of its children's path costs, as we increment the cheaper path. return cost[i - 1] + Math.max(leftPathCost, rightPathCost); }}Complexity
Time
O(n) Each node in the tree is visited exactly once by the recursive function. Therefore, the time complexity is linear in the number of nodes.
Space
O(log n) The space complexity is determined by the maximum depth of the recursion stack. Since the tree is a perfect binary tree with `n` nodes, its height is O(log n).
Trade-offs
Pros
The recursive structure is a natural fit for tree problems and can be more intuitive to understand.
The logic directly follows the problem's recursive sub-structure.
Cons
Uses recursion, which incurs overhead from function calls.
The space complexity is O(log n) due to the recursion stack, which is less optimal than the iterative solution's O(1) space.
This is the most optimal approach, which uses an iterative, bottom-up strategy. It avoids recursion by iterating from the last level of parent nodes up to the root. For each parent, it calculates the necessary increments to equalize the path costs of its two children subtrees and updates the parent's cost in the array to reflect the new, equalized path cost. This allows the information to be propagated up the tree efficiently.
Algorithm
- Initialize a variable
totalIncrementsto 0. - Iterate backwards through the parent nodes of the tree. In a perfect binary tree with nodes 1 to
n, the parent nodes are numbered from1ton/2. So, the loop runs fromi = n/2down to1. - Inside the loop, for each parent node
i:- Identify its left child
2*iand right child2*i+1. - Retrieve the path costs from these children, which are stored in
cost[2*i - 1]andcost[2*i + 1 - 1]respectively. Note that because we are iterating from the bottom up, these values represent the total path cost from that child to a leaf. - Calculate the absolute difference between the left and right path costs:
Math.abs(cost[2*i - 1] - cost[2*i - 1]). Add this value tototalIncrements. - Update the parent's cost entry
cost[i-1]to store the new total path cost from this parent to a leaf. This is done by adding the maximum of its children's path costs to its own cost:cost[i-1] += Math.max(cost[2*i - 1], cost[2*i - 1]).
- Identify its left child
- After the loop finishes,
totalIncrementswill hold the minimum total increments required. Return this value.
Walkthrough
The key insight is that for any parent node, the path costs to all leaves in its left subtree must be equal, and the same for its right subtree. To make all paths from the root equal, we must first satisfy this condition at every level, starting from the bottom.
We can iterate from the last parent node (n/2) up to the root (1). For each parent i, its children are 2*i and 2*i+1. By processing in this reverse order, when we are at node i, the values cost[2*i - 1] and cost[2*i+1 - 1] will have already been updated to represent the total path cost from that child node down to a leaf.
At each parent i, we compare the path costs from its two children. The difference abs(cost_left - cost_right) is the minimum amount we need to add to balance these two subtrees. This amount is added to our total increments. We then update the parent's cost cost[i-1] to be its original cost plus the maximum of the two children's path costs. This new value in cost[i-1] now represents the total path cost from node i to a leaf, which will be used when its own parent is processed.
This method cleverly reuses the input cost array to store the propagating path costs, achieving O(1) extra space.
class Solution { public int minIncrements(int n, int[] cost) { int totalIncrements = 0; // The parent nodes in a perfect binary tree are from 1 to n/2. // We iterate backwards from the last parent up to the root. for (int i = n / 2; i >= 1; i--) { int leftChildIndex = 2 * i; int rightChildIndex = 2 * i + 1; // Array indices are 0-based, so we subtract 1. int leftPathCost = cost[leftChildIndex - 1]; int rightPathCost = cost[rightChildIndex - 1]; // The cost to make the paths from the children equal is their difference. totalIncrements += Math.abs(leftPathCost - rightPathCost); // Propagate the path cost up to the parent. The new path cost from the // parent is its own cost plus the maximum of the children's path costs. cost[i - 1] += Math.max(leftPathCost, rightPathCost); } return totalIncrements; }}Complexity
Time
O(n) The algorithm iterates through the first `n/2` nodes of the tree (the parent nodes) exactly once. Each iteration involves a constant number of operations. Thus, the time complexity is linear with respect to `n`.
Space
O(1) This approach uses a constant amount of extra space. It modifies the input `cost` array in-place to store intermediate path costs, thus not requiring any additional data structures that scale with the input size.
Trade-offs
Pros
Extremely efficient with O(1) space complexity, as it modifies the input array in-place.
Iterative approach avoids recursion overhead and the risk of stack overflow on extremely deep trees.
Simple and concise implementation.
Cons
This approach modifies the input
costarray, which might not be permissible in some scenarios (though it is fine for this problem).The logic of iterating backwards from
n/2might be slightly less intuitive than a direct recursive traversal for some developers.
Solutions
Solution
class Solution {private int[] cost;private int n;private int ans;public int minIncrements(int n, int[] cost) { this.n = n; this.cost = cost; dfs(1); return ans; }private int dfs(int i) { if ((i << 1) > n) { return cost[i - 1]; } int l = dfs(i << 1); int r = dfs(i << 1 | 1); ans += Math.max(l, r) - Math.min(l, r); return cost[i - 1] + Math.max(l, r); }}class Solution {public: int minIncrements(int n, vector<int> &cost) { int ans = 0; function<int(int)> dfs = [&](int i) -> int { if ((i << 1) > n) { return cost[i - 1]; } int l = dfs(i << 1); int r = dfs(i << 1 | 1); ans += max(l, r) - min(l, r); return cost[i - 1] + max(l, r); }; dfs(1); return ans; }};class Solution: def minIncrements(self, n: int, cost: List[int]) -> int: def dfs(i: int) -> int: if (i << 1) > n: return cost[i - 1] l, r = dfs(i << 1), dfs(i << 1 | 1) nonlocal ans ans += max(l, r) - min(l, r) return cost[i - 1] + max(l, r) ans = 0 dfs(1) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.