Facebook Pixel

1753. Maximum Score From Removing Stones

Problem Description

You have three piles of stones with sizes a, b, and c. In each turn of the game, you must:

  • Choose two different non-empty piles
  • Remove one stone from each of the two chosen piles
  • Add 1 point to your score

The game continues until you cannot make a valid move anymore (when fewer than two piles have stones remaining).

Your goal is to find the maximum possible score you can achieve by making optimal moves.

For example, if you have piles of sizes a = 2, b = 4, and c = 6:

  • You could take stones from piles b and c, leaving you with (2, 3, 5) and scoring 1 point
  • Continue taking from the two largest piles to maximize your score
  • The game ends when at most one pile has stones remaining

The solution approach repeatedly takes stones from the two largest piles. By sorting the piles after each move and always choosing the two largest piles (indices 1 and 2 in the sorted array), we ensure we can make the maximum number of moves possible. The algorithm continues until the middle pile becomes empty, at which point we can no longer make valid moves since we need at least two non-empty piles.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is to think about when the game ends and work backwards. The game stops when we have at most one non-empty pile. This means we want to distribute our moves in a way that uses up the stones as evenly as possible.

Consider what happens if one pile is much larger than the others. If pile c is huge while a and b are small, we'll quickly exhaust a and b, leaving many stones unused in c. This limits our score.

The optimal strategy is to always take stones from the two largest piles. Why? By repeatedly reducing the largest piles, we prevent any single pile from becoming too dominant. This keeps all piles relatively balanced, allowing us to continue making moves for as long as possible.

Think of it like balancing water levels in three containers - if we always drain from the two fullest containers, we maximize the time before one runs dry.

Mathematically, the maximum score is actually min(sum(a, b, c) / 2, a + b + c - max(a, b, c)). The first part sum/2 represents the theoretical maximum if we could perfectly pair all stones. The second part handles the case where one pile is larger than the sum of the other two - in this case, we're limited by the smaller piles.

The greedy approach of always choosing the two largest piles naturally achieves this maximum. By sorting after each move and selecting piles at indices 1 and 2, we ensure we're always making the optimal choice that keeps the piles as balanced as possible.

Learn more about Greedy, Math and Heap (Priority Queue) patterns.

Solution Approach

The implementation uses a greedy algorithm with sorting to maintain the optimal selection of piles at each step.

Data Structure: We use a list s to store the three pile sizes, which allows us to sort them repeatedly.

