Facebook Pixel

1000. Minimum Cost to Merge Stones

Problem Description

You are given n piles of stones arranged in a row, where the i-th pile contains stones[i] stones.

In each move, you must merge exactly k consecutive piles into a single pile. The cost of this merge operation equals the total number of stones in those k piles being merged.

Your goal is to find the minimum total cost to merge all piles into one single pile. If it's impossible to merge all piles into one, return -1.

For example, if you have stones [3, 2, 4, 1] and k = 2:

  • You could merge piles at positions 1 and 2 (stones 3 and 2) with cost 5, resulting in [5, 4, 1]
  • Then merge positions 1 and 2 (stones 5 and 4) with cost 9, resulting in [9, 1]
  • Finally merge the last two piles with cost 10, resulting in [10]
  • Total cost would be 5 + 9 + 10 = 24

The key constraint is that you can only merge exactly k consecutive piles at a time - no more, no less. This means that not all configurations can be reduced to a single pile. For instance, if you have 4 piles and k = 3, you can merge 3 piles to get 2 piles, but then you cannot merge 2 piles when k = 3, making it impossible to reach a single pile.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Let's think about when it's possible to merge all stones into one pile. Each merge operation reduces the number of piles by K - 1 (we take K piles and produce 1 pile). Starting with n piles and ending with 1 pile, we need to reduce by n - 1 piles total. This means we need (n - 1) / (K - 1) merge operations. For this to be a whole number, (n - 1) % (K - 1) must equal 0. If not, it's impossible to merge all piles into one.

Now for the minimum cost calculation. The key insight is that merging stones is a range-based problem - we're always working with consecutive piles. This suggests using dynamic programming where we consider all possible ways to merge subarrays of stones.

Let's define f[i][j][m] as the minimum cost to merge stones from pile i to pile j into exactly m piles. Why do we need the third dimension m? Because intermediate states matter - sometimes we need to first merge subsections into a specific number of piles before we can perform the final merge.

The recurrence relation works as follows:

  • To merge stones [i, j] into m piles, we can split the range at some point h and merge [i, h] into 1 pile and [h+1, j] into m-1 piles
  • To merge stones [i, j] into 1 pile, we first need to merge them into K piles (which costs f[i][j][K]), then perform one final merge of those K piles (which costs the sum of all stones in range [i, j])

The base case is f[i][i][1] = 0 (a single pile is already 1 pile with no cost).

We use prefix sums to quickly calculate the total stones in any range [i, j], which represents the cost of the final merge operation when combining K piles into 1.

The answer will be f[1][n][1] - the minimum cost to merge all stones from position 1 to n into exactly 1 pile.

Learn more about Dynamic Programming and Prefix Sum patterns.

Solution Approach

The implementation follows the dynamic programming approach with these key components:

1. Impossibility Check:

if (n - 1) % (K - 1):
    return -1

First, we verify if merging is possible. If (n - 1) is not divisible by (K - 1), we cannot merge all piles into one.

2. Prefix Sum Array:

s = list(accumulate(stones, initial=0))

We create a prefix sum array to quickly calculate the sum of stones in any range [i, j] using s[j] - s[i - 1]. The initial=0 ensures our prefix sum is 1-indexed for easier calculation.

3. DP Table Initialization:

f = [[[inf] * (K + 1) for _ in range(n + 1)] for _ in range(n + 1)]
for i in range(1, n + 1):
    f[i][i][1] = 0

We create a 3D DP table f[i][j][k] where:

  • i is the starting position (1-indexed)
  • j is the ending position (1-indexed)
  • k is the number of piles to merge into

Initialize all values to infinity except f[i][i][1] = 0 (single pile requires no merging).

4. Main DP Loop:

for l in range(2, n + 1):  # length of subarray
    for i in range(1, n - l + 2):  # starting position
        j = i + l - 1  # ending position

We iterate through all possible subarray lengths from 2 to n, and for each length, we consider all valid starting positions.

5. Calculate Minimum Cost for k Piles:

for k in range(1, K + 1):
    for h in range(i, j):
        f[i][j][k] = min(f[i][j][k], f[i][h][1] + f[h + 1][j][k - 1])

