You are given an array of integers stones where stones[i] is the weight of the ith stone.
We are playing a game with the stones. On each turn, we choose any two stones and smash them together. Suppose the stones have weights x and y with x <= y. The result of this smash is:
If x == y, both stones are destroyed, and
If x != y, the stone of weight x is destroyed, and the stone of weight y has new weight y - x.
At the end of the game, there is at most one stone left.
Return the smallest possible weight of the left stone. If there are no stones left, return 0.
Example 1:
Input: stones =[2,4,1,5,6,3]Output:1
Explanation:
We smash 2 and 1 which makes the array [1,4,5,6,3].
Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Dynamic Programming - Understanding both top-down (memoization) and bottom-up (tabulation) approaches
0/1 Knapsack Problem - Selecting items with constraints to optimize a value
Subset Sum - Determining if a subset with a target sum exists
Recursion with Memoization - Caching results of subproblems to avoid redundant computation
1. Recursion
Intuition
The key insight is that smashing stones is equivalent to partitioning them into two groups and finding the minimum difference between their sums. When two stones collide, the result is the absolute difference of their weights. If we think of assigning a positive or negative sign to each stone, the final result is the absolute value of the sum. This transforms the problem into finding a subset with sum as close to half the total as possible.
Algorithm
Compute the total sum of all stones and set a target of half this sum.
Use recursion to explore two choices for each stone: include it in the current subset or skip it.
If the running total reaches or exceeds the target, or we've processed all stones, return the absolute difference between the two groups.
Return the minimum result across all recursive paths.
funclastStoneWeightII(stones []int)int{
stoneSum :=0for_, stone :=range stones {
stoneSum += stone
}
target :=(stoneSum +1)/2var dfs func(i, total int)int
dfs =func(i, total int)int{if total >= target || i ==len(stones){returnabs(total -(stoneSum - total))}returnmin(dfs(i+1, total),dfs(i+1, total+stones[i]))}returndfs(0,0)}funcabs(x int)int{if x <0{return-x
}return x
}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funlastStoneWeightII(stones: IntArray): Int {val stoneSum = stones.sum()val target =(stoneSum +1)/2fundfs(i: Int, total: Int): Int {if(total >= target || i == stones.size){return kotlin.math.abs(total -(stoneSum - total))}returnminOf(dfs(i +1, total),dfs(i +1, total + stones[i]))}returndfs(0,0)}}
classSolution{funclastStoneWeightII(_ stones:[Int])->Int{let stoneSum = stones.reduce(0,+)let target =(stoneSum +1)/2funcdfs(_ i:Int,_ total:Int)->Int{if total >= target || i == stones.count {returnabs(total -(stoneSum - total))}returnmin(dfs(i +1, total),dfs(i +1, total + stones[i]))}returndfs(0,0)}}
Time & Space Complexity
Time complexity: O(2n)
Space complexity: O(min(n,m)) for recursion stack.
Where n is the number of stones and m is the sum of the weights of the stones.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution recomputes the same subproblems many times. For example, reaching a total of 10 using stones at different indices might happen through multiple paths. By caching results based on the current index and running total, we avoid redundant work and speed up the solution significantly.
Algorithm
Compute the total sum and target as in the recursive approach.
Create a memoization dictionary keyed by (index, total).
Before recursing, check if the result is already cached. If so, return it.
Otherwise, compute the result by trying both choices (skip or include the stone), cache it, and return.
funclastStoneWeightII(stones []int)int{
stoneSum :=0for_, stone :=range stones {
stoneSum += stone
}
target :=(stoneSum +1)/2
dp :=make([][]int,len(stones))for i :=range dp {
dp[i]=make([]int, target+1)for j :=range dp[i]{
dp[i][j]=-1}}var dfs func(i, total int)int
dfs =func(i, total int)int{if total >= target || i ==len(stones){returnabs(total -(stoneSum - total))}if dp[i][total]!=-1{return dp[i][total]}
dp[i][total]=min(dfs(i+1, total),dfs(i+1, total+stones[i]))return dp[i][total]}returndfs(0,0)}funcabs(x int)int{if x <0{return-x
}return x
}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funlastStoneWeightII(stones: IntArray): Int {val stoneSum = stones.sum()val target =(stoneSum +1)/2val dp =Array(stones.size){IntArray(target +1){-1}}fundfs(i: Int, total: Int): Int {if(total >= target || i == stones.size){return kotlin.math.abs(total -(stoneSum - total))}if(dp[i][total]!=-1){return dp[i][total]}
dp[i][total]=minOf(dfs(i +1, total),dfs(i +1, total + stones[i]))return dp[i][total]}returndfs(0,0)}}
Where n is the number of stones and m is the sum of the weights of the stones.
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of working recursively from the first stone forward, we can build up solutions iteratively. We create a 2D table where dp[i][t] represents the maximum sum achievable using the first i stones without exceeding capacity t. This is essentially a 0/1 knapsack problem where we want to pack stones into a knapsack of capacity target to maximize the total weight.
Algorithm
Initialize a 2D DP table of size (n+1) x (target+1) with zeros.
For each stone i from 1 to n:
For each possible capacity t from 0 to target:
If the stone fits (t >= stones[i-1]), take the maximum of skipping it or including it.
classSolution:deflastStoneWeightII(self, stones: List[int])->int:
stoneSum =sum(stones)
target = stoneSum //2
n =len(stones)
dp =[[0]*(target +1)for _ inrange(n +1)]for i inrange(1, n +1):for t inrange(target +1):if t >= stones[i -1]:
dp[i][t]=max(dp[i -1][t], dp[i -1][t - stones[i -1]]+ stones[i -1])else:
dp[i][t]= dp[i -1][t]return stoneSum -2* dp[n][target]
publicclassSolution{publicintlastStoneWeightII(int[] stones){int stoneSum =0;for(int stone : stones){
stoneSum += stone;}int target = stoneSum /2;int n = stones.length;int[][] dp =newint[n +1][target +1];for(int i =1; i <= n; i++){for(int t =0; t <= target; t++){if(t >= stones[i -1]){
dp[i][t]=Math.max(dp[i -1][t], dp[i -1][t - stones[i -1]]+ stones[i -1]);}else{
dp[i][t]= dp[i -1][t];}}}return stoneSum -2* dp[n][target];}}
classSolution{public:intlastStoneWeightII(vector<int>& stones){int stoneSum =accumulate(stones.begin(), stones.end(),0);int target = stoneSum /2;int n = stones.size();
vector<vector<int>>dp(n +1,vector<int>(target +1,0));for(int i =1; i <= n; i++){for(int t =0; t <= target; t++){if(t >= stones[i -1]){
dp[i][t]=max(dp[i -1][t], dp[i -1][t - stones[i -1]]+ stones[i -1]);}else{
dp[i][t]= dp[i -1][t];}}}return stoneSum -2* dp[n][target];}};
classSolution{/**
* @param {number[]} stones
* @return {number}
*/lastStoneWeightII(stones){const stoneSum = stones.reduce((a, b)=> a + b,0);const target = Math.floor(stoneSum /2);const n = stones.length;const dp = Array.from({length: n +1},()=>Array(target +1).fill(0),);for(let i =1; i <= n; i++){for(let t =0; t <= target; t++){if(t >= stones[i -1]){
dp[i][t]= Math.max(
dp[i -1][t],
dp[i -1][t - stones[i -1]]+ stones[i -1],);}else{
dp[i][t]= dp[i -1][t];}}}return stoneSum -2* dp[n][target];}}
publicclassSolution{publicintLastStoneWeightII(int[] stones){int stoneSum =0;foreach(int stone in stones){
stoneSum += stone;}int target = stoneSum /2;int n = stones.Length;int[,] dp =newint[n +1, target +1];for(int i =1; i <= n; i++){for(int t =0; t <= target; t++){if(t >= stones[i -1]){
dp[i, t]= Math.Max(dp[i -1, t], dp[i -1, t - stones[i -1]]+ stones[i -1]);}else{
dp[i, t]= dp[i -1, t];}}}return stoneSum -2* dp[n, target];}}
funclastStoneWeightII(stones []int)int{
stoneSum :=0for_, stone :=range stones {
stoneSum += stone
}
target := stoneSum /2
n :=len(stones)
dp :=make([][]int, n+1)for i :=range dp {
dp[i]=make([]int, target+1)}for i :=1; i <= n; i++{for t :=0; t <= target; t++{if t >= stones[i-1]{
dp[i][t]=max(dp[i-1][t], dp[i-1][t-stones[i-1]]+stones[i-1])}else{
dp[i][t]= dp[i-1][t]}}}return stoneSum -2*dp[n][target]}funcmax(a, b int)int{if a > b {return a
}return b
}
classSolution{funclastStoneWeightII(_ stones:[Int])->Int{let stoneSum = stones.reduce(0,+)let target = stoneSum /2let n = stones.count
var dp =[[Int]](repeating:[Int](repeating:0, count: target +1), count: n +1)for i in1...n {for t in0...target {if t >= stones[i -1]{
dp[i][t]=max(dp[i -1][t], dp[i -1][t - stones[i -1]]+ stones[i -1])}else{
dp[i][t]= dp[i -1][t]}}}return stoneSum -2* dp[n][target]}}
Time & Space Complexity
Time complexity: O(n∗m)
Space complexity: O(n∗m)
Where n is the number of stones and m is the sum of the weights of the stones.
4. Dynamic Programming (Space Optimized)
Intuition
Looking at the bottom-up solution, each row only depends on the previous row. This means we don't need to store the entire 2D table. A single 1D array is sufficient if we iterate through capacities in reverse order to avoid overwriting values we still need.
Algorithm
Initialize a 1D DP array of size target + 1 with zeros.
For each stone in the array:
Iterate t from target down to the stone's weight.
Update dp[t] as the maximum of keeping it or adding the stone.
Where n is the number of stones and m is the sum of the weights of the stones.
5. Dynamic Programming (Hash Set)
Intuition
Instead of tracking the maximum achievable sum at each capacity, we can simply track which sums are reachable. A hash set stores all possible subset sums. For each stone, we generate new reachable sums by adding the stone's weight to existing sums. The answer is the largest reachable sum that doesn't exceed target.
Algorithm
Initialize a set containing only 0 (representing an empty subset).
For each stone:
Create a new set by adding the stone's weight to each existing value.
Merge it with the existing set.
If we reach exactly target, return 0 immediately.
Find the maximum value in the set and return stoneSum - 2 * max.
classSolution:deflastStoneWeightII(self, stones: List[int])->int:
stoneSum =sum(stones)
target = stoneSum //2
dp ={0}for stone in stones:
new_dp =set(dp)for val in dp:if val + stone == target:return stoneSum -2* target
if val + stone < target:
new_dp.add(val + stone)
dp = new_dp
return stoneSum -2*max(dp)
publicclassSolution{publicintlastStoneWeightII(int[] stones){int stoneSum =0;for(int stone : stones){
stoneSum += stone;}int target = stoneSum /2;Set<Integer> dp =newHashSet<>();
dp.add(0);for(int stone : stones){Set<Integer> newDp =newHashSet<>(dp);for(int val : dp){if(val + stone == target){return stoneSum -2* target;}if(val + stone < target){
newDp.add(val + stone);}}
dp = newDp;}int maxVal =0;for(int val : dp){
maxVal =Math.max(maxVal, val);}return stoneSum -2* maxVal;}}
classSolution{public:intlastStoneWeightII(vector<int>& stones){int stoneSum =accumulate(stones.begin(), stones.end(),0);int target = stoneSum /2;
unordered_set<int> dp ={0};for(int& stone : stones){
unordered_set<int>newDp(dp);for(int val : dp){if(val + stone == target){return stoneSum -2* target;}if(val + stone < target){
newDp.insert(val + stone);}}
dp = newDp;}int maxVal =0;for(int val : dp){
maxVal =max(maxVal, val);}return stoneSum -2* maxVal;}};
classSolution{/**
* @param {number[]} stones
* @return {number}
*/lastStoneWeightII(stones){const stoneSum = stones.reduce((a, b)=> a + b,0);const target = Math.floor(stoneSum /2);let dp =newSet([0]);for(const stone of stones){const newDp =newSet(dp);for(const val of dp){if(val + stone === target){return stoneSum -2* target;}if(val + stone < target){
newDp.add(val + stone);}}
dp = newDp;}return stoneSum -2* Math.max(...dp);}}
publicclassSolution{publicintLastStoneWeightII(int[] stones){int stoneSum =0;foreach(int stone in stones){
stoneSum += stone;}int target = stoneSum /2;HashSet<int> dp =newHashSet<int>();
dp.Add(0);foreach(int stone in stones){HashSet<int> newDp =newHashSet<int>(dp);foreach(int val in dp){if(val + stone == target){return stoneSum -2* target;}if(val + stone < target){
newDp.Add(val + stone);}}
dp = newDp;}int maxVal =0;foreach(int val in dp){
maxVal = Math.Max(maxVal, val);}return stoneSum -2* maxVal;}}
funclastStoneWeightII(stones []int)int{
stoneSum :=0for_, stone :=range stones {
stoneSum += stone
}
target := stoneSum /2
dp :=make(map[int]bool)
dp[0]=truefor_, stone :=range stones {
newDp :=make(map[int]bool)for val :=range dp {
newDp[val]=trueif val+stone == target {return stoneSum -2*target
}if val+stone < target {
newDp[val+stone]=true}}
dp = newDp
}
maxVal :=0for val :=range dp {if val > maxVal {
maxVal = val
}}return stoneSum -2*maxVal
}
class Solution {funlastStoneWeightII(stones: IntArray): Int {val stoneSum = stones.sum()val target = stoneSum /2var dp =hashSetOf(0)for(stone in stones){val newDp =HashSet(dp)for(v in dp){if(v + stone == target){return stoneSum -2* target
}if(v + stone < target){
newDp.add(v + stone)}}
dp = newDp
}return stoneSum -2*(dp.maxOrNull()?:0)}}
classSolution{funclastStoneWeightII(_ stones:[Int])->Int{let stoneSum = stones.reduce(0,+)let target = stoneSum /2var dp:Set<Int>=[0]for stone in stones {var newDp = dp
for val in dp {if val + stone == target {return stoneSum -2* target
}if val + stone < target {
newDp.insert(val + stone)}}
dp = newDp
}return stoneSum -2*(dp.max()??0)}}
Time & Space Complexity
Time complexity: O(n∗m)
Space complexity: O(m)
Where n is the number of stones and m is the sum of the weights of the stones.
6. Dynamic Programming (Bitset)
Intuition
A bitset can represent reachable sums more compactly than a hash set. Each bit position represents whether that sum is achievable. Shifting the bitset left by a stone's weight and OR-ing with the original efficiently computes all new reachable sums in a single operation.
Algorithm
Initialize a bitset with only bit 0 set (sum 0 is reachable).
For each stone, left-shift the bitset by the stone's weight and OR it with itself.
Starting from target down to 0, find the first set bit. This represents the largest achievable sum not exceeding target.
classSolution:deflastStoneWeightII(self, stones: List[int])->int:
stoneSum =sum(stones)
target = stoneSum //2
dp =1for stone in stones:
dp |= dp << stone
for t inrange(target,-1,-1):if dp &(1<< t):return stoneSum -2* t
classSolution{public:intlastStoneWeightII(vector<int>& stones){int stoneSum =accumulate(stones.begin(), stones.end(),0);int target = stoneSum /2;
bitset<3001> dp;
dp[0]=true;for(int stone : stones){
dp |=(dp << stone);}for(int t = target; t >=0;--t){if(dp[t]){return stoneSum -2* t;}}return0;}};
Time & Space Complexity
Time complexity: O(n∗m)
Space complexity: O(m)
Where n is the number of stones and m is the sum of the weights of the stones.
Common Pitfalls
Not Recognizing the Subset Sum Connection
The problem appears to be about simulating stone collisions, but it is actually a partition problem. The key insight is that the final result equals the absolute difference between two groups of stones. Missing this connection leads to inefficient simulation approaches that fail on larger inputs.
Incorrect Target Calculation
The target should be stoneSum // 2 (floor division), representing the largest possible sum for one partition. Using ceiling division or forgetting to halve the sum leads to incorrect DP table dimensions or wrong final calculations.
Forward Iteration in Space-Optimized DP
In the 1D DP approach, iterating from 0 to target instead of from target down to stone causes each stone to be counted multiple times. This happens because updated values at lower indices affect higher indices within the same iteration, violating the 0/1 knapsack constraint.