LeetCode 2312 - Selling Pieces of Wood
The problem presents a rectangular piece of wood of size m x n and a list of specific prices for certain subrectangles. Each entry in the prices array, [hi, wi, pricei], indicates that a piece of wood of height hi and width wi can be sold for pricei dollars.
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming, Memoization
Solution
Problem Understanding
The problem presents a rectangular piece of wood of size m x n and a list of specific prices for certain subrectangles. Each entry in the prices array, [hi, wi, pricei], indicates that a piece of wood of height hi and width wi can be sold for pricei dollars. You may make any number of horizontal or vertical cuts across the full height or width of a piece to divide it into smaller pieces. The key point is that the wood pieces cannot be rotated, meaning a 2 x 3 piece is not equivalent to a 3 x 2 piece. After cutting, you may sell any pieces according to the price list, and multiple identical pieces can be sold.
The goal is to maximize the total money obtained from a piece of wood of size m x n.
Inputs are bounded by 1 <= m, n <= 200 and up to 2 * 10^4 price entries. This indicates that a brute-force recursive approach without caching will likely be too slow due to exponential splitting possibilities. The problem guarantees that each (hi, wi) pair is distinct and all prices are positive integers.
Important edge considerations include: if a piece matches a priced shape exactly, selling it may be optimal; small or prime dimensions may limit cutting options; and some pieces may not be sold directly but could be used as part of optimal cuts.
Approaches
A brute-force approach would recursively try every possible vertical and horizontal cut for a piece of wood, compute the maximum revenue for each resulting piece, and combine the results. While correct, this approach quickly becomes infeasible due to exponential combinations, especially since m and n can be up to 200.
The key insight for optimization is that the problem exhibits overlapping subproblems. The maximum revenue for a subrectangle depends only on its dimensions and the price list. Therefore, we can use dynamic programming with memoization, storing the maximum revenue for each (height, width) subrectangle. By doing this, each subproblem is computed at most once, drastically reducing redundant computation.
We maintain a memoization table mapping (height, width) to the maximum obtainable price. For each rectangle, we first consider if selling it directly is profitable and then recursively try all horizontal and vertical cuts, combining the subresults to find the maximum.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^(m+n)) | O(1) | Exponential recursive splitting; infeasible for m, n up to 200 |
| Optimal (DP + Memoization) | O(m * n * (m + n)) | O(m * n + p) | Computes each subrectangle once; tries all horizontal and vertical cuts; p = number of prices |
Algorithm Walkthrough
- Preprocess Prices: Store the prices in a hash map or dictionary keyed by
(height, width)for constant-time lookups. This allows quick access to the price of a rectangle if it matches a listed shape. - Initialize Memoization Table: Create a dictionary
dpwheredp[(h, w)]will store the maximum revenue obtainable for a rectangle of heighthand widthw. - Define Recursive Function: Implement a function
dfs(h, w)that returns the maximum revenue for a rectangle of dimensionsh x w. - Base Case: If
(h, w)is in the price map, set the initial maximum revenue toprice_map[(h, w)]. Otherwise, start with zero. - Try Horizontal Cuts: For every possible horizontal cut
ifrom1toh-1, split the rectangle into(i x w)and(h-i x w), and recursively compute their revenues. Update the maximum revenue. - Try Vertical Cuts: For every possible vertical cut
jfrom1tow-1, split the rectangle into(h x j)and(h x (w-j)), recursively compute revenues, and update the maximum revenue. - Memoize and Return: Store the computed maximum revenue in
dp[(h, w)]and return it. - Final Call: Start by calling
dfs(m, n)to compute the maximum revenue for the original piece of wood.
Why it works: The recursion guarantees that all possible cutting combinations are considered. Memoization ensures each subrectangle is only solved once, providing an efficient solution. Since the price for a rectangle depends solely on its dimensions, the subproblem decomposition satisfies optimal substructure.
Python Solution
from typing import List, Tuple
from functools import lru_cache
class Solution:
def sellingWood(self, m: int, n: int, prices: List[List[int]]) -> int:
price_map = {(h, w): price for h, w, price in prices}
from functools import lru_cache
@lru_cache(None)
def dfs(h: int, w: int) -> int:
max_val = price_map.get((h, w), 0)
# Try horizontal cuts
for i in range(1, h):
max_val = max(max_val, dfs(i, w) + dfs(h - i, w))
# Try vertical cuts
for j in range(1, w):
max_val = max(max_val, dfs(h, j) + dfs(h, w - j))
return max_val
return dfs(m, n)
Explanation: We first map the prices for O(1) access. The dfs function recursively explores all cutting options for a rectangle. The use of lru_cache implements memoization, preventing recomputation of subrectangles. The function returns the maximum possible revenue for the requested dimensions.
Go Solution
func sellingWood(m int, n int, prices [][]int) int64 {
priceMap := make(map[[2]int]int64)
for _, p := range prices {
priceMap[[2]int{p[0], p[1]}] = int64(p[2])
}
memo := make(map[[2]int]int64)
var dfs func(h, w int) int64
dfs = func(h, w int) int64 {
key := [2]int{h, w}
if val, ok := memo[key]; ok {
return val
}
maxVal := priceMap[key]
for i := 1; i < h; i++ {
maxVal = max(maxVal, dfs(i, w)+dfs(h-i, w))
}
for j := 1; j < w; j++ {
maxVal = max(maxVal, dfs(h, j)+dfs(h, w-j))
}
memo[key] = maxVal
return maxVal
}
var max func(a, b int64) int64
max = func(a, b int64) int64 {
if a > b {
return a
}
return b
}
return dfs(m, n)
}
Go-specific Notes: We use a map with array keys [2]int to represent rectangle dimensions. All integers are cast to int64 to avoid overflow. Memoization is implemented explicitly with the memo map.
Worked Examples
Example 1: m = 3, n = 5, prices = [[1,4,2],[2,2,7],[2,1,3]]
| Step | Operation | Revenue Calculation | Max Revenue |
|---|---|---|---|
| Initial | No cuts | max(0, prices.get(3,5)=0) | 0 |
| Horizontal cut at 2 | Split 2x5 + 1x5 | dfs(2,5)+dfs(1,5) | 19 |
| Vertical cut at 2 | Split 3x2 + 3x3 | dfs(3,2)+dfs(3,3) | 18 |
| Horizontal cut at 1 | Split 1x5 + 2x5 | dfs(1,5)+dfs(2,5) | 19 |
| Vertical cut at 4 | Split 3x4 + 3x1 | dfs(3,4)+dfs(3,1) | 18 |
Maximum revenue returned: 19
Example 2: m = 4, n = 6, prices = [[3,2,10],[1,4,2],[4,1,3]]
Following similar splitting steps, the maximum revenue obtained is 32.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m * n * (m + n)) | Each subrectangle is computed once. For each subrectangle, we try at most h-1 horizontal and w-1 vertical cuts. |
| Space | O(m * n + p) | Memoization table stores results for each subrectangle, plus a map of prices with p entries. |
The reasoning: memoization ensures each (h, w) is solved only once, and trying all cuts takes linear time in height and width.
Test Cases
# Provided examples
assert Solution().sellingWood(3, 5, [[1,4,2],[2,2,7],[2,1,3]]) == 19 # Example 1
assert Solution().sellingWood(4, 6, [[3,2,10],[1,4,