You are given a stick of length n
and an array cuts
where each element represents a position along the stick (from 0 to n
) where you must make a cut. You can make the cuts in any order. After a cut, the stick is split into two smaller sticks and you continue making cuts on those pieces as needed.
The cost of making a cut is equal to the length of the stick being cut at that moment. Your task is to determine the minimum total cost to perform all the cuts.
cuts
is unique and between 1 and n-1
.At first, it may seem like you could just cut the stick in any order and sum up the costs. However, the cost depends on the current length of the stick being cut, and making cuts in different orders can lead to different total costs.
This is a classic example of a problem where greedy choices do not always yield the optimal result. For instance, always making the smallest or largest cut first can lead to suboptimal solutions. Therefore, we need to consider all possible orders of making the cuts and choose the one with the minimum total cost.
This leads us to consider a recursive or dynamic programming approach, where we break the problem into subproblems: what's the minimum cost to cut a stick between two positions, considering only the cuts within that segment?
To solve this problem efficiently, we use dynamic programming. Here's how we break it down:
n
to the cuts
array (representing the start and end of the stick), and sort the array. This way, we can always refer to the left and right boundaries of any stick segment.
dp[i][j]
represent the minimum cost to cut the stick between cuts[i]
and cuts[j]
.
(i, j)
, try making every possible cut k
between i
and j
(i.e., i < k < j
). The cost is the length of the segment (cuts[j] - cuts[i]
) plus the minimum cost to cut the left and right subsegments:
dp[i][j] = min(dp[i][k] + dp[k][j] + cuts[j] - cuts[i])
for all k
in (i, j)
i
and j
(i.e., j - i == 1
), the cost is 0.
This approach ensures we consider all possible ways to order the cuts, but we avoid recomputation by storing results in the DP table.
Let's walk through an example with n = 7
and cuts = [1, 3, 4, 5]
.
cuts = [0, 1, 3, 4, 5, 7]
dp[0][5]
, the minimum cost to cut from position 0 to 7.
dp[0][5]
, is 16 (as per the sample in Leetcode).
class Solution:
def minCost(self, n: int, cuts: List[int]) -> int:
cuts = [0] + sorted(cuts) + [n]
m = len(cuts)
dp = [[0] * m for _ in range(m)]
for l in range(2, m):
for i in range(m - l):
j = i + l
dp[i][j] = min(
dp[i][k] + dp[k][j] for k in range(i + 1, j)
) + cuts[j] - cuts[i]
return dp[0][m - 1]
class Solution {
public:
int minCost(int n, vector<int>& cuts) {
cuts.push_back(0);
cuts.push_back(n);
sort(cuts.begin(), cuts.end());
int m = cuts.size();
vector<vector<int>> dp(m, vector<int>(m, 0));
for (int len = 2; len < m; ++len) {
for (int i = 0; i + len < m; ++i) {
int j = i + len;
dp[i][j] = INT_MAX;
for (int k = i + 1; k < j; ++k) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + cuts[j] - cuts[i]);
}
}
}
return dp[0][m - 1];
}
};
class Solution {
public int minCost(int n, int[] cuts) {
int m = cuts.length + 2;
int[] newCuts = new int[m];
newCuts[0] = 0;
newCuts[m - 1] = n;
for (int i = 0; i < cuts.length; i++) {
newCuts[i + 1] = cuts[i];
}
Arrays.sort(newCuts);
int[][] dp = new int[m][m];
for (int len = 2; len < m; len++) {
for (int i = 0; i + len < m; i++) {
int j = i + len;
dp[i][j] = Integer.MAX_VALUE;
for (int k = i + 1; k < j; k++) {
dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k][j] + newCuts[j] - newCuts[i]);
}
}
}
return dp[0][m - 1];
}
}
var minCost = function(n, cuts) {
cuts.push(0);
cuts.push(n);
cuts.sort((a, b) => a - b);
const m = cuts.length;
const dp = Array.from({length: m}, () => Array(m).fill(0));
for (let len = 2; len < m; ++len) {
for (let i = 0; i + len < m; ++i) {
let j = i + len;
dp[i][j] = Infinity;
for (let k = i + 1; k < j; ++k) {
dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k][j] + cuts[j] - cuts[i]);
}
}
}
return dp[0][m - 1];
};
Brute-force approach: If we tried all possible orders of cuts recursively, the number of possible ways grows exponentially (factorial in the number of cuts), which is infeasible for large inputs.
Dynamic programming approach:
This is efficient enough for the constraints in the problem.
The Minimum Cost to Cut a Stick problem is a classic example of interval dynamic programming. The key insight is to break the problem into subproblems defined by stick segments and solve each using previously computed solutions. By augmenting the cuts array and carefully filling a DP table, we efficiently find the order of cuts that yields the minimum total cost. This approach avoids brute-force enumeration and leverages overlapping subproblems for optimal performance.