2312. Selling Pieces of Wood
Problem Description
You have a rectangular piece of wood with height m
and width n
. You want to cut this wood into smaller pieces and sell them to maximize your profit.
You're given a 2D array prices
where each element prices[i] = [hi, wi, pricei]
tells you that:
- You can sell a rectangular piece with height
hi
and widthwi
forpricei
dollars - You can sell multiple pieces of the same dimensions
- You don't have to sell all the pieces you cut
Cutting Rules:
- Each cut must go completely across the wood (either full height or full width)
- A vertical cut splits the wood into left and right pieces
- A horizontal cut splits the wood into top and bottom pieces
- You can make as many cuts as you want
- You cannot rotate pieces (a 2×3 piece is different from a 3×2 piece)
Goal: Find the maximum money you can earn by optimally cutting the m × n
wood and selling the resulting pieces according to the given prices.
Example: If you have a 4×6 wood and prices include selling a 2×3 piece for $10, you could potentially cut the wood into multiple 2×3 pieces and sell them. The challenge is finding the optimal cutting strategy that maximizes total revenue.
The solution uses dynamic programming with memoization, where dfs(h, w)
calculates the maximum money from a piece of height h
and width w
. It considers:
- Selling the piece as-is (if a price exists for those dimensions)
- Making horizontal cuts at different positions and recursively solving for both pieces
- Making vertical cuts at different positions and recursively solving for both pieces
The algorithm returns the maximum value among all these options.
Intuition
When faced with this wood cutting problem, we need to find the optimal way to cut a piece of wood to maximize profit. This naturally leads us to think about dynamic programming because:
-
Optimal Substructure: The maximum profit from a piece of wood depends on the maximum profits from smaller pieces. If we cut a 4×6 piece into two 2×6 pieces, the best total profit is the sum of the best profits from each 2×6 piece.
-
Overlapping Subproblems: When cutting different pieces, we'll often encounter the same dimensions repeatedly. For example, cutting a 4×6 piece horizontally in the middle gives two 2×6 pieces, and cutting a 6×6 piece after removing a 2×6 section also gives a 4×6 piece. We don't want to recalculate the same dimensions multiple times.
The key insight is that for any piece of wood with dimensions (h, w)
, we have exactly these options:
- Sell it as-is if there's a price for those exact dimensions
- Cut it horizontally at any position from 1 to
h-1
and maximize the sum of profits from both pieces - Cut it vertically at any position from 1 to
w-1
and maximize the sum of profits from both pieces
We want the maximum among all these choices.
This recursive structure suggests using memoization (top-down DP). Starting with the full m×n
piece, we recursively break it down into smaller pieces, trying all possible cuts. The function dfs(h, w)
represents "what's the maximum money I can get from a piece of height h
and width w
?"
The base case is when we can sell the piece directly (if its dimensions match a price in our list). Otherwise, we try all possible cuts and take the maximum. The @cache
decorator ensures we don't recalculate the same dimensions twice, making our solution efficient.
Learn more about Memoization and Dynamic Programming patterns.
Solution Approach
The implementation uses memoization search (top-down dynamic programming) to solve this problem efficiently.
Data Structure Setup:
First, we create a nested dictionary d
to store the prices for quick lookup. We use defaultdict(dict)
where d[h][w]
gives us the price for a wood piece with height h
and width w
. This allows O(1) price lookups during our calculations.
d = defaultdict(dict)
for h, w, p in prices:
d[h][w] = p
Recursive Function with Memoization:
The core of the solution is the dfs(h, w)
function that calculates the maximum money for a piece with height h
and width w
. The @cache
decorator automatically memoizes results to avoid redundant calculations.
Algorithm Steps:
-
Base Value: Start with
ans = d[h].get(w, 0)
- this checks if we can sell the current piece as-is. If there's no price for these exact dimensions, we start with 0. -
Try Horizontal Cuts: Iterate through all possible horizontal cut positions from 1 to
h // 2 + 1
:for i in range(1, h // 2 + 1): ans = max(ans, dfs(i, w) + dfs(h - i, w))
- Cut at position
i
creates two pieces: one with heighti
and another with heighth - i
- Both pieces maintain the same width
w
- We only iterate to
h // 2 + 1
because cutting at positioni
orh - i
gives the same two pieces
- Cut at position
-
Try Vertical Cuts: Similarly, iterate through all possible vertical cut positions from 1 to
w // 2 + 1
:for i in range(1, w // 2 + 1): ans = max(ans, dfs(h, i) + dfs(h, w - i))
- Cut at position
i
creates two pieces: one with widthi
and another with widthw - i
- Both pieces maintain the same height
h
- Cut at position
-
Return Maximum: The function returns the maximum value among selling as-is and all possible cuts.
Optimization: The solution only iterates to half of each dimension (h // 2 + 1
and w // 2 + 1
) because cutting a 6-unit piece at position 2 or position 4 produces the same two pieces (2-unit and 4-unit), just in different order. This reduces redundant calculations.
The final answer is obtained by calling dfs(m, n)
for the original wood dimensions.
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 3×4 wood piece and the following prices:
- 1×2 piece sells for $3
- 2×2 piece sells for $7
- 3×4 piece sells for $10
We'll trace through dfs(3, 4)
to find the maximum profit.
Step 1: Initial call dfs(3, 4)
- Check if we can sell 3×4 as-is: Yes, for $10. So
ans = 10
- Try horizontal cuts (i from 1 to 2):
- Cut at position 1:
dfs(1, 4) + dfs(2, 4)
- Cut at position 2:
dfs(2, 4) + dfs(1, 4)
(same pieces, different order)
- Cut at position 1:
- Try vertical cuts (i from 1 to 2):
- Cut at position 1:
dfs(3, 1) + dfs(3, 3)
- Cut at position 2:
dfs(3, 2) + dfs(3, 2)
- Cut at position 1:
Step 2: Calculate dfs(1, 4)
- No price for 1×4, so
ans = 0
- No horizontal cuts possible (height is 1)
- Try vertical cuts:
- Cut at 1:
dfs(1, 1) + dfs(1, 3)
= 0 + 0 = 0 - Cut at 2:
dfs(1, 2) + dfs(1, 2)
= 3 + 3 = 6
- Cut at 1:
- Result:
dfs(1, 4) = 6
Step 3: Calculate dfs(2, 4)
- No price for 2×4, so
ans = 0
- Try horizontal cut at 1:
dfs(1, 4) + dfs(1, 4)
= 6 + 6 = 12 - Try vertical cuts:
- Cut at 1:
dfs(2, 1) + dfs(2, 3)
= 0 + 0 = 0 - Cut at 2:
dfs(2, 2) + dfs(2, 2)
= 7 + 7 = 14
- Cut at 1:
- Result:
dfs(2, 4) = 14
Step 4: Back to dfs(3, 4)
- Horizontal cut at 1:
dfs(1, 4) + dfs(2, 4)
= 6 + 14 = 20 - Vertical cut at 2:
dfs(3, 2) + dfs(3, 2)
dfs(3, 2)
: No direct price, try cuts- Horizontal cuts give us combinations of 1×2 and 2×2 pieces
- Best is cutting at position 1:
dfs(1, 2) + dfs(2, 2)
= 3 + 7 = 10
- So vertical cut at 2: 10 + 10 = 20
Final Result: The maximum of:
- Sell as-is: $10
- Best horizontal cut: $20 (splitting into 1×4 and 2×4)
- Best vertical cut: $20 (splitting into two 3×2 pieces)
Therefore, dfs(3, 4) = 20
The optimal strategy is to either:
- Cut horizontally to get 1×4 and 2×4, then cut each into two 1×2 pieces (worth 7 each)
- Cut vertically to get two 3×2 pieces, each optimally cut into one 1×2 and one 2×2 piece
Both strategies yield 10.
Solution Implementation
1from typing import List
2from functools import cache
3from collections import defaultdict
4
5class Solution:
6 def sellingWood(self, m: int, n: int, prices: List[List[int]]) -> int:
7 # Create a nested dictionary to store prices for specific dimensions
8 # price_map[height][width] = price
9 price_map = defaultdict(dict)
10 for height, width, price in prices:
11 price_map[height][width] = price
12
13 @cache
14 def dfs(height: int, width: int) -> int:
15 """
16 Dynamic programming function to find maximum profit for a wood piece.
17
18 Args:
19 height: Current height of the wood piece
20 width: Current width of the wood piece
21
22 Returns:
23 Maximum profit obtainable from this wood piece
24 """
25 # Base case: check if current dimensions have a direct price
26 max_profit = price_map[height].get(width, 0)
27
28 # Try all possible horizontal cuts
29 for cut_height in range(1, height // 2 + 1):
30 # Split into two pieces: (cut_height, width) and (height - cut_height, width)
31 profit_from_horizontal_cut = dfs(cut_height, width) + dfs(height - cut_height, width)
32 max_profit = max(max_profit, profit_from_horizontal_cut)
33
34 # Try all possible vertical cuts
35 for cut_width in range(1, width // 2 + 1):
36 # Split into two pieces: (height, cut_width) and (height, width - cut_width)
37 profit_from_vertical_cut = dfs(height, cut_width) + dfs(height, width - cut_width)
38 max_profit = max(max_profit, profit_from_vertical_cut)
39
40 return max_profit
41
42 # Start the recursive search with the full dimensions
43 return dfs(m, n)
44
1class Solution {
2 // 2D array to store direct prices for specific dimensions
3 private int[][] directPrices;
4 // 2D memoization array to cache computed maximum profits
5 private Long[][] memo;
6
7 /**
8 * Finds the maximum profit from selling a wood piece of dimensions m x n
9 * @param m height of the wood piece
10 * @param n width of the wood piece
11 * @param prices array containing [height, width, price] for specific cuts
12 * @return maximum profit obtainable
13 */
14 public long sellingWood(int m, int n, int[][] prices) {
15 // Initialize the direct prices array
16 directPrices = new int[m + 1][n + 1];
17 // Initialize the memoization array
18 memo = new Long[m + 1][n + 1];
19
20 // Populate direct prices from the given prices array
21 for (int[] price : prices) {
22 int height = price[0];
23 int width = price[1];
24 int value = price[2];
25 directPrices[height][width] = value;
26 }
27
28 // Start the recursive calculation with memoization
29 return dfs(m, n);
30 }
31
32 /**
33 * Recursive helper function to find maximum profit for a piece of given dimensions
34 * @param height current height of the wood piece
35 * @param width current width of the wood piece
36 * @return maximum profit for the given dimensions
37 */
38 private long dfs(int height, int width) {
39 // Check if result is already computed (memoization)
40 if (memo[height][width] != null) {
41 return memo[height][width];
42 }
43
44 // Start with the direct price for this dimension (if available)
45 long maxProfit = directPrices[height][width];
46
47 // Try all possible horizontal cuts
48 for (int cutPosition = 1; cutPosition <= height / 2; cutPosition++) {
49 long profitWithCut = dfs(cutPosition, width) + dfs(height - cutPosition, width);
50 maxProfit = Math.max(maxProfit, profitWithCut);
51 }
52
53 // Try all possible vertical cuts
54 for (int cutPosition = 1; cutPosition <= width / 2; cutPosition++) {
55 long profitWithCut = dfs(height, cutPosition) + dfs(height, width - cutPosition);
56 maxProfit = Math.max(maxProfit, profitWithCut);
57 }
58
59 // Store the result in memo array and return
60 memo[height][width] = maxProfit;
61 return maxProfit;
62 }
63}
64
1class Solution {
2public:
3 long long sellingWood(int m, int n, vector<vector<int>>& prices) {
4 // Type alias for long long
5 using ll = long long;
6
7 // dp[i][j] stores the maximum profit from selling a piece of wood with dimensions i x j
8 // -1 indicates uncomputed state
9 ll dp[m + 1][n + 1];
10
11 // directPrice[i][j] stores the direct selling price of a piece with dimensions i x j
12 // 0 if no direct price is available
13 int directPrice[m + 1][n + 1];
14
15 // Initialize dp array with -1 (uncomputed) and directPrice with 0
16 memset(dp, -1, sizeof(dp));
17 memset(directPrice, 0, sizeof(directPrice));
18
19 // Populate direct prices from the input
20 for (auto& price : prices) {
21 int height = price[0];
22 int width = price[1];
23 int value = price[2];
24 directPrice[height][width] = value;
25 }
26
27 // Recursive function with memoization to find maximum profit
28 function<ll(int, int)> calculateMaxProfit = [&](int height, int width) -> ll {
29 // Return cached result if already computed
30 if (dp[height][width] != -1) {
31 return dp[height][width];
32 }
33
34 // Initialize with direct selling price (could be 0 if not available)
35 ll maxProfit = directPrice[height][width];
36
37 // Try all possible horizontal cuts
38 // Cut the wood into two pieces: (i x width) and ((height - i) x width)
39 for (int i = 1; i <= height / 2; ++i) {
40 ll profitWithCut = calculateMaxProfit(i, width) + calculateMaxProfit(height - i, width);
41 maxProfit = max(maxProfit, profitWithCut);
42 }
43
44 // Try all possible vertical cuts
45 // Cut the wood into two pieces: (height x i) and (height x (width - i))
46 for (int i = 1; i <= width / 2; ++i) {
47 ll profitWithCut = calculateMaxProfit(height, i) + calculateMaxProfit(height, width - i);
48 maxProfit = max(maxProfit, profitWithCut);
49 }
50
51 // Memoize and return the result
52 return dp[height][width] = maxProfit;
53 };
54
55 // Calculate and return the maximum profit for the original m x n piece
56 return calculateMaxProfit(m, n);
57 }
58};
59
1/**
2 * Calculates the maximum profit from selling a wooden board of dimensions m x n
3 * @param m - Height of the wooden board
4 * @param n - Width of the wooden board
5 * @param prices - Array of [height, width, price] representing prices for specific board dimensions
6 * @returns Maximum profit obtainable
7 */
8function sellingWood(m: number, n: number, prices: number[][]): number {
9 // Memoization table to store computed results for each dimension
10 // -1 indicates uncomputed state
11 const memoTable: number[][] = Array.from(
12 { length: m + 1 },
13 () => Array(n + 1).fill(-1)
14 );
15
16 // Direct price lookup table for specific board dimensions
17 const priceTable: number[][] = Array.from(
18 { length: m + 1 },
19 () => Array(n + 1).fill(0)
20 );
21
22 // Populate the price table with given prices
23 for (const [height, width, price] of prices) {
24 priceTable[height][width] = price;
25 }
26
27 /**
28 * Recursive function with memoization to find maximum profit for a board of given dimensions
29 * @param height - Current board height
30 * @param width - Current board width
31 * @returns Maximum profit for the given dimensions
32 */
33 const calculateMaxProfit = (height: number, width: number): number => {
34 // Return memoized result if already computed
35 if (memoTable[height][width] !== -1) {
36 return memoTable[height][width];
37 }
38
39 // Initialize with the direct price for this dimension (if available)
40 let maxProfit = priceTable[height][width];
41
42 // Try all possible horizontal cuts
43 for (let cutPosition = 1; cutPosition <= Math.floor(height / 2); cutPosition++) {
44 const topPiece = calculateMaxProfit(cutPosition, width);
45 const bottomPiece = calculateMaxProfit(height - cutPosition, width);
46 maxProfit = Math.max(maxProfit, topPiece + bottomPiece);
47 }
48
49 // Try all possible vertical cuts
50 for (let cutPosition = 1; cutPosition <= Math.floor(width / 2); cutPosition++) {
51 const leftPiece = calculateMaxProfit(height, cutPosition);
52 const rightPiece = calculateMaxProfit(height, width - cutPosition);
53 maxProfit = Math.max(maxProfit, leftPiece + rightPiece);
54 }
55
56 // Store and return the computed result
57 memoTable[height][width] = maxProfit;
58 return maxProfit;
59 };
60
61 // Start the recursive calculation from the full board dimensions
62 return calculateMaxProfit(m, n);
63}
64
Time and Space Complexity
The time complexity is O(m × n × (m + n) + p)
, where:
p
is the length of the prices array (for preprocessing the dictionary)m × n
represents the number of unique subproblems (all possible height-width combinations)- For each subproblem
(h, w)
, we iterate through:h/2
possible horizontal cuts (splitting by height)w/2
possible vertical cuts (splitting by width)- Total iterations per subproblem:
O(h + w)
, which in worst case isO(m + n)
The space complexity is O(m × n)
, which accounts for:
- The memoization cache storing results for at most
m × n
unique(height, width)
pairs - The dictionary
d
storing the prices, which takesO(p)
space butp ≤ m × n
- The recursion stack depth, which is at most
O(m + n)
in the worst case
Therefore, the overall space complexity is dominated by the cache storage of O(m × n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Inefficient Loop Boundaries Leading to Redundant Calculations
The Pitfall: A common mistake is iterating through all possible cut positions instead of just half:
# Inefficient approach - causes redundant calculations
for cut_height in range(1, height): # Goes from 1 to height-1
max_profit = max(max_profit, dfs(cut_height, width) + dfs(height - cut_height, width))
This creates duplicate work because cutting at position i
or position height - i
produces the same two pieces. For example, cutting a height-6 piece at position 2 or position 4 both create pieces of height 2 and 4.
The Solution: Only iterate to the midpoint:
for cut_height in range(1, height // 2 + 1):
max_profit = max(max_profit, dfs(cut_height, width) + dfs(height - cut_height, width))
2. Incorrect Price Lookup Data Structure
The Pitfall: Using a simple dictionary with tuple keys might seem intuitive but can lead to verbose code:
# Less optimal approach price_dict = {} for h, w, p in prices: price_dict[(h, w)] = p # Then in dfs function: max_profit = price_dict.get((height, width), 0)
The Solution: Use nested dictionaries for cleaner, more readable lookups:
price_map = defaultdict(dict)
for height, width, price in prices:
price_map[height][width] = price
# In dfs function:
max_profit = price_map[height].get(width, 0)
3. Forgetting to Consider Not Cutting At All
The Pitfall:
Starting with max_profit = 0
and only considering cuts, forgetting that the optimal solution might be to sell the piece as-is:
def dfs(height, width):
max_profit = 0 # Wrong! Missing the direct sale option
# Only considering cuts
for cut_height in range(1, height // 2 + 1):
max_profit = max(max_profit, dfs(cut_height, width) + dfs(height - cut_height, width))
# ...
The Solution: Always initialize with the direct sale price (if available):
def dfs(height, width):
# Start with the price of selling this piece as-is
max_profit = price_map[height].get(width, 0)
# Then consider cuts...
4. Missing or Incorrect Memoization
The Pitfall: Implementing manual memoization incorrectly or forgetting it entirely:
# Without memoization - exponential time complexity!
def dfs(height, width):
# recursive calls without caching
# Or incorrect manual memoization
memo = {}
def dfs(height, width):
if (height, width) in memo:
return memo[(height, width)]
# ... calculation ...
# Forgetting to store result: memo[(height, width)] = result
return result # Missing memo storage!
The Solution:
Use Python's @cache
decorator for automatic, correct memoization:
from functools import cache
@cache
def dfs(height, width):
# Automatically memoized
5. Off-by-One Errors in Loop Ranges
The Pitfall: Using incorrect range boundaries that either miss valid cuts or create invalid pieces:
# Wrong - misses the case where we cut exactly in half
for cut_height in range(1, height // 2): # Should be height // 2 + 1
# Wrong - creates a piece with 0 height
for cut_height in range(0, height // 2 + 1): # Should start from 1
The Solution: Ensure cuts start from 1 (minimum piece size) and go up to and including the midpoint:
for cut_height in range(1, height // 2 + 1):
# Correctly handles all unique partitions
Which data structure is used to implement recursion?
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
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
Want a Structured Path to Master System Design Too? Don’t Miss This!