Algorithm Steps:

  1. Initialize: Create a sorted list s = sorted([a, b, c]) so that s[0] ≤ s[1] ≤ s[2]. This ensures we have the piles ordered from smallest to largest.

  2. Main Loop: Continue while s[1] > 0 (the middle pile is non-empty):

    • Why check s[1]? If the middle pile is empty, then the smallest pile s[0] must also be empty (since they're sorted), meaning we have at most one non-empty pile and cannot make valid moves.
  3. Make a Move: In each iteration:

    • Increment the answer counter: ans += 1
    • Remove one stone from the two largest piles: s[1] -= 1 and s[2] -= 1
    • Re-sort the list: s.sort()
  4. Re-sorting Logic: After taking stones from piles at positions 1 and 2, the ordering might change. For example, if we had [1, 3, 5] and took from the larger two, we'd get [1, 2, 4]. The sorting ensures we always identify the correct two largest piles for the next move.

  5. Return: When the loop exits (middle pile becomes 0), return ans as the maximum score.

Time Complexity: O(n log n) where n is the total number of moves (maximum of (a+b+c)/2). Each iteration involves sorting 3 elements, which is O(1), but we do this n times.

Space Complexity: O(1) as we only use a fixed-size list of 3 elements.

The beauty of this approach is its simplicity - by always maintaining sorted order and greedily selecting the two largest piles, we automatically achieve the maximum possible score without complex calculations.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with piles a = 2, b = 4, and c = 6.

Initial Setup:

  • Start with s = sorted([2, 4, 6]) = [2, 4, 6]
  • Score = 0

Move 1:

  • Check: s[1] = 4 > 0 ✓ (can continue)
  • Take from two largest piles: s[1] = 4-1 = 3, s[2] = 6-1 = 5
  • Current state: [2, 3, 5]
  • Sort: [2, 3, 5] (already sorted)
  • Score = 1

Move 2:

  • Check: s[1] = 3 > 0
  • Take from two largest: s[1] = 3-1 = 2, s[2] = 5-1 = 4
  • Current state: [2, 2, 4]
  • Sort: [2, 2, 4]
  • Score = 2

Move 3:

  • Check: s[1] = 2 > 0
  • Take from two largest: s[1] = 2-1 = 1, s[2] = 4-1 = 3
  • Current state: [2, 1, 3]
  • Sort: [1, 2, 3]
  • Score = 3

Move 4:

  • Check: s[1] = 2 > 0
  • Take from two largest: s[1] = 2-1 = 1, s[2] = 3-1 = 2
  • Current state: [1, 1, 2]
  • Sort: [1, 1, 2]
  • Score = 4

Move 5:

  • Check: s[1] = 1 > 0
  • Take from two largest: s[1] = 1-1 = 0, s[2] = 2-1 = 1
  • Current state: [1, 0, 1]
  • Sort: [0, 1, 1]
  • Score = 5

Move 6:

  • Check: s[1] = 1 > 0
  • Take from two largest: s[1] = 1-1 = 0, s[2] = 1-1 = 0
  • Current state: [0, 0, 0]
  • Sort: [0, 0, 0]
  • Score = 6

Termination:

  • Check: s[1] = 0 (not > 0) ✗
  • Game ends with final score = 6

Notice how by always taking from the two largest piles, we avoided depleting any pile too quickly. This balanced approach allowed us to make 6 moves total, which equals (2+4+6)/2 = 6, the theoretical maximum.

Solution Implementation

1class Solution:
2    def maximumScore(self, a: int, b: int, c: int) -> int:
3        """
4        Calculate the maximum score by repeatedly taking one stone from two different piles.
5        Each operation removes one stone from two piles and adds 1 to the score.
6      
7        Args:
8            a: Number of stones in pile A
9            b: Number of stones in pile B
10            c: Number of stones in pile C
11      
12        Returns:
13            Maximum possible score
14        """
15        # Sort the three piles in ascending order
16        piles = sorted([a, b, c])
17        score = 0
18      
19        # Keep taking stones from the two largest piles until the middle pile is empty
20        # This ensures we maximize the number of operations
21        while piles[1] > 0:
22            score += 1
23            piles[1] -= 1  # Take one stone from the middle pile
24            piles[2] -= 1  # Take one stone from the largest pile
25          
26            # Re-sort to maintain ascending order
27            piles.sort()
28      
29        return score
30```
31
32Actually, there's a mathematical optimization we can apply here. The maximum score is either the sum of the two smaller piles (if they together are less than or equal to the largest), or half of the total sum:
33
34```python3
35class Solution:
36    def maximumScore(self, a: int, b: int, c: int) -> int:
37        """
38        Calculate the maximum score by repeatedly taking one stone from two different piles.
39      
40        The optimal strategy is to always take from the two largest piles.
41        The maximum score is min(sum//2, sum - max_pile) where sum is the total stones.
42      
43        Args:
44            a: Number of stones in pile A
45            b: Number of stones in pile B
46            c: Number of stones in pile C
47      
48        Returns:
49            Maximum possible score
50        """
51        # Calculate total sum and find the maximum pile
52        total_sum = a + b + c
53        max_pile = max(a, b, c)
54      
55        # The answer is the minimum of:
56        # 1. Half of total stones (best case scenario)
57        # 2. Sum of two smaller piles (when largest pile is too big)
58        return min(total_sum // 2, total_sum - max_pile)
59
1class Solution {
2    public int maximumScore(int a, int b, int c) {
3        // Create an array to store the three pile sizes
4        int[] piles = new int[] {a, b, c};
5      
6        // Sort the piles in ascending order
7        Arrays.sort(piles);
8      
9        // Initialize the counter for number of operations
10        int operationCount = 0;
11      
12        // Continue while we have at least two non-empty piles
13        // piles[1] > 0 ensures both piles[1] and piles[2] are non-empty
14        // since array is sorted
15        while (piles[1] > 0) {
16            // Increment the operation counter
17            operationCount++;
18          
19            // Remove one stone from the two largest piles
20            piles[1]--;
21            piles[2]--;
22          
23            // Re-sort to maintain ascending order for next iteration
24            Arrays.sort(piles);
25        }
26      
27        // Return the total number of operations performed
28        return operationCount;
29    }
30}
31
1class Solution {
2public:
3    int maximumScore(int a, int b, int c) {
4        // Store the three pile sizes in a vector for easier manipulation
5        vector<int> piles = {a, b, c};
6      
7        // Sort piles in ascending order: piles[0] <= piles[1] <= piles[2]
8        sort(piles.begin(), piles.end());
9      
10        int score = 0;
11      
12        // Continue taking stones while the two smallest piles both have stones
13        // We always take from the two largest piles after sorting
14        while (piles[1] > 0) {
15            // Increment score for each valid operation
16            ++score;
17          
18            // Remove one stone from the two largest piles
19            piles[1]--;
20            piles[2]--;
21          
22            // Re-sort to maintain the invariant: piles[0] <= piles[1] <= piles[2]
23            sort(piles.begin(), piles.end());
24        }
25      
26        return score;
27    }
28};
29
1function maximumScore(a: number, b: number, c: number): number {
2    // Store the three pile sizes in an array for easier manipulation
3    let piles: number[] = [a, b, c];
4  
5    // Sort piles in ascending order: piles[0] <= piles[1] <= piles[2]
6    piles.sort((x, y) => x - y);
7  
8    let score: number = 0;
9  
10    // Continue taking stones while the two smallest piles both have stones
11    // We always take from the two largest piles after sorting
12    while (piles[1] > 0) {
13        // Increment score for each valid operation
14        score++;
15      
16        // Remove one stone from the two largest piles
17        piles[1]--;
18        piles[2]--;
19      
20        // Re-sort to maintain the invariant: piles[0] <= piles[1] <= piles[2]
21        piles.sort((x, y) => x - y);
22    }
23  
24    return score;
25}
26

Time and Space Complexity

Time Complexity: O(min(a, b, c) * log 3) which simplifies to O(min(a, b, c))

The code uses a while loop that continues until s[1] becomes 0. In each iteration:

  • Two elements are decremented by 1 (s[1] and s[2])
  • The array is sorted using s.sort(), which takes O(log 3) = O(1) time for a fixed-size array of 3 elements

The loop runs at most min(a, b, c) times because:

  • After sorting, s[0] ≤ s[1] ≤ s[2]
  • Each iteration decrements the two larger values
  • The loop terminates when the middle element s[1] reaches 0
  • The maximum number of iterations occurs when we can pair the smallest pile with others until it's exhausted

Therefore, the overall time complexity is O(min(a, b, c) * 1) = O(min(a, b, c))

Space Complexity: O(1)

The code uses:

  • A fixed-size list s containing 3 elements
  • A single integer variable ans

No additional space that scales with input size is used, resulting in constant space complexity O(1).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting to Re-sort After Each Move

One of the most common mistakes is taking stones from the two largest piles but forgetting to re-sort the array afterward. This leads to incorrect pile selection in subsequent iterations.

Incorrect Implementation:

def maximumScore(self, a: int, b: int, c: int) -> int:
    piles = sorted([a, b, c])
    score = 0
  
    while piles[1] > 0:
        score += 1
        piles[1] -= 1
        piles[2] -= 1
        # MISSING: piles.sort() - This will cause wrong selections!
  
    return score

Why it fails: After removing stones, the relative sizes change. For example, starting with [2, 5, 6], after one move we have [2, 4, 5]. Without sorting, the next iteration would incorrectly modify indices 1 and 2 again, giving [2, 3, 4], when it should select the actual two largest piles.

Solution: Always sort after each modification to maintain the invariant that piles[0] ≤ piles[1] ≤ piles[2].

2. Incorrect Termination Condition

Another pitfall is using the wrong condition to determine when the game ends.

Incorrect Implementations:

# Wrong: Checking only the largest pile
while piles[2] > 0:  # Game might end earlier!
  
# Wrong: Checking the smallest pile
while piles[0] > 0:  # Game continues even after this!
  
# Wrong: Checking if we can make exactly one more move
while piles[1] > 0 and piles[2] > 0:  # Redundant check

Why checking piles[1] > 0 is correct: When the middle pile becomes 0, we know:

  • The smallest pile piles[0] must also be 0 (due to sorting)
  • We have at most one non-empty pile (the largest)
  • We cannot make valid moves with fewer than 2 non-empty piles

3. Attempting to Optimize by Taking From Different Piles

Some might think taking from the smallest and largest pile could be beneficial in certain cases to "balance" the piles.

Incorrect Strategy:

def maximumScore(self, a: int, b: int, c: int) -> int:
    piles = sorted([a, b, c])
    score = 0
  
    while piles[1] > 0:
        score += 1
        # Wrong: Trying to "balance" by taking from smallest and largest
        if piles[2] - piles[0] > piles[1]:
            piles[0] -= 1
            piles[2] -= 1
        else:
            piles[1] -= 1
            piles[2] -= 1
        piles.sort()
  
    return score

Why it's wrong: The greedy approach of always taking from the two largest piles is mathematically optimal. Trying to balance piles can actually reduce the total number of moves possible.

4. Integer Overflow in Mathematical Solution

When using the optimized mathematical formula, be careful with the order of operations.

Potential Issue:

# In some languages, this might overflow for large values
return min((a + b + c) // 2, a + b + c - max(a, b, c))

Better Approach:

def maximumScore(self, a: int, b: int, c: int) -> int:
    total = a + b + c
    max_pile = max(a, b, c)
  
    # Store intermediate results to avoid recalculation
    return min(total // 2, total - max_pile)

5. Not Handling Edge Cases

While the problem typically guarantees non-negative inputs, it's good practice to consider edge cases:

Robust Implementation:

def maximumScore(self, a: int, b: int, c: int) -> int:
    # Handle edge case where all piles are 0
    if a == 0 and b == 0 and c == 0:
        return 0
  
    # Handle case where only one pile has stones
    non_zero_count = sum(1 for x in [a, b, c] if x > 0)
    if non_zero_count < 2:
        return 0
  
    # Proceed with normal algorithm
    total = a + b + c
    max_pile = max(a, b, c)
    return min(total // 2, total - max_pile)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the relationship between a tree and a graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More