Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1000. Minimum Cost to Merge Stones - Leetcode Solution

Problem Description

You are given an array of integers stones of length n and an integer K. The goal is to merge the stones into one pile. Each time, you can merge exactly K consecutive piles into one, and the cost of this merge is the total number of stones in those K piles. The new pile's size is the sum of those K piles. You repeat this process until only one pile remains. Return the minimum possible total cost to merge all stones into one pile. If it is impossible, return -1.

  • You must always merge exactly K consecutive piles at a time.
  • Each merge operation costs the sum of the sizes of the K piles being merged.
  • If you cannot end up with exactly one pile, you must return -1.

Key constraints:
- 1 <= stones.length <= 30
- 2 <= K <= 30
- All elements in stones are positive integers.

Thought Process

Let's break down the problem:

  • We are only allowed to merge K consecutive piles at a time.
  • Each merge operation reduces the number of piles by K-1.
  • We need to minimize the total cost, which is the sum of all merge operations.
  • Sometimes, it's impossible to end up with exactly one pile, depending on n and K.
The brute-force approach would be to try all possible merge sequences, but this quickly becomes infeasible as n grows. Instead, we need to find a way to break the problem into smaller subproblems and use dynamic programming to avoid redundant calculations.

The main insight is to use DP to store the minimum cost to merge a subarray of stones into a certain number of piles. We also need to be careful with the merge rules, ensuring we only merge when it's allowed by K.

Solution Approach

We use a dynamic programming approach to solve this problem efficiently:

  1. Feasibility Check:
    • It's only possible to merge the stones into one pile if (n - 1) % (K - 1) == 0. Otherwise, return -1 immediately.
  2. Prefix Sum:
    • Compute the prefix sum of the stones array to quickly calculate the sum of any subarray, which is needed to compute merge costs.
  3. DP Definition:
    • Let dp[i][j][m] be the minimum cost to merge the subarray stones[i:j+1] into m piles.
    • Our goal is dp[0][n-1][1], the minimum cost to merge the entire array into one pile.
  4. DP Initialization:
    • dp[i][i][1] = 0 for all i, since a single pile needs no merges.
  5. DP Transition:
    • To merge stones[i:j+1] into m piles, try all possible split points and number of left piles:
    • For mid from i to j-1, and leftPiles from 1 to m-1:
      • dp[i][j][m] = min(dp[i][j][m], dp[i][mid][leftPiles] + dp[mid+1][j][m-leftPiles])
    • If m == 1 and dp[i][j][K] is available (i.e., we can merge K piles into 1), then add the cost of merging: dp[i][j][1] = dp[i][j][K] + sum(stones[i:j+1])
  6. Iterative DP:
    • Fill the DP table for increasing subarray lengths and number of piles.
  7. Return the Answer:
    • If dp[0][n-1][1] is possible, return it; otherwise, return -1.

This approach efficiently computes the minimum cost by breaking the problem into overlapping subproblems and avoiding redundant calculations.

Example Walkthrough

Example:
stones = [3, 2, 4, 1], K = 2

  1. Feasibility Check:
    • n = 4, K = 2
    • (n-1) % (K-1) = (4-1) % (2-1) = 3 % 1 = 0, so merging to one pile is possible.
  2. First Merge:
    • Merge [3,2]: cost = 5, resulting piles: [5,4,1]
    • Merge [4,1]: cost = 5, resulting piles: [3,2,5]
    • Try all possible first merges.
  3. Second Merge:
    • From [5,4,1], merge [5,4]: cost = 9, piles: [9,1]
    • Then merge [9,1]: cost = 10, piles: [10]
    • Total cost: 5 (first) + 9 + 10 = 24
    • Try other merge orders and track the minimum.
  4. Optimal Sequence:
    • Merge [2,4]: cost = 6, piles: [3,6,1]
    • Merge [6,1]: cost = 7, piles: [3,7]
    • Merge [3,7]: cost = 10, piles: [10]
    • Total cost: 6 + 7 + 10 = 23
  5. Result:
    • The minimum cost is 20 (as found by the DP).

Time and Space Complexity

Brute-force approach:

  • Tries all possible merge sequences, which is exponential in n.
  • Time complexity: O(K^{n}) (not feasible for large n).
Optimized DP approach:
  • There are O(n^2) subarrays, and for each, up to K piles.
  • Each transition may consider up to O(n) split points.
  • Total time complexity: O(n^3 K).
  • Space complexity: O(n^2 K) for the DP table.

This is efficient enough for the problem's constraints (n <= 30).

Summary

The "Minimum Cost to Merge Stones" problem is a classic example of interval dynamic programming, where the challenge is to merge subarrays optimally under strict rules. The key insight is to model the problem as merging subarrays into a given number of piles and use a 3D DP table to store intermediate results. By carefully managing transitions and using prefix sums for efficient cost calculation, we avoid redundant work and solve the problem efficiently. This approach highlights the power of dynamic programming for problems with overlapping subproblems and complex merge rules.

