Topics › Dynamic Programming › DP on Subsequences
Rod Cutting Problem - (DP - 24)
Java source ↗ Edit this article ↗
Jump to code:
Problem
You have a rod of length n and an array price of length n, where price[i] is the selling price of a piece of length i + 1. You may cut the rod into any number of pieces of integer length (or not cut it at all), and you may cut the same length as many times as you like. Return the maximum total price you can get for the pieces.
Constraints: 1 ≤ n ≤ 1000, 0 ≤ price[i] ≤ 10^5. The answer is at most 1000 × 10^5 = 10^8, so it fits in an int.
Example 1
price = [2, 5, 7, 8, 10] // lengths 1..5, rod length 5
answer = 12 // 2 + 2 + 1 sells for 5 + 5 + 2 (2 + 3 also gives 5 + 7)
Example 2
price = [3, 5, 8, 9, 10, 17, 17, 20]
answer = 24 // eight pieces of length 1: 8 × 3
Recognising the pattern
Think of every piece length L = i + 1 as an item with weight L and value price[i], and of the rod as a knapsack of capacity n. Choosing pieces whose lengths add up to n is choosing items whose weights add up to the capacity. Two details pin down which knapsack this is:
- The same length may be cut many times, so every item has an unlimited supply. This is the unbounded knapsack.
- The pieces must cover the rod exactly, but because a length-1 piece always exists and prices are non-negative, any leftover can be sold as length-1 pieces. So “exactly
n” and “at mostn” give the same answer, and the base case can simply sell the remainder in unit pieces.
Greedy does not work. With price = [0, 5, 8, 0] the best price per unit is length 3 (8 / 3 ≈ 2.67), but cutting 3 + 1 earns only 8, while 2 + 2 earns 10. The best choice for one piece depends on what the rest of the rod can earn, which is exactly what DP tracks.
Approach 1: Recursion
Intuition. Let f(ind, len) be the best price for a rod of length len when only piece lengths 1 .. ind + 1 are allowed. Look at the largest allowed length, ind + 1:
- Not take it: then the answer is
f(ind - 1, len). - Take one piece of it (only if it fits): earn
price[ind]and solvef(ind, len - (ind + 1)). The index stays atind, notind - 1, because the same length may be cut again. That single difference is what turns 0/1 knapsack into unbounded knapsack.
When ind == 0 only length 1 is allowed, so the rod is cut into len unit pieces worth len * price[0].
Deciding lengths in a fixed order (largest index first) means each multiset of pieces is explored once; the order in which cuts are made does not matter for the total.
Algorithm.
- If
ind == 0, returnlen * price[0]. notTake = f(ind - 1, len).- If
ind + 1 ≤ len,take = price[ind] + f(ind, len - (ind + 1)); otherwisetake = -∞. - Return
max(take, notTake). The answer isf(n - 1, n).
static int recursive(int[] price) {
int n = price.length;
return solve(n - 1, n, price);
}
/** Best value for a rod of length len using only piece lengths 1 .. ind + 1. */
static int solve(int ind, int len, int[] price) {
if (ind == 0) return len * price[0]; // only length-1 pieces remain: cut len of them
int notTake = solve(ind - 1, len, price);
int take = Integer.MIN_VALUE;
int pieceLen = ind + 1;
if (pieceLen <= len) take = price[ind] + solve(ind, len - pieceLen, price); // stay at ind: unlimited supply
return Math.max(take, notTake);
}
Complexity. Time is exponential: every call branches twice, and the take branch does not decrease ind, so the tree can be larger than 2^n. Space O(n) for the stack: every call either lowers ind or lowers len by at least 2.
Approach 2: Memoization
Intuition. The state is just the pair (ind, len), with n choices for ind and n + 1 for len. Different cut sequences reach the same state constantly (cutting 2 then 3 and cutting 3 then 2 both leave len - 5), so caching each state’s answer removes all repeated work. Prices are non-negative, so -1 safely marks “not computed”.
Algorithm.
- Create
dp[n][n + 1]filled with-1. - Same recursion; return
dp[ind][len]if already set, otherwise compute it, store it, and return it.
static int memoization(int[] price) {
int n = price.length;
int[][] dp = new int[n][n + 1];
for (int[] row : dp) Arrays.fill(row, -1);
return memo(n - 1, n, price, dp);
}
static int memo(int ind, int len, int[] price, int[][] dp) {
if (ind == 0) return len * price[0];
if (dp[ind][len] != -1) return dp[ind][len];
int notTake = memo(ind - 1, len, price, dp);
int take = Integer.MIN_VALUE;
int pieceLen = ind + 1;
if (pieceLen <= len) take = price[ind] + memo(ind, len - pieceLen, price, dp);
return dp[ind][len] = Math.max(take, notTake);
}
Complexity. Time O(n^2): n × (n + 1) states, each doing constant work. Space O(n^2) for the table plus O(n) stack.
Approach 3: Tabulation
Intuition. f(ind, len) reads row ind - 1 (not take) and row ind at a smaller length (take). So fill rows in increasing ind, and within a row fill lengths from small to large. The base row ind = 0 is len * price[0] for every len.
Algorithm.
dp[0][len] = len * price[0]forlen = 0 .. n.- For
ind = 1 .. n - 1andlen = 0 .. n:notTake = dp[ind - 1][len]; if the piece fits,take = price[ind] + dp[ind][len - (ind + 1)]; store the larger. - Return
dp[n - 1][n].
static int tabulation(int[] price) {
int n = price.length;
int[][] dp = new int[n][n + 1];
for (int len = 0; len <= n; len++) dp[0][len] = len * price[0]; // base case: only length-1 pieces
for (int ind = 1; ind < n; ind++) {
int pieceLen = ind + 1;
for (int len = 0; len <= n; len++) {
int notTake = dp[ind - 1][len];
int take = Integer.MIN_VALUE;
if (pieceLen <= len) take = price[ind] + dp[ind][len - pieceLen];
dp[ind][len] = Math.max(take, notTake);
}
}
return dp[n - 1][n];
}
Complexity. Time O(n^2), space O(n^2).
Approach 4: Space optimized
Intuition. The usual next step keeps two rows, prev and cur. Here we can go further and keep one array. Look at what the update for best[len] needs:
notTakeis the previous row at the samelen. Before we overwritebest[len], it still holds exactly that value.takeneeds the current row atlen - pieceLen. Moving left to right, that cell has already been updated in this pass, so it holds the current row’s value.
So an in-place left-to-right sweep computes the recurrence exactly. (In 0/1 knapsack, take needs the previous row instead, which is why that version must sweep right to left.) Lengths smaller than pieceLen cannot use the piece and keep their value, so the loop starts at pieceLen.
Algorithm.
best[len] = len * price[0]for alllen.- For each
ind = 1 .. n - 1, forlen = ind + 1 .. n:best[len] = max(best[len], price[ind] + best[len - (ind + 1)]). - Return
best[n].
static int spaceOptimized(int[] price) {
int n = price.length;
int[] best = new int[n + 1];
for (int len = 0; len <= n; len++) best[len] = len * price[0];
for (int ind = 1; ind < n; ind++) {
int pieceLen = ind + 1;
for (int len = pieceLen; len <= n; len++) {
best[len] = Math.max(best[len], price[ind] + best[len - pieceLen]); // best[len - pieceLen] is already this row
}
}
return best[n];
}
Dry run on Example 1, price = [2, 5, 7, 8, 10]:
| after piece length | best[0..5] |
|---|---|
| 1 (price 2) | [0, 2, 4, 6, 8, 10] |
| 2 (price 5) | [0, 2, 5, 7, 10, 12] |
| 3 (price 7) | [0, 2, 5, 7, 10, 12] |
| 4 (price 8) | [0, 2, 5, 7, 10, 12] |
| 5 (price 10) | [0, 2, 5, 7, 10, 12] |
During the length-2 pass, best[4] = max(8, 5 + best[2]) = 10 uses best[2] = 5, which was set moments earlier in the same pass; that is two length-2 pieces. The answer is best[5] = 12.
Complexity. Time O(n^2), space O(n).
Edge cases and pitfalls
- Rod of length 1. The only option is one unit piece,
price[0]. The base case covers it without special code. - Index vs length.
price[i]belongs to lengthi + 1. Forgetting the+ 1subtracts the wrong amount fromlenand silently gives wrong answers. - Staying at
indon take. Moving toind - 1after taking would allow each length above 1 only once, which is the 0/1 knapsack recurrence. Onprice = [1, 10, 1, 1, 1]it would return 13 instead of 21, because it could no longer cut two length-2 pieces. - Loop direction in the 1D version. Right to left would read the previous row for
takeand again forbid reuse. - Greedy by price per unit. Fails, as shown with
[0, 5, 8, 0]. The Java tests include that case. - Zero prices. All-zero prices give 0;
-1is still a safe memo sentinel because no real answer is negative. - Testing. Besides the fixed cases, 200 seeded random price lists of length up to 12 are run through all four methods.
Related problems
- Unbounded knapsack is the same recurrence with arbitrary weights and a separate capacity.
- Coin Change 2 (DP - 22) counts combinations instead of maximising, with the same left-to-right single array.
- Minimum Coins (DP - 20) minimises the number of items needed to fill the capacity exactly.
- 0 and 1 Knapsack allows each item once, which flips the 1D loop direction.
- Rod cutting problem is the same problem as listed in the SDE sheet.