2312. Selling Pieces of Wood
Problem Description
You are tasked with cutting a piece of wood to make the maximum amount of money possible. The wood is a rectangle of given height m
and width n
. There's a list of different dimensions that specific pieces can be sold for, and each dimension has an associated price. These dimensions and prices are presented in a 2D integer array 'prices', with each entry being a triplet [h_i, w_i, price_i]
, signifying that a piece of wood of height h_i
and width w_i
can be sold for price_i
dollars.
You can split the original piece of wood vertically or horizontally, creating two smaller pieces at each cut, and you can continue to cut the smaller pieces recursively in the same manner. After cutting the wood into smaller pieces, you can sell some or all of the pieces at the prices listed for their respective dimensions. However, the wood has a grain that must be respected, preventing you from rotating pieces to interchange their dimensions.
You need to find the most profitable way to cut the original m x n
rectangular piece of wood by possibly selling some of the resulting smaller pieces. You can cut and sell the pieces in any quantity as long as they match the dimensions listed in the 'prices' array.
Intuition
The problem requires maximizing the profit from cutting and selling pieces of the original wood. This implies that we need to consider every possible way of cutting the wood to figure out the best strategy. To do this efficiently, we employ a dynamic programming (DP) approach, where we remember the results (memoization) of the best way to cut smaller pieces to avoid recalculating them multiple times.
The intuition behind the solution is to consider each piece of wood that we cut, from the original down to the smallest possible, and check all the ways it can be cut further. We can do this recursively by defining a function dfs(h, w)
that returns the maximum price obtainable from a piece of wood with height h
and width w
.
In this recursive approach, at each step, we try cutting the piece horizontally at all possible heights and vertically at all possible widths. After each cut, we have two smaller pieces, and we recursively calculate the maximum price we can get from both parts combined by calling dfs()
on each of them and summing their values. We do this for all possible cuts and return the maximum sum obtained. We also make sure to check if we can sell the current piece without any cuts, which is why we check the 'prices' dictionary for a direct match.
The use of the cache
decorator from the functools
module in Python allows us to memoize the results of the dfs
function, so we do not recompute the value for the same height and width combination multiple times. The 'd' dictionary is used to map directly from the piece's dimensions to its price for quick look-ups.
The main idea boils down to exploring every cutting possibility and remembering the maximum profit for each piece's dimensions, effectively building a solution from the bottom up.
Learn more about Memoization and Dynamic Programming patterns.
Solution Approach
The solution leverages a top-down dynamic programming approach using recursion with memoization. Here's how the implementation unfolds:
-
Memoization with
cache
Decorator: Thecache
decorator from thefunctools
module allows the functiondfs
to cache the results of subproblems (i.e., smaller wood pieces). This means that once adfs(h, w)
calculation is made, it doesn't need to be recalculated when the same dimensions are encountered again in the recursion. -
Recursive Function
dfs
: The recursive functiondfs(h, w)
computes the maximum price that can be obtained for a piece of wood of heighth
and widthw
. It’s defined within thesellingWood
method so it can access thed
dictionary for prices. -
Dictionary of Prices
d
: A dictionaryd
, whered[h][w]
is the price of a piece of wood of heighth
and widthw
. The dictionary is a defaultdict of dicts so that we can easily refer tod[h].get(w, 0)
, which returns 0 if there is no direct price listed for such a dimension. This hashing structure allows for speedy lookups during the recursion. -
Initialization of Answer
ans
: Each call ofdfs(h, w)
starts by attempting to find if the current dimensions of the wood piece have a price listed. If so, it initializes the value ofans
with this price or 0 if such dimensions are not present ind
. -
Exploring Horizontal and Vertical Cuts: The function then iterates over all possible horizontal cuts (
for i in range(1, h // 2 + 1)
) and vertical cuts (for i in range(1, w // 2 + 1)
), making recursive calls todfs
on the resulting two pieces. It sums the values from both pieces and updatesans
if this sum is greater than the currentans
.- Horizontal cut leads to pieces
dfs(i, w)
anddfs(h - i, w)
- Vertical cut leads to pieces
dfs(h, i)
anddfs(h, w - i)
- Horizontal cut leads to pieces
Each loop iteration effectively simulates one straight cut across the entire dimension of the wood; each possible cut is considered this way.
-
Returning the Maximum Price: After iterating through all potential cuts,
ans
will hold the maximum price obtainable for the dimensionsh
andw
. The function returnsans
, contributing to the recursive construction of the solution. -
Final Return Call: The method
sellingWood
finishes by returning the result ofdfs(m, n)
, which represents the maximum money that can be earned from the original piece of wood with dimensionsm x n
.
The use of these patterns - recursion, memoization, intelligent brute force with pruning of unnecessary recalculations, and a clear dictionary-based structure for instant access to known prices - all come together to create an efficient solution to the problem of maximizing the profit obtained from cutting a piece of wood into salable parts.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a small example to illustrate the solution approach using the dynamic programming method outlined above. Suppose we have a piece of wood of height m = 4
and width n = 4
, and we have the following list of prices for certain dimensions:
prices = [[1, 4, 5], [4, 1, 5], [2, 2, 7], [4, 4, 20]]
Here, a 1 x 4
piece of wood can be sold for 5, a 2 x 2
piece for 20.
Following the dynamic programming approach:
- Setup: First, we initialize our dictionary
d
based on the prices given.
d = {1: {4: 5}, 4: {1: 5, 4: 20}, 2: {2: 7}}
-
Base Cases: If we try to sell the original piece of
4 x 4
without any cuts, we look into the dictionaryd
and findd[4][4] = 20
. So, without making any cuts, we can get a maximum profit of $20. -
Recursion with Cuts:
- Let's try to make horizontal and vertical cuts to see if we can get more profit.
- First, a horizontal cut after 1 unit would give us two pieces:
1 x 4
and3 x 4
. There is a price for1 x 4
, which is $5. We don't have a price for3 x 4
; thus, we need to cut it further. - If we cut the
3 x 4
piece again horizontally at 1, we end up with a1 x 4
(7, making $14 together). - For the vertical cuts, we'll have similar scenarios, and we recursively apply the same logic.
-
Comparing Results:
- In each recursive step, we sum the profit from cutting and compare it with the current maximum. If a cut yields a higher sum, we update our answer
ans
. - In this example, if we sell the
4 x 4
piece as is, we’ll get 5 + 14 = $24.
- In each recursive step, we sum the profit from cutting and compare it with the current maximum. If a cut yields a higher sum, we update our answer
-
Iterating Through All Cuts:
- We iterate over all possible combinations of horizontal and vertical cuts, neither of which will give more than $24, so that’s our maximum possible profit.
-
Memoization:
- As we figure out the best cuts, our function
dfs
uses the@cache
decorator to remember the profits for every smaller piece's dimensions to avoid recalculating them later.
- As we figure out the best cuts, our function
-
Returning the Maximum Profit:
- After testing all possible cuts, we find that the best strategy to maximize profit is to cut the
4 x 4
piece of wood into one1 x 4
piece and a3 x 4
piece, and then further cut the3 x 4
piece into a1 x 4
and a2 x 4
piece, with the2 x 4
being cut into two2 x 2
pieces. This strategy gives us the maximum profit of $24 from the original4 x 4
piece. - The result returned by
dfs(4, 4)
would be $24, which is the maximum money that can be earned from this piece of wood.
- After testing all possible cuts, we find that the best strategy to maximize profit is to cut the
By employing this recursive method with memoization, we've navigated through the various cutting strategies, quickly ignored unprofitable paths, and found the most profitable way to cut and sell the original piece of wood.
Solution Implementation
1from functools import lru_cache # lru_cache is used for memoization.
2from collections import defaultdict # defaultdict for initializing dictionary values.
3
4class Solution:
5 def sellingWood(self, height: int, width: int, price_list: list) -> int:
6 # A memoized helper function to calculate the maximum price.
7 @lru_cache(maxsize=None)
8 def dfs(current_height, current_width):
9 # Initialize the answer for current piece (height x width).
10 # Check if there's a direct price for the current piece and use it as a starting point.
11 max_price = height_price_mapping[current_height].get(current_width, 0)
12
13 # Recursively cut the piece horizontally and compare to find the max price.
14 for i in range(1, current_height // 2 + 1):
15 price_horizontal_cut = dfs(i, current_width) + dfs(current_height - i, current_width)
16 max_price = max(max_price, price_horizontal_cut)
17
18 # Recursively cut the piece vertically and compare to find the max price.
19 for i in range(1, current_width // 2 + 1):
20 price_vertical_cut = dfs(current_height, i) + dfs(current_height, current_width - i)
21 max_price = max(max_price, price_vertical_cut)
22
23 # Return the maximum price that can be obtained from the current piece.
24 return max_price
25
26 # Dictionary to hold heights as keys and another dictionary as value,
27 # which maps widths to prices.
28 height_price_mapping = defaultdict(dict)
29 for height, width, price in price_list:
30 height_price_mapping[height][width] = price
31
32 # Kick off the recursive function with the full size of the wood piece.
33 return dfs(height, width)
34
1import java.util.Arrays;
2
3class Solution {
4 private long[][] memo; // Table to memoize the results of subproblems
5 private int[][] priceTable; // To store the prices for wooden pieces of different sizes
6
7 // Calculates the maximum amount of money that can be earned by selling wooden pieces
8 public long sellingWood(int m, int n, int[][] prices) {
9 priceTable = new int[m + 1][n + 1];
10 memo = new long[m + 1][n + 1];
11
12 // Initialize memo table with -1 to indicate uncalculated subproblems
13 for(long[] row : memo) {
14 Arrays.fill(row, -1);
15 }
16
17 // Fill in the priceTable with given prices
18 for(int[] price : prices) {
19 priceTable[price[0]][price[1]] = price[2];
20 }
21
22 // Start the Depth-First Search (DFS) from the full size
23 return dfs(m, n);
24 }
25
26 // DFS to calculate the maximum price with the given m x n size
27 private long dfs(int height, int width) {
28 // If we have already calculated this subproblem, return the result
29 if(memo[height][width] != -1) {
30 return memo[height][width];
31 }
32
33 // Initialize the maximum price with direct sale price or 0 if not sellable directly
34 long maxPrice = priceTable[height][width];
35
36 // Divide the plate vertically, and try every possible split point
37 for(int i = 1; i <= height / 2; ++i) {
38 maxPrice = Math.max(maxPrice, dfs(i, width) + dfs(height - i, width));
39 }
40
41 // Divide the plate horizontally, and try every possible split point
42 for(int i = 1; i <= width / 2; ++i) {
43 maxPrice = Math.max(maxPrice, dfs(height, i) + dfs(height, width - i));
44 }
45
46 // Save the maximum price in the memo table before returning
47 memo[height][width] = maxPrice;
48 return maxPrice;
49 }
50}
51
1using ll = long long; // Define ll as an alias for long long data type.
2
3class Solution {
4public:
5 long long sellingWood(int height, int width, vector<vector<int>>& prices) {
6 // Initialize a 2D vector for memoization, filled with -1.
7 vector<vector<ll>> memo(height + 1, vector<ll>(width + 1, -1));
8
9 // Initialize a 2D vector for storing the prices with dimensions provided.
10 vector<vector<int>> priceTable(height + 1, vector<int>(width + 1, 0));
11 // Store the given prices in the priceTable.
12 for (auto& price : prices) {
13 priceTable[price[0]][price[1]] = price[2];
14 }
15
16 // Perform Depth-First Search to find the maximum selling price.
17 return dfs(height, width, priceTable, memo);
18 }
19
20 // Helper function to perform the Depth-First Search.
21 ll dfs(int height, int width, vector<vector<int>>& priceTable, vector<vector<ll>>& memo) {
22 // If the result has been computed before, return the stored value.
23 if (memo[height][width] != -1) return memo[height][width];
24
25 // Initialize the answer with the direct price of the current dimension if available, or zero.
26 ll maxPrice = priceTable[height][width];
27
28 // Try splitting the wood vertically and find the maximum price.
29 for (int i = 1; i <= height / 2; ++i) {
30 maxPrice = max(maxPrice, dfs(i, width, priceTable, memo) + dfs(height - i, width, priceTable, memo));
31 }
32
33 // Try splitting the wood horizontally and find the maximum price.
34 for (int i = 1; i <= width / 2; ++i) {
35 maxPrice = max(maxPrice, dfs(height, i, priceTable, memo) + dfs(height, width - i, priceTable, memo));
36 }
37
38 // Store the computed result in the memoization table.
39 memo[height][width] = maxPrice;
40
41 // Return the maximum price.
42 return maxPrice;
43 }
44};
45
1// Define type alias for long long data type.
2type ll = BigInt;
3
4// 2D array type definition for readability
5type Matrix = number[][];
6type BigMatrix = ll[][];
7
8// Define the sellingWood function globally
9function sellingWood(height: number, width: number, prices: Matrix): ll {
10 // Initialize a 2D array for memoization, filled with -1n (BigInt)
11 const memo: BigMatrix = Array.from({length: height + 1}, () =>
12 Array(width + 1).fill(-1n));
13
14 // Initialize a 2D array for storing the prices with given dimensions
15 const priceTable: Matrix = Array.from({length: height + 1}, () =>
16 Array(width + 1).fill(0));
17
18 // Store the given prices in the priceTable
19 prices.forEach((price: number[]) => {
20 priceTable[price[0]][price[1]] = price[2];
21 });
22
23 // Perform Depth-First Search to find the maximum selling price
24 return dfs(height, width, priceTable, memo);
25}
26
27// Helper function to perform the Depth-First Search, defined globally
28function dfs(height: number, width: number, priceTable: Matrix, memo: BigMatrix): ll {
29 // If the result has been computed before, return the stored value
30 if (memo[height][width] !== -1n) return memo[height][width];
31
32 // Initialize the answer with the direct price of the current dimensions if available, or zero
33 let maxPrice: ll = BigInt(priceTable[height][width]);
34
35 // Try splitting the wood vertically and find the maximum price
36 for (let i = 1; i <= Math.floor(height / 2); i++) {
37 maxPrice = max(maxPrice, dfs(i, width, priceTable, memo) + dfs(height - i, width, priceTable, memo));
38 }
39
40 // Try splitting the wood horizontally and find the maximum price
41 for (let i = 1; i <= Math.floor(width / 2); i++) {
42 maxPrice = max(maxPrice, dfs(height, i, priceTable, memo) + dfs(height, width - i, priceTable, memo));
43 }
44
45 // Store the computed result in the memoization array
46 memo[height][width] = maxPrice;
47
48 // Return the maximum price
49 return maxPrice;
50}
51
52// Helper function for max comparison of BigInts
53function max(a: ll, b: ll): ll {
54 return a > b ? a : b;
55}
56
Time and Space Complexity
Time Complexity
The time complexity of the sellingWood
function is mainly determined by the number of states that the dynamic programming will compute and how many operations it performs for each state. Since this function uses a top-down dynamic programming approach with memoization (enabled by the @cache
decorator), each possible state is only computed once.
The number of states here is proportional to the number of possible subproblems, which is the number of ways we can split the height and width of the wood. Each piece of wood can have a height and width ranging from 1
to m
and 1
to n
, respectively, resulting in O(m * n)
states.
At each state (h, w), the function performs two loops: the first one splits the height and the second one splits the width. The former ranges up to h // 2
and the latter up to w // 2
for each state. Therefore, in the worst-case scenario, the function will do h/2 + w/2
comparisons.
Putting it all together, the worst-case time complexity is:
O(m * n * (m/2 + n/2))
However, as this is an overestimation considering the recurrence relation, a tighter bound would likely be better expressed by considering the height and width are divided in various splits, yet the exact time complexity can be difficult to define without a closed form for the recurrence, particularly due to the effects of memoization.
Space Complexity
The space complexity is determined by the maximum size of the call stack due to recursion and the space used for memoization.
-
Each recursive call requires space on the call stack until the base case is reached. In the worst case, the call stack could grow linearly with min(
m
,n
) if only one of the dimensions were always split into a single unit piece along with the maximum other dimension. -
The memoization cache will store a result for each unique (h, w) combination. Therefore, in the worst case, it will store
O(m * n)
entries.
As such, the space complexity of the algorithm is O(m * n)
due to the memoization storage, with an additional consideration for the recursive call stack depth.
In the given code, the d
variable is also a memoization cache, but its entries are only based on the available prices, so it doesn't affect the overall worst-case space complexity, which is still O(m * n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
How does quick sort divide the problem into subproblems?
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
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!