1691. Maximum Height by Stacking Cuboids
Problem Description
You are given n
cuboids (3D rectangular boxes), where each cuboid has three dimensions: width, length, and height. The dimensions of the i-th
cuboid are given as cuboids[i] = [width_i, length_i, height_i]
.
Your task is to select a subset of these cuboids and stack them on top of each other to achieve the maximum possible height.
The stacking rules are:
-
Cuboid
i
can be placed on top of cuboidj
if and only if:width_i ≤ width_j
length_i ≤ length_j
height_i ≤ height_j
-
You can rotate any cuboid before placing it. This means you can rearrange its three dimensions in any order (e.g., you can make any dimension the "height", "width", or "length").
The goal is to find the maximum total height you can achieve by stacking a valid subset of cuboids.
For example, if you have a cuboid with dimensions [1, 2, 3]
, you can rotate it to have dimensions like [2, 3, 1]
or [3, 1, 2]
, etc., choosing whichever orientation helps you build the tallest stack.
The key insight is that for any valid stacking configuration, we can ensure each cuboid is oriented such that its dimensions are sorted (smallest to largest), and the largest dimension becomes the height. This maximizes the contribution of each cuboid to the total height while maintaining valid stacking conditions.
Intuition
The key observation is that we're allowed to rotate cuboids, which gives us flexibility in choosing which dimension becomes the height. Since we want to maximize the total height, we should orient each cuboid to use its largest dimension as the height whenever possible.
Think about it this way: if we have a cuboid with dimensions [1, 2, 3]
, and we can choose any orientation, why not put 3
as the height to maximize our contribution? But there's a constraint - we need to ensure valid stacking.
For any two cuboids that can be stacked, if we sort the dimensions of each cuboid individually (so [1, 2, 3]
stays [1, 2, 3]
but [3, 1, 2]
becomes [1, 2, 3]
), we can always use the largest dimension as height while maintaining the stacking validity. This is because if cuboid A can be placed on cuboid B in any orientation, then cuboid A with sorted dimensions can be placed on cuboid B with sorted dimensions, using the largest values as heights.
Once we've sorted each cuboid's dimensions, we need to determine the optimal stacking order. If we sort all cuboids by their dimensions (comparing them lexicographically), we create a natural ordering where smaller cuboids come before larger ones. This ordering ensures that if cuboid i
comes before cuboid j
in our sorted list, and i
can be placed on j
, we'll consider this possibility.
Now the problem transforms into finding the best subset and their stacking order. This resembles the longest increasing subsequence problem, but instead of finding the longest sequence, we're finding the sequence with maximum height sum. We can use dynamic programming where f[i]
represents the maximum height achievable with cuboid i
at the bottom. For each cuboid i
, we check all previous cuboids j
that could potentially go on top of it (where j
's width and length are both less than or equal to i
's), and take the best option.
The rotation flexibility combined with sorting essentially converts a complex 3D stacking problem into a more manageable dynamic programming problem on sorted sequences.
Learn more about Dynamic Programming and Sorting patterns.
Solution Approach
The implementation follows these key steps:
Step 1: Sort each cuboid's dimensions
for c in cuboids: c.sort()
We sort each cuboid's three dimensions in ascending order. After this operation, each cuboid has its dimensions arranged as [smallest, middle, largest]
. This ensures that we'll use the largest dimension as the height when stacking.
Step 2: Sort all cuboids
cuboids.sort()
We sort the entire array of cuboids lexicographically. This creates an ordering where smaller cuboids appear before larger ones. For example, [1, 2, 3]
comes before [1, 2, 4]
and [2, 3, 4]
. This ordering is crucial because it guarantees that if cuboid j
can be placed on cuboid i
, then j
appears before i
in the sorted array.
Step 3: Dynamic Programming
n = len(cuboids)
f = [0] * n
We create a DP array f
where f[i]
represents the maximum height achievable when cuboid i
is at the bottom of the stack.
Step 4: Fill the DP table
for i in range(n):
for j in range(i):
if cuboids[j][1] <= cuboids[i][1] and cuboids[j][2] <= cuboids[i][2]:
f[i] = max(f[i], f[j])
f[i] += cuboids[i][2]
For each cuboid i
, we:
- Check all previous cuboids
j
(wherej < i
) - Verify if cuboid
j
can be placed on top of cuboidi
by checking:cuboids[j][1] <= cuboids[i][1]
(middle dimension comparison)cuboids[j][2] <= cuboids[i][2]
(largest dimension comparison)- We don't need to check
cuboids[j][0] <= cuboids[i][0]
because the lexicographic sorting already ensures this condition
- If valid, update
f[i]
to be the maximum of its current value andf[j]
(the best height achievable withj
on top) - Finally, add cuboid
i
's height (cuboids[i][2]
) tof[i]
The state transition equation is:
f[i] = max{f[j]} + height[i]
for all valid j < i
Step 5: Return the result
return max(f)
The answer is the maximum value in the DP array, representing the tallest possible stack among all possible configurations.
The time complexity is O(n²)
for the DP iterations plus O(n log n)
for sorting, resulting in O(n²)
overall. The space complexity is O(n)
for the DP array.
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 the solution with a concrete example. Suppose we have the following cuboids:
cuboids = [[2,3,1], [1,1,3], [2,2,1]]
Step 1: Sort each cuboid's dimensions
After sorting each cuboid individually:
[2,3,1]
becomes[1,2,3]
[1,1,3]
becomes[1,1,3]
[2,2,1]
becomes[1,2,2]
Now we have: cuboids = [[1,2,3], [1,1,3], [1,2,2]]
Each cuboid now has its largest dimension as the last element, which will be used as height.
Step 2: Sort all cuboids lexicographically
Sorting the entire array:
[1,1,3]
comes first (smallest second dimension)[1,2,2]
comes second[1,2,3]
comes third
After sorting: cuboids = [[1,1,3], [1,2,2], [1,2,3]]
Step 3 & 4: Dynamic Programming
Initialize DP array: f = [0, 0, 0]
Process each cuboid:
i = 0: cuboid = [1,1,3]
- No previous cuboids to check
f[0] = 0 + 3 = 3
- DP array:
f = [3, 0, 0]
i = 1: cuboid = [1,2,2]
- Check j = 0: cuboid
[1,1,3]
- Can
[1,1,3]
be placed on[1,2,2]
? - Check:
1 ≤ 2
(middle) ✓ and3 ≤ 2
(height) ✗ - Cannot stack, so skip
- Can
f[1] = 0 + 2 = 2
- DP array:
f = [3, 2, 0]
i = 2: cuboid = [1,2,3]
- Check j = 0: cuboid
[1,1,3]
- Can
[1,1,3]
be placed on[1,2,3]
? - Check:
1 ≤ 2
(middle) ✓ and3 ≤ 3
(height) ✓ - Valid! Update:
f[2] = max(0, f[0]) = max(0, 3) = 3
- Can
- Check j = 1: cuboid
[1,2,2]
- Can
[1,2,2]
be placed on[1,2,3]
? - Check:
2 ≤ 2
(middle) ✓ and2 ≤ 3
(height) ✓ - Valid! Update:
f[2] = max(3, f[1]) = max(3, 2) = 3
- Can
- Add current height:
f[2] = 3 + 3 = 6
- DP array:
f = [3, 2, 6]
Step 5: Return maximum
The maximum value in f
is 6, which represents stacking:
- Bottom:
[1,2,3]
(oriented from original[2,3,1]
) - Top:
[1,1,3]
(oriented from original[1,1,3]
) - Total height: 3 + 3 = 6
This is indeed the maximum possible height for this set of cuboids.
Solution Implementation
1class Solution:
2 def maxHeight(self, cuboids: List[List[int]]) -> int:
3 # Sort each cuboid's dimensions in ascending order
4 # This ensures we can use any dimension as height while maintaining validity
5 for cuboid in cuboids:
6 cuboid.sort()
7
8 # Sort all cuboids lexicographically
9 # This helps establish a potential stacking order
10 cuboids.sort()
11
12 num_cuboids = len(cuboids)
13
14 # dp[i] represents the maximum height achievable with cuboid i at the top
15 dp = [0] * num_cuboids
16
17 # Process each cuboid as a potential top of the stack
18 for i in range(num_cuboids):
19 # Check all previous cuboids as potential bases
20 for j in range(i):
21 # A cuboid j can be placed below cuboid i if:
22 # - width of j <= width of i (cuboids[j][1] <= cuboids[i][1])
23 # - depth of j <= depth of i (cuboids[j][2] <= cuboids[i][2])
24 # Note: length is already satisfied due to sorting
25 if (cuboids[j][1] <= cuboids[i][1] and
26 cuboids[j][2] <= cuboids[i][2]):
27 # Update maximum height if stacking on cuboid j is better
28 dp[i] = max(dp[i], dp[j])
29
30 # Add current cuboid's height (largest dimension after sorting)
31 dp[i] += cuboids[i][2]
32
33 # Return the maximum height among all possible configurations
34 return max(dp)
35
1class Solution {
2 public int maxHeight(int[][] cuboids) {
3 // Step 1: Sort each cuboid's dimensions in ascending order
4 // This ensures the largest dimension becomes the potential height
5 for (int[] cuboid : cuboids) {
6 Arrays.sort(cuboid);
7 }
8
9 // Step 2: Sort all cuboids by their dimensions (width, depth, height)
10 // This enables dynamic programming by ensuring potential base cuboids come before top ones
11 Arrays.sort(cuboids, (cuboidA, cuboidB) -> {
12 if (cuboidA[0] != cuboidB[0]) {
13 return cuboidA[0] - cuboidB[0]; // Compare by width first
14 }
15 if (cuboidA[1] != cuboidB[1]) {
16 return cuboidA[1] - cuboidB[1]; // Then by depth
17 }
18 return cuboidA[2] - cuboidB[2]; // Finally by height
19 });
20
21 int numberOfCuboids = cuboids.length;
22 // dp[i] represents the maximum height achievable with cuboid i as the top
23 int[] dp = new int[numberOfCuboids];
24
25 // Step 3: Calculate maximum height using dynamic programming
26 for (int currentIndex = 0; currentIndex < numberOfCuboids; currentIndex++) {
27 // Check all previous cuboids as potential bases
28 for (int previousIndex = 0; previousIndex < currentIndex; previousIndex++) {
29 // A cuboid can be stacked on another if all dimensions are smaller or equal
30 boolean canStack = cuboids[previousIndex][1] <= cuboids[currentIndex][1] &&
31 cuboids[previousIndex][2] <= cuboids[currentIndex][2];
32
33 if (canStack) {
34 // Update maximum height if stacking on previous cuboid gives better result
35 dp[currentIndex] = Math.max(dp[currentIndex], dp[previousIndex]);
36 }
37 }
38 // Add current cuboid's height to the stack
39 dp[currentIndex] += cuboids[currentIndex][2];
40 }
41
42 // Step 4: Return the maximum height among all possible stacks
43 return Arrays.stream(dp).max().getAsInt();
44 }
45}
46
1class Solution {
2public:
3 int maxHeight(vector<vector<int>>& cuboids) {
4 // Step 1: Sort each cuboid's dimensions in ascending order
5 // This ensures that for each cuboid, we have width <= length <= height
6 // This is optimal because we want to maximize height when stacking
7 for (auto& cuboid : cuboids) {
8 sort(cuboid.begin(), cuboid.end());
9 }
10
11 // Step 2: Sort all cuboids lexicographically
12 // This ensures that if cuboid i can be placed on cuboid j,
13 // then i appears before j in the sorted array
14 sort(cuboids.begin(), cuboids.end());
15
16 int n = cuboids.size();
17
18 // dp[i] represents the maximum height achievable with cuboid i as the top
19 vector<int> dp(n);
20
21 // Step 3: Dynamic programming to find maximum stackable height
22 for (int i = 0; i < n; ++i) {
23 // Check all previous cuboids to see if they can be placed below current cuboid
24 for (int j = 0; j < i; ++j) {
25 // A cuboid j can be placed below cuboid i if:
26 // width[j] <= width[i] AND length[j] <= length[i] AND height[j] <= height[i]
27 // Since we sorted dimensions: cuboid[0] = width, cuboid[1] = length, cuboid[2] = height
28 if (cuboids[j][1] <= cuboids[i][1] && cuboids[j][2] <= cuboids[i][2]) {
29 // Update maximum height achievable with cuboid i on top
30 dp[i] = max(dp[i], dp[j]);
31 }
32 }
33 // Add current cuboid's height to the stack
34 dp[i] += cuboids[i][2];
35 }
36
37 // Step 4: Return the maximum height among all possible stacks
38 return *max_element(dp.begin(), dp.end());
39 }
40};
41
1/**
2 * Finds the maximum height of stacked cuboids where each cuboid can be rotated
3 * and must satisfy stacking constraints (width, length, height all must be >= the cuboid below)
4 * @param cuboids - Array of cuboids, each represented as [width, length, height]
5 * @returns The maximum possible height of the stack
6 */
7function maxHeight(cuboids: number[][]): number {
8 // Sort dimensions of each cuboid in ascending order
9 // This ensures we always use the optimal orientation with smallest dimensions first
10 for (const cuboid of cuboids) {
11 cuboid.sort((a, b) => a - b);
12 }
13
14 // Sort cuboids lexicographically by their dimensions
15 // This ensures we can build valid stacks in a bottom-up manner
16 cuboids.sort((a, b) => {
17 // First compare by width (smallest dimension)
18 if (a[0] !== b[0]) {
19 return a[0] - b[0];
20 }
21 // Then compare by length (middle dimension)
22 if (a[1] !== b[1]) {
23 return a[1] - b[1];
24 }
25 // Finally compare by height (largest dimension)
26 return a[2] - b[2];
27 });
28
29 const numberOfCuboids: number = cuboids.length;
30 // Dynamic programming array where dp[i] represents the maximum height
31 // achievable with cuboid i as the top of the stack
32 const dp: number[] = Array(numberOfCuboids).fill(0);
33
34 // Build up the DP solution
35 for (let i = 0; i < numberOfCuboids; ++i) {
36 // Check all previous cuboids that could potentially be placed below cuboid i
37 for (let j = 0; j < i; ++j) {
38 // Check if cuboid j can be placed below cuboid i
39 // Width and length must be <= since we've already sorted dimensions
40 const canStack: boolean = cuboids[j][1] <= cuboids[i][1] &&
41 cuboids[j][2] <= cuboids[i][2];
42
43 if (canStack) {
44 // Update maximum height achievable with cuboid i on top
45 dp[i] = Math.max(dp[i], dp[j]);
46 }
47 }
48 // Add the height of current cuboid (largest dimension after sorting)
49 dp[i] += cuboids[i][2];
50 }
51
52 // Return the maximum height among all possible stacks
53 return Math.max(...dp);
54}
55
Time and Space Complexity
Time Complexity: O(n^2)
The time complexity is dominated by the following operations:
- Sorting each cuboid's dimensions:
O(n)
iterations, each takingO(1)
time since each cuboid has only 3 dimensions - Sorting all cuboids:
O(n log n)
- Nested loops for dynamic programming:
O(n^2)
, where the outer loop runsn
times and the inner loop runs up toi
times for each iteration
The overall time complexity is O(n) + O(n log n) + O(n^2) = O(n^2)
, where n
is the number of cuboids.
Space Complexity: O(n)
The space complexity comes from:
- The array
f
of sizen
used for dynamic programming:O(n)
- The in-place sorting doesn't require additional space beyond constant variables
Therefore, the total space complexity is O(n)
, where n
is the number of cuboids.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Dimension Comparison After Sorting
The Problem: A common mistake is assuming that after sorting cuboids lexicographically, we only need to check if one cuboid can be placed on another by comparing their sorted dimensions directly. However, the sorting alone doesn't guarantee all three dimensions satisfy the stacking constraints.
Incorrect Approach:
# WRONG: Only checking if cuboids[j] <= cuboids[i] lexicographically
for i in range(n):
for j in range(i):
# This is insufficient!
if cuboids[j] <= cuboids[i]:
f[i] = max(f[i], f[j])
f[i] += cuboids[i][2]
Why It Fails:
Consider cuboids [1, 3, 5]
and [2, 2, 4]
. After sorting, we have:
- Cuboid A:
[1, 3, 5]
- Cuboid B:
[2, 2, 4]
Lexicographically, A < B (since 1 < 2), but B cannot be placed on A because B's width (2) > A's width (1).
The Solution: Explicitly check all relevant dimensions after the first one:
for i in range(n):
for j in range(i):
# Must check all dimensions explicitly
if (cuboids[j][0] <= cuboids[i][0] and
cuboids[j][1] <= cuboids[i][1] and
cuboids[j][2] <= cuboids[i][2]):
f[i] = max(f[i], f[j])
f[i] += cuboids[i][2]
Pitfall 2: Forgetting to Consider Each Cuboid as a Potential Base
The Problem: Some might think they need to find a single continuous chain of cuboids and forget that any cuboid can serve as the base of the stack, not just the "smallest" one.
Incorrect Approach:
# WRONG: Starting from a fixed base
def maxHeight(self, cuboids):
for c in cuboids:
c.sort()
cuboids.sort()
# Assuming we must start from cuboids[0]
height = cuboids[0][2]
for i in range(1, len(cuboids)):
if can_stack(cuboids[i-1], cuboids[i]):
height += cuboids[i][2]
return height
Why It Fails: This approach forces us to use consecutive cuboids and start from a specific base, missing optimal solutions where we skip certain cuboids or start from a different base.
The Solution: The DP approach correctly considers each cuboid as a potential base and finds the maximum among all possibilities:
# Correct: Consider all possible configurations
return max(dp) # Returns the best result among all possible bases
Pitfall 3: Not Sorting Individual Cuboid Dimensions First
The Problem: Forgetting to sort each cuboid's dimensions before processing leads to suboptimal solutions because we're not maximizing the height contribution of each cuboid.
Incorrect Approach:
# WRONG: Not sorting individual cuboid dimensions
def maxHeight(self, cuboids):
# Missing: for c in cuboids: c.sort()
cuboids.sort() # Only sorting the array of cuboids
# ... rest of the DP logic
Why It Fails:
Without sorting individual dimensions, a cuboid [5, 1, 2]
would use 2 as its height instead of the optimal 5 (after rotation to [1, 2, 5]
).
The Solution: Always sort each cuboid's dimensions first to ensure the largest dimension becomes the height:
# Correct: Sort each cuboid's dimensions first for cuboid in cuboids: cuboid.sort() # Now each cuboid is [smallest, middle, largest]
What are the most two important steps in writing a depth first search function? (Select 2)
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Don’t Miss This!