1547. Minimum Cost to Cut a Stick - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Recursion - The brute-force approach explores all possible orderings of cuts using recursive function calls
  • Dynamic Programming (Memoization) - Overlapping subproblems require caching results to avoid redundant computation
  • Interval DP - The problem structure involves optimizing over contiguous segments/intervals
  • Sorting - Cut positions must be sorted to efficiently define subproblems by cut indices

1. Recursion

Intuition

Each cut costs the length of the current stick segment. The order of cuts matters because earlier cuts determine the lengths of segments for later cuts. We need to try all possible orderings of cuts and find the one with minimum total cost. For a segment from position l to r, we try each valid cut point, pay the segment length, then recursively solve the resulting sub-segments.

Algorithm

  1. Define dfs(l, r) to return the minimum cost to make all cuts within the segment [l, r].
  2. Base case: If r - l == 1, no cuts are possible, return 0.
  3. For each cut point c between l and r:
    • Calculate cost as (r - l) + dfs(l, c) + dfs(c, r).
    • Track the minimum across all choices.
  4. Return 0 if no cuts exist in the range, otherwise return the minimum cost.
class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        def dfs(l, r):
            if r - l == 1:
                return 0
            res = float("inf")
            for c in cuts:
                if l < c < r:
                    res = min(res, (r - l) + dfs(l, c) + dfs(c, r))
            res = 0 if res == float("inf") else res
            return res

        return dfs(0, n)

Time & Space Complexity

  • Time complexity: O(mN)O(m ^ N)
  • Space complexity: O(N)O(N) for recursion stack.

Where mm is the size of the cutscuts array, nn is the length of the stick, and N=min(n,m)N = min(n, m).


2. Dynamic Programming (Top-Down) - I

Intuition

The recursive solution has overlapping subproblems since many segment pairs (l, r) are computed multiple times. By caching results for each unique (l, r) pair, we avoid redundant computation. The state depends only on the segment boundaries, not on how we arrived at that segment.

Algorithm

  1. Create a memoization dictionary keyed by (l, r) pairs.
  2. Define dfs(l, r) that returns cached results when available.
  3. For each cut point in the range, compute the cost recursively.
  4. Store and return the minimum cost for each segment.
class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        dp = {}

        def dfs(l, r):
            if r - l == 1:
                return 0
            if (l, r) in dp:
                return dp[(l, r)]

            res = float("inf")
            for c in cuts:
                if l < c < r:
                    res = min(res, (r - l) + dfs(l, c) + dfs(c, r))
            res = 0 if res == float("inf") else res
            dp[(l, r)] = res
            return res

        return dfs(0, n)

Time & Space Complexity

  • Time complexity: O(mN2)O(m * N ^ 2)
  • Space complexity: O(N2)O(N ^ 2)

Where mm is the size of the cutscuts array, nn is the length of the stick, and N=min(n,m)N = min(n, m).


3. Dynamic Programming (Top-Down) - II

Intuition

Instead of using arbitrary segment boundaries, we can index by cut positions. After sorting cuts, we define states by cut indices rather than positions. This reduces the state space from potentially O(n^2) to O(m^2) where m is the number of cuts. We pass indices i and j representing the range of cuts to consider, plus the actual segment boundaries l and r.

Algorithm

  1. Sort the cuts array.
  2. Create a 2D memoization table indexed by cut indices i and j.
  3. Define dfs(l, r, i, j) where i to j are the cut indices within segment [l, r].
  4. Base case: If i > j, no cuts in this range, return 0.
  5. Try each cut index mid from i to j, recursively solve both sides.
  6. Return the minimum cost.
class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        m = len(cuts)
        cuts.sort()
        dp = [[-1] * (m + 1) for _ in range(m + 1)]

        def dfs(l, r, i, j):
            if i > j:
                return 0
            if dp[i][j] != -1:
                return dp[i][j]

            res = float("inf")
            for mid in range(i, j + 1):
                cur = (r - l) + dfs(l, cuts[mid], i, mid - 1) + dfs(cuts[mid], r, mid + 1, j)
                res = min(res, cur)

            dp[i][j] = res
            return res

        return dfs(0, n, 0, m - 1)

Time & Space Complexity

  • Time complexity: O(mlogm+m3)O(m\log m + m ^ 3)
  • Space complexity: O(m2)O(m ^ 2)

Where mm is the size of the cutscuts array and nn is the length of the stick.


4. Dynamic Programming (Bottom-Up)

Intuition

We can solve this iteratively by building up from smaller segments to larger ones. First, add 0 and n as boundary points to the sorted cuts. Then dp[i][j] represents the minimum cost to cut the segment between cuts[i] and cuts[j]. We fill the table by increasing segment length, since longer segments depend on solutions for shorter ones.

Algorithm

  1. Create extended cuts array: [0] + sorted(cuts) + [n].
  2. Initialize a 2D DP table with zeros.
  3. For each segment length from 2 to m+1:
    • For each starting index i:
      • Set j = i + length.
      • Try each cut point mid between i and j.
      • dp[i][j] = min(cuts[j] - cuts[i] + dp[i][mid] + dp[mid][j]).
  4. Return dp[0][m + 1].
class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        m = len(cuts)
        cuts = [0] + sorted(cuts) + [n]
        dp = [[0] * (m + 2) for _ in range(m + 2)]

        for length in range(2, m + 2):
            for i in range(m + 2 - length):
                j = i + length
                dp[i][j] = float("inf")
                for mid in range(i + 1, j):
                    dp[i][j] = min(
                        dp[i][j],
                        cuts[j] - cuts[i] + dp[i][mid] + dp[mid][j]
                    )

        return dp[0][m + 1]

Time & Space Complexity

  • Time complexity: O(mlogm+m3)O(m\log m + m ^ 3)
  • Space complexity: O(m2)O(m ^ 2)

Where mm is the size of the cutscuts array and nn is the length of the stick.


Common Pitfalls

Forgetting to Sort the Cuts Array

The cuts array is not guaranteed to be sorted in the input. Processing cuts in an unsorted order leads to incorrect subproblem definitions because the algorithm assumes cuts are ordered by position when dividing segments.

Not Adding Boundary Points (0 and n)

In the bottom-up DP approach, the segment boundaries 0 and n must be included in the extended cuts array. Without these endpoints, the algorithm cannot properly represent the full stick or calculate costs for segments touching the edges.

Using Wrong Cost Calculation

The cost of a cut equals the length of the current segment being cut, not the position of the cut itself. Confusing cuts[mid] (the cut position) with cuts[j] - cuts[i] (the segment length) produces incorrect results.

Incorrect Base Case Handling

The base case occurs when no cuts exist within a segment, returning cost 0. Returning infinity or failing to detect when i > j in the memoized solution causes the algorithm to miss valid states or compute incorrect minimum values.

State Space Confusion Between Approaches

The recursive solutions using (l, r) positions vs. (i, j) cut indices define different state spaces. Mixing these representations or using position-based states without proper indexing leads to exponential state explosion and memory issues for large stick lengths.