For merging range [i, j] into k piles, we try all possible split points h. We merge [i, h] into 1 pile and [h+1, j] into k-1 piles, taking the minimum cost among all splits.

6. Final Merge to One Pile:

f[i][j][1] = f[i][j][K] + s[j] - s[i - 1]

To merge range [i, j] into 1 pile, we first merge it into K piles (cost: f[i][j][K]), then perform the final merge of those K piles into 1. The cost of this final merge equals the total stones in the range, calculated as s[j] - s[i - 1].

7. Return Result:

return f[1][n][1]

The answer is the minimum cost to merge all stones (from position 1 to n) into exactly 1 pile.

The time complexity is O(n³ * K) and space complexity is O(n² * K), where n is the number of stone piles.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with stones = [3, 2, 4, 1] and K = 2.

Step 1: Check if merging is possible

  • We have n = 4 piles
  • Check: (n - 1) % (K - 1) = (4 - 1) % (2 - 1) = 3 % 1 = 0
  • Since the remainder is 0, we can merge all piles into one

Step 2: Build prefix sum array

  • s = [0, 3, 5, 9, 10]
  • This helps us quickly calculate range sums. For example, sum of stones[2:4] = s[4] - s[1] = 10 - 3 = 7

Step 3: Initialize DP table

  • Create 3D array f[i][j][m] initialized to infinity
  • Set base cases: f[1][1][1] = 0, f[2][2][1] = 0, f[3][3][1] = 0, f[4][4][1] = 0
  • (Each single pile is already 1 pile with zero cost)

Step 4: Fill DP table for increasing lengths

Length 2 subarrays:

  • For [3, 2] (i=1, j=2):
    • To get 2 piles: f[1][2][2] = f[1][1][1] + f[2][2][1] = 0 + 0 = 0
    • To get 1 pile: f[1][2][1] = f[1][2][2] + (s[2] - s[0]) = 0 + 5 = 5
  • For [2, 4] (i=2, j=3):
    • To get 2 piles: f[2][3][2] = 0
    • To get 1 pile: f[2][3][1] = 0 + 6 = 6
  • For [4, 1] (i=3, j=4):
    • To get 2 piles: f[3][4][2] = 0
    • To get 1 pile: f[3][4][1] = 0 + 5 = 5

Length 3 subarrays:

  • For [3, 2, 4] (i=1, j=3):
    • To get 2 piles (split at h=1): f[1][3][2] = f[1][1][1] + f[2][3][1] = 0 + 6 = 6
    • To get 2 piles (split at h=2): f[1][3][2] = min(6, f[1][2][1] + f[3][3][1]) = min(6, 5 + 0) = 5
    • To get 1 pile: f[1][3][1] = f[1][3][2] + (s[3] - s[0]) = 5 + 9 = 14
  • For [2, 4, 1] (i=2, j=4):
    • To get 2 piles: f[2][4][2] = min(f[2][2][1] + f[3][4][1], f[2][3][1] + f[4][4][1]) = min(0 + 5, 6 + 0) = 5
    • To get 1 pile: f[2][4][1] = 5 + 7 = 12

Length 4 (entire array):

  • For [3, 2, 4, 1] (i=1, j=4):
    • To get 2 piles:
      • Split at h=1: f[1][1][1] + f[2][4][1] = 0 + 12 = 12
      • Split at h=2: f[1][2][1] + f[3][4][1] = 5 + 5 = 10
      • Split at h=3: f[1][3][1] + f[4][4][1] = 14 + 0 = 14
      • Minimum: f[1][4][2] = 10
    • To get 1 pile: f[1][4][1] = f[1][4][2] + (s[4] - s[0]) = 10 + 10 = 20

Step 5: Return result The minimum cost to merge all stones into one pile is f[1][4][1] = 20.

This represents the optimal merging sequence:

  1. Merge [3, 2] → [5] with cost 5
  2. Merge [4, 1] → [5] with cost 5
  3. Merge [5, 5] → [10] with cost 10 Total cost: 5 + 5 + 10 = 20

Solution Implementation

