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.
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]
intom
piles, we can split the range at some pointh
and merge[i, h]
into 1 pile and[h+1, j]
intom-1
piles - To merge stones
[i, j]
into 1 pile, we first need to merge them intoK
piles (which costsf[i][j][K]
), then perform one final merge of thoseK
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 EvaluatorExample 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
- To get 2 piles:
- 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
- To get 2 piles:
- 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
- To get 2 piles:
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
- To get 2 piles (split at h=1):
- 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
- To get 2 piles:
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
- Split at h=1:
- To get 1 pile:
f[1][4][1] = f[1][4][2] + (s[4] - s[0]) = 10 + 10 = 20
- To get 2 piles:
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:
- Merge [3, 2] → [5] with cost 5
- Merge [4, 1] → [5] with cost 5
- 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:
- The cost to arrange into
K
piles (dp[start][end][K]
) - The cost of merging those
K
piles (sum of all stones in the range)
Which of the following uses divide and conquer strategy?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!