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]intompiles, we can split the range at some pointhand merge[i, h]into 1 pile and[h+1, j]intom-1piles - To merge stones
[i, j]into 1 pile, we first need to merge them intoKpiles (which costsf[i][j][K]), then perform one final merge of thoseKpiles (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:
iis the starting position (1-indexed)jis the ending position (1-indexed)kis 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 = 4piles - 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]
461class 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}
621class 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};
541function 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}
59Time 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
Kpiles (dp[start][end][K]) - The cost of merging those
Kpiles (sum of all stones in the range)
What's the output of running the following function using input 56?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
271private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
301const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30Recommended 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 Given an array of integers arr and a target value find a subarray that sums to the target Return the start and end indices of the subarray Input arr 1 20 3 30 5 4 target 7 Output 1 4 Explanation The subarray 20 3 30 from index 1 inclusive
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!