1class Solution:
2    def mergeStones(self, stones: List[int], K: int) -> int:
3        n = len(stones)
4      
5        # Check if it's possible to merge all stones into one pile
6        # We need exactly (n-1)/(K-1) merge operations
7        if (n - 1) % (K - 1) != 0:
8            return -1
9      
10        # Build prefix sum array for quick range sum calculation
11        # prefix_sum[i] = sum of stones[0] to stones[i-1]
12        prefix_sum = [0]
13        for stone in stones:
14            prefix_sum.append(prefix_sum[-1] + stone)
15      
16        # dp[i][j][m] = minimum cost to merge stones[i-1] to stones[j-1] into m piles
17        # Initialize with infinity (impossible states)
18        dp = [[[float('inf')] * (K + 1) for _ in range(n + 1)] for _ in range(n + 1)]
19      
20        # Base case: single stone forms 1 pile with cost 0
21        for i in range(1, n + 1):
22            dp[i][i][1] = 0
23      
24        # Iterate over all possible subarray lengths
25        for length in range(2, n + 1):
26            # Iterate over all possible starting positions
27            for start in range(1, n - length + 2):
28                end = start + length - 1
29              
30                # Try to form different number of piles
31                for num_piles in range(2, K + 1):
32                    # Try different split points
33                    for split in range(start, end, K - 1):
34                        # Merge left part into 1 pile and right part into (num_piles - 1) piles
35                        dp[start][end][num_piles] = min(
36                            dp[start][end][num_piles],
37                            dp[start][split][1] + dp[split + 1][end][num_piles - 1]
38                        )
39              
40                # Merge K piles into 1 pile
41                # The cost is the sum of all stones in the range
42                dp[start][end][1] = dp[start][end][K] + prefix_sum[end] - prefix_sum[start - 1]
43      
44        # Return minimum cost to merge all stones into 1 pile
45        return dp[1][n][1]
46
1class Solution {
2    public int mergeStones(int[] stones, int K) {
3        int n = stones.length;
4      
5        // Check if it's possible to merge all stones into 1 pile
6        // We reduce K piles to 1 pile in each merge, so we reduce by (K-1) piles each time
7        // Starting from n piles, we need (n-1) to be divisible by (K-1)
8        if ((n - 1) % (K - 1) != 0) {
9            return -1;
10        }
11      
12        // Build prefix sum array for quick range sum calculation
13        int[] prefixSum = new int[n + 1];
14        for (int i = 1; i <= n; i++) {
15            prefixSum[i] = prefixSum[i - 1] + stones[i - 1];
16        }
17      
18        // dp[i][j][m] = minimum cost to merge stones[i-1] to stones[j-1] into m piles
19        int[][][] dp = new int[n + 1][n + 1][K + 1];
20      
21        // Initialize with a large value (infinity)
22        final int INF = 1 << 20;
23        for (int[][] matrix : dp) {
24            for (int[] row : matrix) {
25                Arrays.fill(row, INF);
26            }
27        }
28      
29        // Base case: single stone forms 1 pile with 0 cost
30        for (int i = 1; i <= n; i++) {
31            dp[i][i][1] = 0;
32        }
33      
34        // Process all possible lengths of subarrays
35        for (int length = 2; length <= n; length++) {
36            // Try all starting positions for current length
37            for (int start = 1; start + length - 1 <= n; start++) {
38                int end = start + length - 1;
39              
40                // Try to form different number of piles (from 2 to K)
41                for (int piles = 2; piles <= K; piles++) {
42                    // Try all possible split points
43                    for (int split = start; split < end; split++) {
44                        // Merge left part into 1 pile and right part into (piles-1) piles
45                        dp[start][end][piles] = Math.min(
46                            dp[start][end][piles], 
47                            dp[start][split][1] + dp[split + 1][end][piles - 1]
48                        );
49                    }
50                }
51              
52                // Merge K piles into 1 pile
53                // Cost is the minimum cost to form K piles plus the sum of all stones in range
54                dp[start][end][1] = dp[start][end][K] + prefixSum[end] - prefixSum[start - 1];
55            }
56        }
57      
58        // Return the minimum cost to merge all stones into 1 pile
59        return dp[1][n][1];
60    }
61}
62
1class Solution {
2public:
3    int mergeStones(vector<int>& stones, int K) {
4        int n = stones.size();
5      
6        // Check if it's possible to merge all stones into 1 pile
7        // We reduce by (K-1) stones in each merge, so (n-1) must be divisible by (K-1)
8        if ((n - 1) % (K - 1) != 0) {
9            return -1;
10        }
11      
12        // Build prefix sum array for quick range sum calculation
13        vector<int> prefixSum(n + 1, 0);
14        for (int i = 1; i <= n; ++i) {
15            prefixSum[i] = prefixSum[i - 1] + stones[i - 1];
16        }
17      
18        // dp[i][j][m] = minimum cost to merge stones[i-1...j-1] into m piles
19        // Initialize with large values (using 0x3f3f3f3f as infinity)
20        vector<vector<vector<int>>> dp(n + 1, 
21            vector<vector<int>>(n + 1, 
22                vector<int>(K + 1, 0x3f3f3f3f)));
23      
24        // Base case: single stone forms 1 pile with 0 cost
25        for (int i = 1; i <= n; ++i) {
26            dp[i][i][1] = 0;
27        }
28      
29        // Process all possible lengths
30        for (int length = 2; length <= n; ++length) {
31            // Process all possible starting positions
32            for (int i = 1; i + length - 1 <= n; ++i) {
33                int j = i + length - 1;  // Ending position
34              
35                // Try to form k piles from range [i, j]
36                for (int piles = 2; piles <= K; ++piles) {
37                    // Split at position h: [i, h] forms 1 pile, [h+1, j] forms (piles-1) piles
38                    for (int h = i; h < j; ++h) {
39                        dp[i][j][piles] = min(dp[i][j][piles], 
40                                             dp[i][h][1] + dp[h + 1][j][piles - 1]);
41                    }
42                }
43              
44                // Merge K piles into 1 pile
45                // The cost is the sum of all stones in range [i, j]
46                dp[i][j][1] = dp[i][j][K] + prefixSum[j] - prefixSum[i - 1];
47            }
48        }
49      
50        // Return the minimum cost to merge all stones into 1 pile
51        return dp[1][n][1];
52    }
53};
54
1function mergeStones(stones: number[], K: number): number {
2    const n = stones.length;
3  
4    // Check if it's possible to merge all stones into 1 pile
5    // We reduce by (K-1) stones in each merge, so (n-1) must be divisible by (K-1)
6    if ((n - 1) % (K - 1) !== 0) {
7        return -1;
8    }
9  
10    // Build prefix sum array for quick range sum calculation
11    const prefixSum: number[] = new Array(n + 1).fill(0);
12    for (let i = 1; i <= n; i++) {
13        prefixSum[i] = prefixSum[i - 1] + stones[i - 1];
14    }
15  
16    // dp[i][j][m] = minimum cost to merge stones[i-1...j-1] into m piles
17    // Initialize with large values (using Infinity)
18    const dp: number[][][] = Array(n + 1)
19        .fill(null)
20        .map(() => 
21            Array(n + 1)
22                .fill(null)
23                .map(() => 
24                    Array(K + 1).fill(Infinity)
25                )
26        );
27  
28    // Base case: single stone forms 1 pile with 0 cost
29    for (let i = 1; i <= n; i++) {
30        dp[i][i][1] = 0;
31    }
32  
33    // Process all possible lengths
34    for (let length = 2; length <= n; length++) {
35        // Process all possible starting positions
36        for (let i = 1; i + length - 1 <= n; i++) {
37            const j = i + length - 1;  // Ending position
38          
39            // Try to form k piles from range [i, j]
40            for (let piles = 2; piles <= K; piles++) {
41                // Split at position h: [i, h] forms 1 pile, [h+1, j] forms (piles-1) piles
42                for (let h = i; h < j; h++) {
43                    dp[i][j][piles] = Math.min(
44                        dp[i][j][piles], 
45                        dp[i][h][1] + dp[h + 1][j][piles - 1]
46                    );
47                }
48            }
49          
50            // Merge K piles into 1 pile
51            // The cost is the sum of all stones in range [i, j]
52            dp[i][j][1] = dp[i][j][K] + prefixSum[j] - prefixSum[i - 1];
53        }
54    }
55  
56    // Return the minimum cost to merge all stones into 1 pile
57    return dp[1][n][1];
58}
59

