956. Tallest Billboard
Problem Description
You need to build a billboard with two steel supports of equal height, one on each side. You're given a collection of rods
with different lengths that can be welded together to create these supports.
The goal is to make the tallest possible billboard by:
- Dividing the rods into two groups (one for each support)
- Each group's rods are welded together to form a support
- Both supports must have exactly the same height
- You want to maximize this height
For example, if you have rods of lengths [1, 2, 3]
:
- You could weld rod 3 for one support (height = 3)
- Weld rods 1 and 2 for the other support (height = 1 + 2 = 3)
- This gives you a billboard height of 3
Not all rods need to be used - some can be left out if it helps achieve equal heights for both supports.
Return the maximum possible height of the billboard. If it's impossible to create two supports of equal height, return 0
.
The solution uses dynamic programming with memoization. The dfs(i, j)
function tracks:
i
: the current rod index being consideredj
: the difference in height between the two supports
For each rod, there are three choices:
- Skip the rod (don't use it)
- Add it to the taller support (increasing the difference
j
) - Add it to the shorter support (decreasing the difference
j
)
The function returns the maximum height achievable when both supports are equal (when j = 0
).
Intuition
The key insight is that we need to track the difference between the two support heights rather than tracking each support's height separately. This transforms the problem into finding a way to make this difference equal to zero while maximizing the height of the supports.
Think of it this way: imagine you're building the two supports simultaneously. At any point, one support might be taller than the other. We can represent this state with just the difference in their heights. Our goal is to end with a difference of 0 (equal heights).
For each rod, we have three decisions:
- Don't use it - the difference stays the same
- Add it to the taller support - this increases the difference
- Add it to the shorter support - this reduces the difference
Why track the difference instead of individual heights? Because if we know the difference and how much we've added to get there, we can reconstruct the actual heights. More importantly, this reduces our state space significantly - instead of tracking two potentially large values, we track just one.
The brilliant part is in the third choice: when we add a rod to the shorter support, we're actually contributing to the final billboard height. If the rod is longer than the current difference, it makes the previously shorter support become the taller one, and we flip which support is "ahead". The amount we can count toward our final answer is min(j, rods[i])
- either we close the entire gap j
, or we use the entire rod length.
This dynamic programming approach with memoization ensures we explore all possible ways to distribute the rods while keeping track of the maximum achievable height when both supports are equal.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with memoization through Python's @cache
decorator. Let's break down the implementation:
State Definition:
dfs(i, j)
represents the maximum height achievable when:- We've considered rods from index
i
to the end - The current difference between the two supports is
j
- We've considered rods from index
Base Case:
- When
i >= len(rods)
(all rods considered):- If
j == 0
: both supports are equal, return0
(no more height can be added) - If
j != 0
: supports are unequal, return-inf
(invalid state)
- If
Recursive Transitions:
For each rod at index i
, we have three choices:
-
Skip the rod:
dfs(i + 1, j)
- move to next rod without changing the difference
-
Add to the taller support:
dfs(i + 1, j + rods[i])
- increases the difference byrods[i]
-
Add to the shorter support:
dfs(i + 1, abs(rods[i] - j)) + min(j, rods[i])
- The new difference becomes
abs(rods[i] - j)
- We add
min(j, rods[i])
to our answer because:- If
rods[i] <= j
: we close gap byrods[i]
, contributing that much to final height - If
rods[i] > j
: we close the entire gapj
and overshoot, contributingj
to final height
- If
Why this works:
- The function returns the maximum additional height we can achieve from the current state
- When we add a rod to the shorter support, we're "banking" height that counts toward our final answer
- The
@cache
decorator memoizes results, preventing redundant calculations
Time Complexity: O(n * S)
where n
is the number of rods and S
is the sum of all rod lengths (maximum possible difference)
Space Complexity: O(n * S)
for the memoization cache
The initial call dfs(0, 0)
starts with the first rod and zero difference, returning the maximum achievable billboard height.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with rods = [1, 2, 3, 4]
.
We start with dfs(0, 0)
- considering rod at index 0 (length 1) with difference 0.
Step 1: Rod 1 (length 1)
From dfs(0, 0)
, we have three choices:
- Skip it:
dfs(1, 0)
- Add to taller support:
dfs(1, 1)
(difference becomes 0+1=1) - Add to shorter support: Since both are equal (j=0), this also gives
dfs(1, 1)
but contributesmin(0, 1) = 0
Step 2: Rod 2 (length 2)
Let's follow the path where we added rod 1 to create difference 1: dfs(1, 1)
- Skip it:
dfs(2, 1)
- Add to taller support:
dfs(2, 3)
(difference becomes 1+2=3) - Add to shorter support:
dfs(2, 1)
+min(1, 2) = 1
(rod 2 closes gap of 1, contributing 1 to height)
Step 3: Rod 3 (length 3)
Following the path where we added rod 2 to shorter support with contribution 1: dfs(2, 1)
- Skip it:
dfs(3, 1)
- Add to taller support:
dfs(3, 4)
(difference becomes 1+3=4) - Add to shorter support:
dfs(3, 2)
+min(1, 3) = 1
(closes gap of 1, contributing 1 to height)
Step 4: Rod 4 (length 4)
Following path dfs(3, 2)
with accumulated height of 2:
- Skip it:
dfs(4, 2)
→ returns-inf
(unequal supports at end) - Add to taller support:
dfs(4, 6)
→ returns-inf
- Add to shorter support:
dfs(4, 2)
+min(2, 4) = 2
→ total contribution is 2, but base case returns-inf
Finding the optimal path: The algorithm explores all paths. The optimal solution is:
- Use rod 1 for support A (height = 1)
- Use rod 4 for support B (height = 4)
- Use rod 3 for support A (height = 1+3 = 4)
- Skip rod 2
This gives us two supports of height 4 each.
The recursion tracks this as:
- Start:
dfs(0, 0)
- Add rod 1 to one support:
dfs(1, 1)
- Skip rod 2:
dfs(2, 1)
- Add rod 3 to shorter support:
dfs(3, 2)
+ contribution of 1 - Add rod 4 to shorter support:
dfs(4, 0)
+ contribution of 2 - Base case with j=0 returns 0, total = 1+2+0 = 3...
Actually, let me recalculate the optimal path more carefully:
- Add rod 1 to support A: difference = 1
- Add rod 4 to support B: since 4 > 1, support B becomes taller by 3, contribution = min(1,4) = 1
- Add rod 3 to support B (currently shorter): contribution = min(3,3) = 3
- Final height = 1 + 3 = 4
The maximum billboard height is 4.
Solution Implementation
1class Solution:
2 def tallestBillboard(self, rods: List[int]) -> int:
3 """
4 Find the tallest billboard that can be built using two supports of equal height.
5
6 Args:
7 rods: List of available rod lengths
8
9 Returns:
10 Maximum height of billboard (height of one support)
11 """
12 from functools import cache
13 from math import inf
14
15 @cache
16 def dp(index: int, height_diff: int) -> int:
17 """
18 Dynamic programming function to find maximum height of shorter support.
19
20 Args:
21 index: Current rod index being considered
22 height_diff: Absolute difference between two support heights
23
24 Returns:
25 Maximum achievable height of the shorter support from this state
26 """
27 # Base case: no more rods to consider
28 if index >= len(rods):
29 # Valid only if both supports have equal height
30 return 0 if height_diff == 0 else -inf
31
32 current_rod = rods[index]
33
34 # Option 1: Skip current rod
35 result = dp(index + 1, height_diff)
36
37 # Option 2: Add current rod to the taller support
38 result = max(result, dp(index + 1, height_diff + current_rod))
39
40 # Option 3: Add current rod to the shorter support
41 # The shorter support gains min(height_diff, current_rod) in effective height
42 height_gain = min(height_diff, current_rod)
43 new_diff = abs(current_rod - height_diff)
44 result = max(result, dp(index + 1, new_diff) + height_gain)
45
46 return result
47
48 return dp(0, 0)
49
1class Solution {
2 // Memoization table: memo[index][difference] stores the maximum height of the shorter billboard
3 // when considering rods from index onwards with current difference between two billboards
4 private Integer[][] memo;
5 private int[] rods;
6 private int rodCount;
7
8 public int tallestBillboard(int[] rods) {
9 // Calculate total sum of all rod lengths for array sizing
10 int totalSum = 0;
11 for (int rodLength : rods) {
12 totalSum += rodLength;
13 }
14
15 // Initialize instance variables
16 this.rodCount = rods.length;
17 this.rods = rods;
18 this.memo = new Integer[rodCount][totalSum + 1];
19
20 // Start DFS from index 0 with difference 0 between two billboards
21 return dfs(0, 0);
22 }
23
24 /**
25 * DFS with memoization to find maximum height of equal billboards
26 * @param index Current rod index being considered
27 * @param difference Current difference between heights of two billboards
28 * @return Maximum height of the shorter billboard that can be achieved
29 */
30 private int dfs(int index, int difference) {
31 // Base case: all rods have been considered
32 if (index >= rodCount) {
33 // Return 0 if billboards are equal height, otherwise return invalid value
34 return difference == 0 ? 0 : -(1 << 30);
35 }
36
37 // Check if this state has been computed before
38 if (memo[index][difference] != null) {
39 return memo[index][difference];
40 }
41
42 // Option 1: Skip current rod (don't use it for either billboard)
43 int maxHeight = dfs(index + 1, difference);
44
45 // Option 2: Add current rod to the taller billboard
46 maxHeight = Math.max(maxHeight, dfs(index + 1, difference + rods[index]));
47
48 // Option 3: Add current rod to the shorter billboard
49 // New difference = |current rod length - current difference|
50 // Add min(difference, rod length) to account for height added to shorter billboard
51 int newDifference = Math.abs(rods[index] - difference);
52 int heightAdded = Math.min(difference, rods[index]);
53 maxHeight = Math.max(maxHeight, dfs(index + 1, newDifference) + heightAdded);
54
55 // Store result in memoization table and return
56 return memo[index][difference] = maxHeight;
57 }
58}
59
1class Solution {
2public:
3 int tallestBillboard(vector<int>& rods) {
4 // Calculate total sum of all rods
5 int totalSum = accumulate(rods.begin(), rods.end(), 0);
6 int numRods = rods.size();
7
8 // DP memoization table
9 // dp[i][j] represents the maximum height of the common part when:
10 // - we've considered rods from index i to n-1
11 // - the difference between two supports is j
12 int dp[numRods][totalSum + 1];
13 memset(dp, -1, sizeof(dp));
14
15 // Recursive function with memoization
16 // Parameters:
17 // - rodIndex: current rod being considered
18 // - heightDiff: absolute difference between heights of two supports
19 // Returns: maximum height of the shorter support
20 function<int(int, int)> solve = [&](int rodIndex, int heightDiff) -> int {
21 // Base case: all rods have been considered
22 if (rodIndex >= numRods) {
23 // Valid only if both supports have equal height (difference is 0)
24 return heightDiff == 0 ? 0 : -(1 << 30);
25 }
26
27 // Return memoized result if already computed
28 if (dp[rodIndex][heightDiff] != -1) {
29 return dp[rodIndex][heightDiff];
30 }
31
32 // Option 1: Skip current rod (don't use it in either support)
33 int maxHeight = solve(rodIndex + 1, heightDiff);
34
35 // Option 2: Add current rod to the taller support
36 // This increases the height difference
37 maxHeight = max(maxHeight, solve(rodIndex + 1, heightDiff + rods[rodIndex]));
38
39 // Option 3: Add current rod to the shorter support
40 // The new difference becomes |heightDiff - rods[rodIndex]|
41 // We add min(heightDiff, rods[rodIndex]) to track the height increase
42 // of the shorter support
43 int heightGain = min(heightDiff, rods[rodIndex]);
44 maxHeight = max(maxHeight,
45 solve(rodIndex + 1, abs(heightDiff - rods[rodIndex])) + heightGain);
46
47 // Store and return the result
48 return dp[rodIndex][heightDiff] = maxHeight;
49 };
50
51 // Start with rod index 0 and height difference 0
52 return solve(0, 0);
53 }
54};
55
1/**
2 * Finds the maximum height of two billboard supports that can be built with equal heights
3 * using the given rods. Each rod can be used on either support or not used at all.
4 *
5 * @param rods - Array of rod lengths
6 * @returns Maximum possible height of the billboard supports (when both are equal)
7 */
8function tallestBillboard(rods: number[]): number {
9 // Calculate total sum of all rod lengths
10 const totalSum: number = rods.reduce((accumulator, current) => accumulator + current, 0);
11
12 // Number of rods available
13 const rodCount: number = rods.length;
14
15 // Memoization table: dp[i][j] represents the maximum height achievable
16 // starting from rod index i with height difference j between two supports
17 const memoTable: number[][] = new Array(rodCount)
18 .fill(0)
19 .map(() => new Array(totalSum + 1).fill(-1));
20
21 /**
22 * Recursive function with memoization to find maximum height
23 *
24 * @param rodIndex - Current rod index being considered
25 * @param heightDifference - Current difference in height between two supports
26 * @returns Maximum achievable height from this state
27 */
28 const findMaxHeight = (rodIndex: number, heightDifference: number): number => {
29 // Base case: all rods have been considered
30 if (rodIndex >= rodCount) {
31 // Valid only if both supports have equal height (difference is 0)
32 return heightDifference === 0 ? 0 : -(1 << 30);
33 }
34
35 // Return memoized result if already computed
36 if (memoTable[rodIndex][heightDifference] !== -1) {
37 return memoTable[rodIndex][heightDifference];
38 }
39
40 // Option 1: Skip current rod
41 let maxHeight: number = findMaxHeight(rodIndex + 1, heightDifference);
42
43 // Option 2: Add current rod to the taller support
44 maxHeight = Math.max(
45 maxHeight,
46 findMaxHeight(rodIndex + 1, heightDifference + rods[rodIndex])
47 );
48
49 // Option 3: Add current rod to the shorter support
50 // The height added to result is the minimum of current difference and rod length
51 // because we're only counting the height that contributes to equalizing
52 maxHeight = Math.max(
53 maxHeight,
54 findMaxHeight(rodIndex + 1, Math.abs(heightDifference - rods[rodIndex])) +
55 Math.min(heightDifference, rods[rodIndex])
56 );
57
58 // Store and return the computed result
59 memoTable[rodIndex][heightDifference] = maxHeight;
60 return maxHeight;
61 };
62
63 // Start the recursive search from rod 0 with height difference 0
64 return findMaxHeight(0, 0);
65}
66
Time and Space Complexity
Time Complexity: O(n * S)
where n
is the length of the rods array and S
is the sum of all rod lengths.
The analysis follows from the memoized recursive solution:
- The function
dfs(i, j)
has two parameters: indexi
ranging from0
ton-1
, and differencej
which can range from-S
toS
whereS = sum(rods)
. - Due to the
@cache
decorator, each unique state(i, j)
is computed only once. - Total number of unique states:
n * (2S + 1) ≈ O(n * S)
- Each state performs constant time operations (three recursive calls with memoization).
- Therefore, total time complexity is
O(n * S)
.
Space Complexity: O(n * S)
The space complexity comes from:
- The memoization cache stores up to
O(n * S)
states. - The recursion stack depth is at most
n
, contributingO(n)
to space. - Since
O(n * S) > O(n)
, the overall space complexity isO(n * S)
.
Note: In practice, not all states (i, j)
will be reached, so the actual complexity might be lower, but the worst-case remains O(n * S)
for both time and space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the State Representation
Pitfall: Many developers incorrectly interpret j
(height_diff) as the actual heights of the two supports, leading to confusion about what the function returns.
Issue: The state dfs(i, j)
doesn't track individual support heights but rather their difference. This abstraction is crucial for reducing the state space from O(n * S * S) to O(n * S).
Solution: Think of j
as always representing |height_of_taller - height_of_shorter|. The function returns the additional height that can be added to the shorter support while maintaining or achieving equality.
2. Incorrect Calculation When Adding to Shorter Support
Pitfall: Writing dfs(i + 1, abs(rods[i] - j)) + rods[i]
instead of dfs(i + 1, abs(rods[i] - j)) + min(j, rods[i])
.
Issue: When adding a rod to the shorter support, only the portion that contributes to equalizing the heights counts toward the final answer. If the rod is longer than the gap, only the gap amount contributes to the billboard height.
Solution: Always use min(j, rods[i])
to represent the actual contribution to the final height:
# Correct: only count the height that helps equalize
result = max(result, dfs(i + 1, abs(rods[i] - j)) + min(j, rods[i]))
3. Not Handling Negative Infinity Properly
Pitfall: Using a small negative number like -1
or -10000
instead of negative infinity for invalid states.
Issue: Since we're taking maximum values, using insufficiently small numbers can lead to incorrect results when valid paths exist with negative intermediate values.
Solution: Always use -inf
from the math module:
from math import inf
# Base case for invalid state
if index >= len(rods):
return 0 if height_diff == 0 else -inf
4. Forgetting to Consider the "Skip" Option
Pitfall: Only considering adding rods to either support, forgetting that skipping a rod is also valid.
Issue: The optimal solution might require leaving some rods unused. For example, with rods [1, 2, 3, 4]
, the optimal solution is to use [1, 3]
and [4]
for height 4, skipping rod 2.
Solution: Always include the skip option as the first choice:
# Option 1: Skip current rod (always consider this first) result = dp(index + 1, height_diff)
5. Cache Key Collision with Negative Differences
Pitfall: Not using absolute values for height differences, allowing both positive and negative differences in the cache.
Issue: States like (i, 3)
and (i, -3)
represent the same configuration (one support is 3 units taller) but would be cached separately, wasting memory and computation.
Solution: Always maintain height_diff
as a non-negative value using abs()
:
# When adding to shorter support, ensure new difference is absolute
new_diff = abs(current_rod - height_diff)
result = max(result, dp(index + 1, new_diff) + height_gain)
Which technique can we use to find the middle of a linked list?
Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!