Taro Logo

Stone Game II

Medium
Amazon logo
Amazon
3 views
Topics:
Dynamic Programming

Alice and Bob Stone Game

Alice and Bob continue their games with piles of stones. There are a number of piles arranged in a row, and each pile has a positive integer number of stones piles[i]. The objective of the game is to end with the most stones.

Alice and Bob take turns, with Alice starting first.

On each player's turn, that player can take all the stones in the first X remaining piles, where 1 <= X <= 2M. Then, we set M = max(M, X). Initially, M = 1.

The game continues until all the stones have been taken.

Assuming Alice and Bob play optimally, return the maximum number of stones Alice can get.

Example 1:

piles = [2,7,9,4,4]

In this example, the optimal strategy for Alice results in her collecting 10 stones. For instance:

  • If Alice takes one pile (2) at the beginning, Bob takes two piles (7,9), then Alice takes the remaining two piles (4,4). Alice's total is 2 + 4 + 4 = 10.
  • If Alice takes two piles (2,7) at the beginning, Bob takes the remaining three piles (9,4,4). Alice's total is 2 + 7 = 9.

Therefore, 10 is the maximum Alice can get.

Example 2:

piles = [1,2,3,4,5,100]

What is the maximum number of stones Alice can get assuming optimal play?

Constraints:

  • 1 <= piles.length <= 100
  • 1 <= piles[i] <= 10^4

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What is the range of values for each pile of stones in the input array?
  2. Can the number of piles, N, be zero or one? If so, what should the output be in those cases?
  3. If both Alice and Bob play optimally, and Alice's score is tied with Bob's, what value should I return: Alice's score, or is there a specified value for ties?
  4. Is the input array guaranteed to be non-empty and contain only positive integers?
  5. What is the maximum value of N, the number of stone piles, for the input array?

Brute Force Solution

Approach

We're trying to find the maximum number of stones a player can get in a game by trying every possible combination of moves. The brute force approach explores all possible gameplays from start to finish, figuring out the best one. We consider every choice a player can make at each turn and see where it leads.

Here's how the algorithm would work step-by-step:

  1. Start by considering the first player's possible choices: they can pick one pile of stones, or two piles, or more (up to a certain number of piles based on the game's rules).
  2. For each of these choices, figure out what's left for the second player to pick from.
  3. Now, imagine the second player does the same thing: they consider all their possible choices (picking one or more piles).
  4. Keep going back and forth between the players, each time considering all possible choices based on what's left.
  5. Eventually, the game ends when there are no more stones to pick.
  6. At the end of each possible game, figure out how many stones the first player collected.
  7. Compare the number of stones the first player got in all the different possible games, and choose the path that leads to the highest score for the first player.

Code Implementation

def stone_game_ii_brute_force(piles):
    number_of_piles = len(piles)

    def solve(current_piles, max_number_of_picks):
        if not current_piles:
            return 0

        max_score = float('-inf')

        # Iterate through all possible number of piles to pick
        for number_of_picks in range(1, 2 * max_number_of_picks + 1):
            if number_of_picks > len(current_piles):
                break

            # Calculate score from picking the number of piles
            current_score = sum(current_piles[:number_of_picks])

            # Calculate the opponent's best score from the remaining piles.
            opponent_score = solve(
                current_piles[number_of_picks:],
                max(max_number_of_picks, number_of_picks)
            )

            # Update the max score
            max_score = max(max_score, current_score - opponent_score)

        return max_score

    # The first player seeks to maximize their score.
    return (sum(piles) + solve(piles, 1)) // 2

Big(O) Analysis

Time Complexity
O(2^n)The described brute force approach explores all possible gameplays. At each step, the first player has multiple choices on how many piles to take, and for each choice, the game continues recursively with the second player. In the worst-case scenario, especially in early stages of the game, there could be an exponential number of possible game states to explore, roughly growing as 2^n where n represents the total number of stones because each player’s choices create a branching decision tree. This makes the time complexity exponential.
Space Complexity
O(N*N*logN)The brute force approach explores all possible gameplays using recursion. The maximum depth of the recursion is proportional to N, the number of piles, because at each step, a player takes some piles. At each level of recursion, we are making logN calls due to the multiple possible choices the player can make which is a function of M (1 <= M <= piles.length). A memoization table of size N*N is used to store the result of subproblems to avoid recomputation. Therefore, the auxiliary space complexity is O(N*N*logN).