Time and Space Complexity

Time Complexity: O(n³ * K)

The algorithm uses dynamic programming with four nested loops:

  • The outermost loop iterates through lengths from 2 to n: O(n) iterations
  • The second loop iterates through starting positions i: O(n) iterations
  • The third loop iterates through k from 1 to K: O(K) iterations
  • The innermost loop iterates through split positions h from i to j-1: O(n) iterations in the worst case

This gives us a total time complexity of O(n * n * K * n) = O(n³ * K).

Additionally, the prefix sum calculation takes O(n) time, but this doesn't affect the overall complexity.

Space Complexity: O(n² * K)

The algorithm uses a 3D DP array f with dimensions (n+1) × (n+1) × (K+1), which requires O(n² * K) space.

The prefix sum array s uses O(n) additional space, but this is dominated by the DP array.

Therefore, the total space complexity is O(n² * K).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Split Point Iteration

Pitfall: When trying to merge a range [start, end] into num_piles piles, developers often iterate through all possible split points from start to end-1. However, this leads to redundant calculations and incorrect results because not all split points are valid when merging into exactly K piles at a time.

Why it's wrong:

# INCORRECT approach:
for split in range(start, end):  # Checking every possible split
    dp[start][end][num_piles] = min(
        dp[start][end][num_piles],
        dp[start][split][1] + dp[split + 1][end][num_piles - 1]
    )

