Last Stone Weight II
MedPrompt
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 weightxis destroyed, and the stone of weightyhas new weighty - 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,7,4,1,8,1]
Output: 1
Explanation:
We can combine 2 and 4 to get 2, so the array converts to [2,7,1,8,1] then,
we can combine 7 and 8 to get 1, so the array converts to [2,1,1,1] then,
we can combine 2 and 1 to get 1, so the array converts to [1,1,1] then,
we can combine 1 and 1 to get 0, so the array converts to [1], then that's the optimal value.Example 2:
Input: stones = [31,26,33,21,40]
Output: 5
Constraints:
1 <= stones.length <= 301 <= stones[i] <= 100
Approaches
3 approaches with complexity analysis and trade-offs.
This approach explores every possible way to partition the stones into two groups. For each stone, we can either place it in the first group or the second. This creates 2^n possible partitions, where n is the number of stones. We can implement this using a recursive function that tries both possibilities for each stone and finds the minimum possible difference between the two groups' sums.
Algorithm
- The core idea is to simulate the smashing process by partitioning the stones into two groups. The final weight is the absolute difference between the total weights of these two groups.
- We define a recursive function, for example,
findMinWeight(index, sum1, sum2). index: The index of the current stone being considered.sum1,sum2: The current total weights of the two groups.- Base Case: When
indexreaches the end of thestonesarray, all stones have been assigned to a group. We return the absolute differenceabs(sum1 - sum2). - Recursive Step: For the stone at
stones[index], we explore two possibilities:- Add
stones[index]to the first group: Make a recursive callfindMinWeight(index + 1, sum1 + stones[index], sum2). - Add
stones[index]to the second group: Make a recursive callfindMinWeight(index + 1, sum1, sum2 + stones[index]).
- Add
- The function returns the minimum of the results from these two recursive calls.
- The initial call to start the process is
findMinWeight(0, 0, 0).
Walkthrough
The problem of finding the minimum last stone weight can be rephrased as partitioning the set of stones into two subsets, S1 and S2, such that the absolute difference of their sums, |sum(S1) - sum(S2)|, is minimized. This is because smashing two stones x and y to get y - x is equivalent to putting them in different groups. The brute-force approach systematically explores all 2^n possible partitions. A recursive function can be designed to handle this exploration. For each stone, the function makes two recursive calls: one where the stone is added to S1 and another where it's added to S2. The base case for the recursion is when all stones have been assigned, at which point we compute the difference and compare it with the minimum difference found so far.
class Solution { public int lastStoneWeightII(int[] stones) { return findMinWeight(stones, 0, 0, 0); } private int findMinWeight(int[] stones, int index, int sum1, int sum2) { if (index == stones.length) { return Math.abs(sum1 - sum2); } // Option 1: Add the current stone to the first group int diff1 = findMinWeight(stones, index + 1, sum1 + stones[index], sum2); // Option 2: Add the current stone to the second group int diff2 = findMinWeight(stones, index + 1, sum1, sum2 + stones[index]); return Math.min(diff1, diff2); }}Complexity
Time
O(2^n), where `n` is the number of stones. For each stone, we make two recursive calls, leading to an exponential number of computations.
Space
O(n), where `n` is the number of stones. This space is used by the recursion call stack.
Trade-offs
Pros
Conceptually simple and straightforward to implement.
Cons
Extremely inefficient due to its exponential time complexity.
Will result in a 'Time Limit Exceeded' (TLE) error for the given constraints.
The brute-force approach suffers from re-computing solutions to the same subproblems. We can optimize this using memoization, a top-down dynamic programming technique. The problem can be transformed into finding a subset of stones whose sum is as close as possible to half of the total sum of all stones. This is a classic 0/1 Knapsack problem. We use a recursive function with a memoization table (e.g., a 2D array) to store the results of subproblems, defined by the current stone index and the remaining capacity (target sum).
Algorithm
- Reframe the problem: We want to partition the stones into two sets,
S1andS2, to minimize|sum(S1) - sum(S2)|. LettotalSumbe the sum of all stones. Thensum(S2) = totalSum - sum(S1). We want to minimize|sum(S1) - (totalSum - sum(S1))| = |2*sum(S1) - totalSum|. This is minimized whensum(S1)is as close as possible tototalSum / 2. - The problem is now a 0/1 Knapsack problem: find a subset of stones whose sum is maximized but does not exceed
capacity = totalSum / 2. - Define a recursive function
findMaxSum(index, capacity)with memoization. - Create a memoization table
memo[n][capacity+1]to store results of subproblems. - Base Case: If
index < 0orcapacity <= 0, return 0. - Memoization Check: If
memo[index][capacity]is already computed, return the stored value. - Recursive Step: For
stones[index]:- Exclude option:
findMaxSum(index - 1, capacity). - Include option (if
stones[index] <= capacity):stones[index] + findMaxSum(index - 1, capacity - stones[index]).
- Exclude option:
- The result for the current state is the maximum of the two options. Store it in the memo table.
- The initial call is
findMaxSum(stones.length - 1, totalSum / 2)to getmaxSum. - The final answer is
totalSum - 2 * maxSum.
Walkthrough
By observing that the final weight is |sum(S1) - sum(S2)|, and sum(S1) + sum(S2) = totalSum, we can rewrite the expression to minimize as totalSum - 2*sum(S1), assuming sum(S1) <= sum(S2). To minimize this, we need to maximize sum(S1) such that sum(S1) <= totalSum / 2. This is the 0/1 Knapsack problem where item weights and values are the stone weights, and the knapsack capacity is totalSum / 2.
We define a function findMaxSum(index, capacity) that returns the maximum subset sum from stones up to index that fits within capacity. A 2D array memo stores the results to avoid redundant calculations.
class Solution { private Integer[][] memo; public int lastStoneWeightII(int[] stones) { int totalSum = 0; for (int stone : stones) { totalSum += stone; } int target = totalSum / 2; memo = new Integer[stones.length][target + 1]; int maxSum = findMaxSum(stones, stones.length - 1, target); return totalSum - 2 * maxSum; } private int findMaxSum(int[] stones, int index, int capacity) { if (index < 0 || capacity <= 0) { return 0; } if (memo[index][capacity] != null) { return memo[index][capacity]; } // Option 1: Exclude the current stone int excludeSum = findMaxSum(stones, index - 1, capacity); // Option 2: Include the current stone if it fits int includeSum = 0; if (stones[index] <= capacity) { includeSum = stones[index] + findMaxSum(stones, index - 1, capacity - stones[index]); } memo[index][capacity] = Math.max(includeSum, excludeSum); return memo[index][capacity]; }}Complexity
Time
O(n * totalSum), where `n` is the number of stones. Each state `(index, capacity)` is computed only once.
Space
O(n * totalSum), where `n` is the number of stones. This is for the memoization table and the recursion stack.
Trade-offs
Pros
Significantly more efficient than brute-force.
Guaranteed to find the optimal solution within the time limits for the given constraints.
Cons
The space complexity of O(n * totalSum) can be large if the total sum is very high, although it's acceptable for the given constraints.
This is an iterative, bottom-up version of the dynamic programming solution. It solves the same 0/1 Knapsack problem but builds the solution from the ground up, which is often more efficient in practice and can be optimized for space. We use a boolean array to keep track of all possible subset sums that can be formed using the stones.
Algorithm
- As with the memoization approach, transform the problem into finding the maximum subset sum
Sthat is less than or equal tototalSum / 2. - Calculate
totalSumof all stones and settarget = totalSum / 2. - Create a 1D boolean array
dpof sizetarget + 1.dp[j]will betrueif a subset with sumjis achievable. - Initialize
dp[0] = true, as a sum of 0 is always possible (by choosing no stones). - Iterate through each
stonein thestonesarray:- For each stone, update the
dparray. Iterate fromj = targetdown tostone. - Set
dp[j] = dp[j] || dp[j - stone]. This update rule means a sumjis now possible if it was already possible before considering the current stone, or if a sumj - stonewas possible (to which we now add the currentstone). The backward iteration is crucial to prevent using the same stone multiple times in one subset.
- For each stone, update the
- After processing all stones, find the largest index
maxSumfromtargetdown to 0 for whichdp[maxSum]istrue. - The final result is
totalSum - 2 * maxSum.
Walkthrough
The bottom-up DP approach iteratively determines all achievable subset sums. We use a boolean array dp where dp[i] indicates whether a subset sum of i is possible. The size of this array is (totalSum / 2) + 1. We initialize dp[0] to true. Then, for each stone, we iterate through the possible sums from target down to the stone's weight. This backward iteration ensures that each stone is considered only once for each sum calculation. After filling the dp table, we find the largest sum s for which dp[s] is true. This s is the sum of one partition that is closest to totalSum / 2. The minimum difference is then totalSum - 2*s.
class Solution { public int lastStoneWeightII(int[] stones) { int totalSum = 0; for (int stone : stones) { totalSum += stone; } int target = totalSum / 2; boolean[] dp = new boolean[target + 1]; dp[0] = true; for (int stone : stones) { for (int j = target; j >= stone; j--) { dp[j] = dp[j] || dp[j - stone]; } } int maxSum = 0; for (int j = target; j >= 0; j--) { if (dp[j]) { maxSum = j; break; } } return totalSum - 2 * maxSum; }}Complexity
Time
O(n * totalSum). We have a nested loop iterating through `n` stones and sums up to `target` (`totalSum / 2`).
Space
O(totalSum). We only need a 1D array of size `target + 1` to store the reachable sums.
Trade-offs
Pros
Optimal time complexity for this problem's constraints.
Most space-efficient DP solution, using only O(totalSum) space.
Iterative approach avoids recursion overhead, potentially leading to faster execution.
Cons
The logic, especially the backward iteration for the sum, might be less intuitive at first glance compared to the recursive formulation.
Solutions
Solution
class Solution {public int lastStoneWeightII(int[] stones) { int s = 0; for (int v : stones) { s += v; } int m = stones.length; int n = s >> 1; int[] dp = new int[n + 1]; for (int v : stones) { for (int j = n; j >= v; --j) { dp[j] = Math.max(dp[j], dp[j - v] + v); } } return s - dp[n] * 2; }}/** * @param {number[]} stones * @return {number} */ var lastStoneWeightII = function (stones) { let s = 0; for (let v of stones) { s += v; } const n = s >> 1; let dp = new Array(n + 1).fill(0); for (let v of stones) { for (let j = n; j >= v; --j) { dp[j] = Math.max(dp[j], dp[j - v] + v); } } return s - dp[n] * 2; };class Solution: def lastStoneWeightII(self, stones: List[int]) -> int: s = sum(stones) m, n = len(stones), s >> 1 dp = [0] * (n + 1) for v in stones: for j in range(n, v - 1, - 1): dp[j] = max(dp[j], dp[j - v] + v) return s - dp[- 1] * 2class Solution {public: int lastStoneWeightII(vector<int> &stones) { int s = accumulate(stones.begin(), stones.end(), 0); int n = s >> 1; vector<int> dp(n + 1); for (int &v : stones) for (int j = n; j >= v; --j) dp[j] = max(dp[j], dp[j - v] + v); return s - dp[n] * 2; }};Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.