Optimal Solution

Approach

The core idea is to use a 'best move' approach at each step, working backwards to figure out the overall optimal strategy. It involves knowing what your maximum score can be from any given point in the game, assuming both you and your opponent play perfectly.

Here's how the algorithm would work step-by-step:

  1. Think about the end of the game first. If there are only a few piles of stones left, the current player will simply take all of them.
  2. Now, imagine there are a few more piles. Figure out the best score the current player can guarantee for themselves, knowing the next player will also try to maximize their own score.
  3. To do this, consider all the possible number of piles the current player can take (as dictated by the rules). For each possibility, figure out the other player's best possible score in the remaining game (which we've already calculated in the previous steps).
  4. The current player will choose the option that gives them the maximum score, knowing the other player will then play optimally.
  5. Repeat this process, working backwards from the end of the game, until you reach the beginning.
  6. The result at the beginning of the game tells you the maximum score the first player can achieve if both players play perfectly.

Code Implementation

def stone_game_ii(piles):
    number_of_piles = len(piles)
    prefix_sum = [0] * (number_of_piles + 1)
    for i in range(number_of_piles):
        prefix_sum[i + 1] = prefix_sum[i] + piles[i]

    memo = {}

    def get_score_difference(current_index, max_piles):
        if current_index >= number_of_piles:
            return 0
        
        if (current_index, max_piles) in memo:
            return memo[(current_index, max_piles)]

        max_score = float('-inf')
        # Try all possible numbers of piles to take
        for piles_taken in range(1, 2 * max_piles + 1):
            if current_index + piles_taken > number_of_piles:
                break

            # Calculate current player's score
            current_score = prefix_sum[number_of_piles] - prefix_sum[current_index]
            remaining_score_difference = get_score_difference(current_index + piles_taken, max(max_piles, piles_taken))

            # Determine the maximum score
            max_score = max(max_score, current_score - remaining_score_difference)

        memo[(current_index, max_piles)] = max_score
        return max_score

    # The first player's score is half the total sum plus half the difference
    first_player_score = (prefix_sum[number_of_piles] + get_score_difference(0, 1)) // 2

    return first_player_score

# Memoization is crucial to avoid redundant calculations.

Big(O) Analysis

Time Complexity
O(n^2)The algorithm uses dynamic programming to determine the optimal score. It iterates backwards through the piles array, with the outer loop going through each pile (n piles). For each pile, it considers all possible values of X (number of piles to take), where X can range up to n/2. Calculating the optimal score for each state (pile index and X) requires considering at most n/2 possibilities, resulting in approximately n * n/2 operations. This simplifies to a time complexity of O(n^2), where n is the number of piles.
Space Complexity
O(N*N)The dynamic programming approach implied by the best move strategy and working backwards creates a table to store intermediate results. The dimensions of this table relate to the current pile index and the current value of M. Because the maximum value of M can grow to at most N, where N is the number of piles, the table requires O(N*N) space to store these intermediate scores for each possible game state.

Edge Cases

CaseHow to Handle
Empty pile arrayReturn 0 as no stones can be taken when the pile is empty.
Single element pile arrayReturn the value of the single element as the first player takes it all.
All piles have zero stonesReturn 0 as no player can gain any points from these piles.
Array with very large numbers, potential integer overflow in sum calculationUse a data type that supports larger numbers (e.g., long) to prevent overflow when calculating pile sums.
Maximum size of input array based on problem constraintsEnsure the solution's time and space complexity are within acceptable bounds for the maximum allowed input size, considering memoization or dynamic programming optimizations.
M becomes larger than the remaining pilesAdjust the upper bound for the number of piles that can be taken to the number of remaining piles.
Negative pile sizes, if permitted by problem statementHandle negative pile sizes appropriately, potentially by returning an error, or allowing them and incorporating them into calculations (may require adjustments to base cases/logic).
Extreme boundary values for pile sizes (min/max integer values)Verify that calculations involving pile sizes do not result in integer overflow or underflow, especially when using memoization.