This checks unnecessary splits that don't align with the constraint of merging exactly K piles at a time.

Correct approach:

# CORRECT approach:
for split in range(start, end, K - 1):  # Step by K-1
    dp[start][end][num_piles] = min(
        dp[start][end][num_piles],
        dp[start][split][1] + dp[split + 1][end][num_piles - 1]
    )

The split points should increment by K-1 because when we merge K piles into 1, we reduce the total count by K-1. This ensures we only consider valid merging patterns.

2. Misunderstanding the Impossibility Condition

Pitfall: Developers might check n % K == 0 or other incorrect conditions to determine if merging is possible.

Why it's wrong:

# INCORRECT checks:
if n % K != 0:  # Wrong!
    return -1

if n % K == 1:  # Also wrong!
    return -1

Correct approach:

# CORRECT check:
if (n - 1) % (K - 1) != 0:
    return -1

Explanation: Each merge operation takes K piles and produces 1 pile, reducing the total by K-1. To go from n piles to 1 pile, we need exactly (n-1)/(K-1) merge operations. This is only possible if (n-1) is divisible by (K-1).

3. Off-by-One Errors in Prefix Sum Calculation

Pitfall: Using 0-indexed prefix sums with 1-indexed DP table leads to index confusion.

Why it's wrong:

# INCORRECT: Mixing 0-indexed and 1-indexed
prefix_sum = list(accumulate(stones))  # 0-indexed
# Later in code:
cost = prefix_sum[end] - prefix_sum[start - 1]  # Index error when start = 1

Correct approach:

# CORRECT: Consistent 1-indexed approach
prefix_sum = [0]
for stone in stones:
    prefix_sum.append(prefix_sum[-1] + stone)
# Or using accumulate:
prefix_sum = list(accumulate(stones, initial=0))

# Now safe to use:
cost = prefix_sum[end] - prefix_sum[start - 1]

4. Forgetting the Final Merge Cost

Pitfall: Only calculating the cost to arrange stones into K piles without adding the final merge cost.

Why it's wrong:

# INCORRECT: Missing the final merge cost
dp[start][end][1] = dp[start][end][K]  # Forgot to add merge cost!

Correct approach:

# CORRECT: Include the cost of the final merge
dp[start][end][1] = dp[start][end][K] + prefix_sum[end] - prefix_sum[start - 1]

The cost to merge K piles into 1 includes both:

  1. The cost to arrange into K piles (dp[start][end][K])
  2. The cost of merging those K piles (sum of all stones in the range)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More