1049. Last Stone Weight II
Problem Description
You have a collection of stones, each with a specific weight given in an array stones
, where stones[i]
represents the weight of the i-th stone.
You're playing a stone-smashing game with the following rules:
- On each turn, you select any two stones and smash them together
- If you pick two stones with weights
x
andy
(wherex ≤ y
):- When
x == y
: Both stones are completely destroyed - When
x != y
: The lighter stone (weightx
) is destroyed, and the heavier stone (weighty
) becomes a new stone with weighty - x
- When
The game continues until you have at most one stone remaining.
Your goal is to find the minimum possible weight of the final remaining stone. If no stones remain at the end, return 0
.
For example, if you have stones [2, 7, 4, 1, 8, 1]
:
- One possible sequence: smash 2 and 4 → get 2, smash 7 and 8 → get 1, smash 1 and 1 → get 0, smash 2 and 1 → get 1, final result is 1
- The optimal strategy would minimize this final weight
The solution uses dynamic programming to solve this as a subset sum problem. The key insight is that smashing stones is equivalent to partitioning them into two groups and finding the minimum difference between the groups' sums. The DP approach finds the maximum sum possible that doesn't exceed half of the total sum, then calculates the minimum difference as total_sum - 2 * max_subset_sum
.
Intuition
Let's think about what happens when we smash stones together. When we smash stones with weights x
and y
, we get a new stone with weight |x - y|
. This is essentially subtracting one weight from another.
If we trace through the entire game, we can observe that each stone in the original array will eventually contribute to the final result with either a positive or negative sign. For instance, if we have stones [a, b, c, d]
, the final result could be something like a - b + c - d
or a + b - c - d
, depending on how we pair and smash them.
This means we're essentially trying to divide the stones into two groups: one group gets a positive sign and the other gets a negative sign. The final weight will be the absolute difference between the sums of these two groups.
To minimize the final stone weight, we want to minimize |sum(group1) - sum(group2)|
. This is equivalent to partitioning the stones into two subsets with sums as close to each other as possible.
If the total sum is S
, and one subset has sum subset_sum
, then the other subset has sum S - subset_sum
. The difference between them is |(S - subset_sum) - subset_sum| = |S - 2 * subset_sum|
.
To minimize this difference, we want subset_sum
to be as close to S/2
as possible. The problem now transforms into: "Find the maximum sum of a subset that doesn't exceed S/2
."
This is a classic 0/1 knapsack problem where:
- The "capacity" of our knapsack is
S/2
- Each stone can either be included in our subset (put in knapsack) or not
- We want to maximize the sum without exceeding
S/2
Once we find this maximum possible subset_sum
, the minimum final stone weight is S - 2 * subset_sum
.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a dynamic programming approach to solve the 0/1 knapsack problem we identified in our intuition.
Setup:
- Calculate the total sum
s
of all stones - Set the target capacity as
n = s >> 1
(which iss // 2
) - Create a 2D DP table
dp[m+1][n+1]
wherem
is the number of stones
DP Table Definition:
dp[i][j]
represents the maximum sum we can achieve using the firsti
stones with a capacity limit ofj
- The table has dimensions
(m+1) × (n+1)
to handle the base case where we use 0 stones
DP Transition:
For each stone i
(from 1 to m) and each capacity j
(from 0 to n):
-
Option 1 - Don't take the current stone:
dp[i][j] = dp[i-1][j]
- We inherit the best result from considering the first
i-1
stones
-
Option 2 - Take the current stone (if it fits):
- Only possible if
stones[i-1] <= j
(the stone's weight doesn't exceed current capacity) - If we take it:
dp[i][j] = dp[i-1][j - stones[i-1]] + stones[i-1]
- This means we add the stone's weight to the best result we could get with remaining capacity
- Only possible if
-
Choose the maximum:
dp[i][j] = max(Option 1, Option 2)
Building the Solution: The algorithm fills the DP table row by row:
for i in range(1, m + 1):
for j in range(n + 1):
dp[i][j] = dp[i - 1][j] # Don't take stone i
if stones[i - 1] <= j: # Can we take stone i?
dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] + stones[i - 1])
Final Answer:
dp[-1][-1]
(ordp[m][n]
) gives us the maximum sum of a subset that doesn't exceeds/2
- This represents one group of stones with sum closest to half the total
- The other group has sum
s - dp[m][n]
- The minimum difference is:
(s - dp[m][n]) - dp[m][n] = s - 2 * dp[m][n]
Time Complexity: O(m * n)
where m
is the number of stones and n
is half the total sum
Space Complexity: O(m * n)
for the DP table
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 the solution with stones = [2, 7, 4, 1, 8, 1]
.
Step 1: Calculate total sum and target
- Total sum
s = 2 + 7 + 4 + 1 + 8 + 1 = 23
- Target capacity
n = 23 // 2 = 11
- We want to find the maximum subset sum ≤ 11
Step 2: Initialize DP table
- Create
dp[7][12]
(6 stones + 1, capacity 0 to 11) - Initialize first row to 0 (using 0 stones gives sum 0)
Step 3: Fill DP table
Let's trace through key decisions:
For stone 1 (weight = 2):
- At capacity j=2: Can take stone →
dp[1][2] = 2
- At capacity j=3: Can take stone →
dp[1][3] = 2
- Continue for all capacities...
For stone 2 (weight = 7):
- At capacity j=7: Can take stone →
dp[2][7] = 7
- At capacity j=9: Can take stone 2 alone (7) or stones 1+2 (2+7=9) →
dp[2][9] = 9
- At capacity j=11: Best is stones 1+2 →
dp[2][11] = 9
For stone 3 (weight = 4):
- At capacity j=6: Can take stones 1+3 (2+4=6) →
dp[3][6] = 6
- At capacity j=11: Can take stones 2+3 (7+4=11) or keep previous best (9) →
dp[3][11] = 11
For stone 4 (weight = 1):
- At capacity j=8: Can add stone 4 to previous combinations →
dp[4][8] = 8
- At capacity j=11: Previous best was already 11 →
dp[4][11] = 11
For stone 5 (weight = 8):
- At capacity j=10: Can take stones 1+5 (2+8=10) →
dp[5][10] = 10
- At capacity j=11: Can take stones 1+4+5 (2+1+8=11) or keep 11 →
dp[5][11] = 11
For stone 6 (weight = 1):
- At capacity j=11: Already at maximum →
dp[6][11] = 11
Step 4: Calculate result
- Maximum subset sum ≤ 11 is
dp[6][11] = 11
- One group has sum 11, the other has sum 23 - 11 = 12
- Minimum stone weight = 23 - 2×11 = 23 - 22 = 1
The final DP table (showing key columns):
j=0 j=2 j=4 j=6 j=7 j=9 j=10 j=11 i=0 0 0 0 0 0 0 0 0 i=1(2) 0 2 2 2 2 2 2 2 i=2(7) 0 2 2 2 7 9 9 9 i=3(4) 0 2 4 6 7 9 9 11 i=4(1) 0 2 4 6 7 9 10 11 i=5(8) 0 2 4 6 7 9 10 11 i=6(1) 0 2 4 6 7 9 10 11
This gives us the minimum possible final stone weight of 1, which matches the expected result!
Solution Implementation
1class Solution:
2 def lastStoneWeightII(self, stones: List[int]) -> int:
3 # Calculate the total sum of all stones
4 total_sum = sum(stones)
5
6 # Number of stones and target sum (half of total)
7 num_stones = len(stones)
8 target = total_sum // 2
9
10 # DP table: dp[i][j] represents the maximum sum we can achieve
11 # using first i stones with sum not exceeding j
12 dp = [[0] * (target + 1) for _ in range(num_stones + 1)]
13
14 # Fill the DP table
15 for i in range(1, num_stones + 1):
16 for j in range(target + 1):
17 # Option 1: Don't include the current stone
18 dp[i][j] = dp[i - 1][j]
19
20 # Option 2: Include the current stone if it fits
21 if stones[i - 1] <= j:
22 dp[i][j] = max(
23 dp[i][j],
24 dp[i - 1][j - stones[i - 1]] + stones[i - 1]
25 )
26
27 # The minimum difference is: total_sum - 2 * maximum_sum_in_one_subset
28 # This works because if we partition stones into two groups with sums S1 and S2,
29 # where S1 <= S2, then S1 + S2 = total_sum and the difference is S2 - S1
30 # We want to maximize S1 (up to total_sum/2), so S2 - S1 = total_sum - 2*S1
31 return total_sum - 2 * dp[num_stones][target]
32
1class Solution {
2 public int lastStoneWeightII(int[] stones) {
3 // Calculate the total sum of all stones
4 int totalSum = 0;
5 for (int stone : stones) {
6 totalSum += stone;
7 }
8
9 // Number of stones
10 int numStones = stones.length;
11
12 // Target capacity is half of total sum
13 // We want to find a subset with sum as close to totalSum/2 as possible
14 int targetCapacity = totalSum / 2;
15
16 // dp[i][j] represents the maximum sum we can achieve using first i stones
17 // with sum not exceeding j
18 int[][] dp = new int[numStones + 1][targetCapacity + 1];
19
20 // Fill the dp table using 0/1 knapsack approach
21 for (int i = 1; i <= numStones; i++) {
22 for (int j = 0; j <= targetCapacity; j++) {
23 // Option 1: Don't include current stone
24 dp[i][j] = dp[i - 1][j];
25
26 // Option 2: Include current stone if it fits
27 if (stones[i - 1] <= j) {
28 dp[i][j] = Math.max(dp[i][j],
29 dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
30 }
31 }
32 }
33
34 // The minimum difference is (totalSum - subset1Sum) - subset1Sum
35 // which equals totalSum - 2 * subset1Sum
36 // where subset1Sum is the maximum sum not exceeding totalSum/2
37 return totalSum - dp[numStones][targetCapacity] * 2;
38 }
39}
40
1class Solution {
2public:
3 int lastStoneWeightII(vector<int>& stones) {
4 // Calculate the total sum of all stones
5 int totalSum = accumulate(stones.begin(), stones.end(), 0);
6
7 // Number of stones and target sum (half of total)
8 // We want to find the maximum sum <= totalSum/2
9 int numStones = stones.size();
10 int targetSum = totalSum / 2;
11
12 // DP table: dp[i][j] represents the maximum sum we can achieve
13 // using first i stones with sum not exceeding j
14 vector<vector<int>> dp(numStones + 1, vector<int>(targetSum + 1, 0));
15
16 // Fill the DP table
17 for (int i = 1; i <= numStones; ++i) {
18 for (int currentCapacity = 0; currentCapacity <= targetSum; ++currentCapacity) {
19 // Option 1: Don't take the current stone
20 dp[i][currentCapacity] = dp[i - 1][currentCapacity];
21
22 // Option 2: Take the current stone if it fits
23 int currentStoneWeight = stones[i - 1];
24 if (currentStoneWeight <= currentCapacity) {
25 dp[i][currentCapacity] = max(
26 dp[i][currentCapacity],
27 dp[i - 1][currentCapacity - currentStoneWeight] + currentStoneWeight
28 );
29 }
30 }
31 }
32
33 // The answer is the difference between the two groups:
34 // Group 1: dp[numStones][targetSum] (maximum sum <= totalSum/2)
35 // Group 2: totalSum - dp[numStones][targetSum]
36 // Difference: (totalSum - dp[numStones][targetSum]) - dp[numStones][targetSum]
37 // = totalSum - 2 * dp[numStones][targetSum]
38 return totalSum - 2 * dp[numStones][targetSum];
39 }
40};
41
1/**
2 * @param {number[]} stones - Array of stone weights
3 * @return {number} - Minimum possible weight of the last stone
4 */
5function lastStoneWeightII(stones: number[]): number {
6 // Calculate the total sum of all stone weights
7 let totalSum: number = 0;
8 for (const stoneWeight of stones) {
9 totalSum += stoneWeight;
10 }
11
12 // Target sum is half of the total sum (for minimum difference between two subsets)
13 const targetSum: number = totalSum >> 1; // Bit shift right by 1 (equivalent to Math.floor(totalSum / 2))
14
15 // Initialize DP array where dp[i] represents the maximum sum achievable
16 // using stones that doesn't exceed weight i
17 const dp: number[] = new Array(targetSum + 1).fill(0);
18
19 // Apply 0/1 knapsack algorithm
20 for (const stoneWeight of stones) {
21 // Iterate backwards to avoid using the same stone multiple times
22 for (let currentCapacity = targetSum; currentCapacity >= stoneWeight; --currentCapacity) {
23 // Either include current stone or exclude it, take the maximum
24 dp[currentCapacity] = Math.max(
25 dp[currentCapacity], // Exclude current stone
26 dp[currentCapacity - stoneWeight] + stoneWeight // Include current stone
27 );
28 }
29 }
30
31 // The minimum difference is: totalSum - 2 * maxSumInOneSubset
32 // This represents the difference between two subsets
33 return totalSum - dp[targetSum] * 2;
34}
35
Time and Space Complexity
Time Complexity: O(m * n)
where m
is the number of stones and n
is half of the total sum of all stones.
The algorithm uses a nested loop structure:
- The outer loop iterates through all stones from
1
tom
, giving usO(m)
iterations - The inner loop iterates from
0
ton
(wheren = sum(stones) / 2
), giving usO(n)
iterations for each stone - Inside the nested loops, all operations (comparisons, assignments, array accesses) are
O(1)
- Therefore, the overall time complexity is
O(m * n)
Since n
can be at most O(m * max(stones))
in the worst case (where all stones have the maximum value), the time complexity can also be expressed as O(m * sum(stones) / 2)
or simply O(m * S)
where S
is the sum of all stone weights.
Space Complexity: O(m * n)
where m
is the number of stones and n
is half of the total sum.
The space complexity is dominated by:
- The 2D DP table
dp
with dimensions(m + 1) × (n + 1)
, requiringO(m * n)
space - The input list
stones
is given as input and not counted in auxiliary space - A few constant variables (
s
,m
,n
, loop counters) requiringO(1)
space - Therefore, the overall space complexity is
O(m * n)
This can be optimized to O(n)
space by using a 1D DP array since each row only depends on the previous row, but the current implementation uses the full 2D table.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow with Large Stone Weights
When calculating the total sum, if individual stone weights are large (near the constraint limits), the sum might cause issues in some languages. In Python this is handled automatically, but in languages like Java or C++, you might need to use appropriate data types.
Solution: Use long
or appropriate large integer types when calculating sums in statically-typed languages.
2. Incorrect DP Table Indexing
A frequent mistake is confusing the indexing between stones array and DP table. Since DP table uses 1-based indexing for stones (row 0 represents using 0 stones), but the stones array uses 0-based indexing, accessing stones[i]
when filling dp[i][j]
is wrong.
Incorrect:
if stones[i] <= j: # Wrong! Off-by-one error
dp[i][j] = max(dp[i][j], dp[i-1][j - stones[i]] + stones[i])
Correct:
if stones[i-1] <= j: # Correct! Account for different indexing
dp[i][j] = max(dp[i][j], dp[i-1][j - stones[i-1]] + stones[i-1])
3. Using Full Sum Instead of Half
Some might create a DP table with the second dimension as total_sum
instead of total_sum // 2
. This wastes memory and computation since we only need to find subsets up to half the total.
Solution: Always use target = total_sum // 2
as the capacity limit.
4. Space Optimization Pitfall
When optimizing space to use a 1D array instead of 2D, iterating forward through capacities will cause incorrect results due to overwriting values needed for computation.
Incorrect 1D optimization:
dp = [0] * (target + 1)
for stone in stones:
for j in range(target + 1): # Wrong! Forward iteration
if stone <= j:
dp[j] = max(dp[j], dp[j - stone] + stone)
Correct 1D optimization:
dp = [0] * (target + 1)
for stone in stones:
for j in range(target, stone - 1, -1): # Correct! Backward iteration
dp[j] = max(dp[j], dp[j - stone] + stone)
5. Misunderstanding the Problem Transformation
Not recognizing that the stone smashing process is equivalent to partitioning into two subsets. Some might try to simulate the actual smashing process, which leads to exponential time complexity.
Solution: Understand that after all operations, stones can be divided into two groups where one group's stones are subtracted from the other group's stones. The result is |sum(group1) - sum(group2)|, which we want to minimize.
Which of these pictures shows the visit order of a depth-first search?
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!