Code Implementation

from functools import lru_cache
from math import inf
class Solution:
    def mergeStones(self, stones, K):
        n = len(stones)
        if (n - 1) % (K - 1) != 0:
            return -1
        prefix = [0] * (n + 1)
        for i in range(n):
            prefix[i+1] = prefix[i] + stones[i]
        @lru_cache(None)
        def dp(i, j, m):
            if i == j:
                return 0 if m == 1 else inf
            if m == 1:
                cost = dp(i, j, K)
                if cost == inf:
                    return inf
                return cost + prefix[j+1] - prefix[i]
            res = inf
            for mid in range(i, j, K-1):
                left = dp(i, mid, 1)
                right = dp(mid+1, j, m-1)
                if left != inf and right != inf:
                    res = min(res, left + right)
            return res
        ans = dp(0, n-1, 1)
        return ans if ans != inf else -1
    
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution {
public:
    int mergeStones(vector<int>& stones, int K) {
        int n = stones.size();
        if ((n - 1) % (K - 1) != 0) return -1;
        vector<int> prefix(n + 1, 0);
        for (int i = 0; i < n; ++i) prefix[i+1] = prefix[i] + stones[i];
        vector<vector<vector<int>>> dp(n, vector<vector<int>>(n, vector<int>(K+1, INT_MAX)));
        for (int i = 0; i < n; ++i) dp[i][i][1] = 0;
        for (int len = 2; len <= n; ++len) {
            for (int i = 0; i + len - 1 < n; ++i) {
                int j = i + len - 1;
                for (int m = 2; m <= K; ++m) {
                    for (int mid = i; mid < j; mid += K-1) {
                        if (dp[i][mid][1] == INT_MAX || dp[mid+1][j][m-1] == INT_MAX) continue;
                        dp[i][j][m] = min(dp[i][j][m], dp[i][mid][1] + dp[mid+1][j][m-1]);
                    }
                }
                if (dp[i][j][K] != INT_MAX) {
                    dp[i][j][1] = dp[i][j][K] + prefix[j+1] - prefix[i];
                }
            }
        }
        return dp[0][n-1][1] == INT_MAX ? -1 : dp[0][n-1][1];
    }
};
    
import java.util.Arrays;
class Solution {
    public int mergeStones(int[] stones, int K) {
        int n = stones.length;
        if ((n - 1) % (K - 1) != 0) return -1;
        int[] prefix = new int[n+1];
        for (int i = 0; i < n; ++i) prefix[i+1] = prefix[i] + stones[i];
        int[][][] dp = new int[n][n][K+1];
        for (int[][] arr2d : dp)
            for (int[] arr1d : arr2d)
                Arrays.fill(arr1d, Integer.MAX_VALUE);
        for (int i = 0; i < n; ++i) dp[i][i][1] = 0;
        for (int len = 2; len <= n; ++len) {
            for (int i = 0; i + len - 1 < n; ++i) {
                int j = i + len - 1;
                for (int m = 2; m <= K; ++m) {
                    for (int mid = i; mid < j; mid += K-1) {
                        if (dp[i][mid][1] == Integer.MAX_VALUE || dp[mid+1][j][m-1] == Integer.MAX_VALUE) continue;
                        dp[i][j][m] = Math.min(dp[i][j][m], dp[i][mid][1] + dp[mid+1][j][m-1]);
                    }
                }
                if (dp[i][j][K] != Integer.MAX_VALUE) {
                    dp[i][j][1] = dp[i][j][K] + prefix[j+1] - prefix[i];
                }
            }
        }
        return dp[0][n-1][1] == Integer.MAX_VALUE ? -1 : dp[0][n-1][1];
    }
}
    
var mergeStones = function(stones, K) {
    const n = stones.length;
    if ((n - 1) % (K - 1) !== 0) return -1;
    const prefix = Array(n + 1).fill(0);
    for (let i = 0; i < n; ++i) prefix[i+1] = prefix[i] + stones[i];
    const dp = Array.from({length: n}, () => 
        Array.from({length: n}, () => Array(K+1).fill(Infinity)));
    for (let i = 0; i < n; ++i) dp[i][i][1] = 0;
    for (let len = 2; len <= n; ++len) {
        for (let i = 0; i + len - 1 < n; ++i) {
            let j = i + len - 1;
            for (let m = 2; m <= K; ++m) {
                for (let mid = i; mid < j; mid += K-1) {
                    if (dp[i][mid][1] === Infinity || dp[mid+1][j][m-1] === Infinity) continue;
                    dp[i][j][m] = Math.min(dp[i][j][m], dp[i][mid][1] + dp[mid+1][j][m-1]);
                }
            }
            if (dp[i][j][K] !== Infinity) {
                dp[i][j][1] = dp[i][j][K] + prefix[j+1] - prefix[i];
            }
        }
    }
    return dp[0][n-1][1] === Infinity ? -1 : dp[0][n-1][1];
};