Taro Logo

Stone Game III

Hard
Asked by:
Profile picture
Profile picture
Profile picture
39 views
Topics:
ArraysDynamic Programming

Alice and Bob continue their games with piles of stones. There are several stones arranged in a row, and each stone has an associated value which is an integer given in the array stoneValue.

Alice and Bob take turns, with Alice starting first. On each player's turn, that player can take 1, 2, or 3 stones from the first remaining stones in the row.

The score of each player is the sum of the values of the stones taken. The score of each player is 0 initially.

The objective of the game is to end with the highest score, and the winner is the player with the highest score and there could be a tie. The game continues until all the stones have been taken.

Assume Alice and Bob play optimally.

Return "Alice" if Alice will win, "Bob" if Bob will win, or "Tie" if they will end the game with the same score.

Example 1:

Input: stoneValue = [1,2,3,7]
Output: "Bob"
Explanation: Alice will always lose. Her best move will be to take three piles and the score become 6. Now the score of Bob is 7 and Bob wins.

Example 2:

Input: stoneValue = [1,2,3,-9]
Output: "Alice"
Explanation: Alice must choose all the three piles at the first move to win and leave Bob with negative score.
If Alice chooses one pile her score will be 1 and the next move Bob's score becomes 5. In the next move, Alice will take the pile with value = -9 and lose.
If Alice chooses two piles her score will be 3 and the next move Bob's score becomes 3. In the next move, Alice will take the pile with value = -9 and also lose.
Remember that both play optimally so here Alice will choose the scenario that makes her win.

Example 3:

Input: stoneValue = [1,2,3,6]
Output: "Tie"
Explanation: Alice cannot win this game. She can end the game in a draw if she decided to choose all the first three piles, otherwise she will lose.

Constraints:

  • 1 <= stoneValue.length <= 5 * 104
  • -1000 <= stoneValue[i] <= 1000

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 are the constraints on the size of the `stoneValue` array, and what is the range of values for each stone?
  2. Can the stone values be negative, zero, or only positive?
  3. If Alice and Bob end with the same score, should I return "Tie"?
  4. Is the length of the stoneValue array always greater than 0?
  5. Can you provide a small example input and expected output to ensure I understand the problem correctly?

Brute Force Solution

Approach

The goal is to find the best strategy for Alice and Bob to pick stones. A brute force approach explores every possible combination of stone choices that both Alice and Bob can make to determine if Alice can win.

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

  1. Consider that Alice always goes first and can take either one, two, or three stones from the pile.
  2. Figure out Alice's score if she takes one stone, and then what happens when Bob gets to play. Also figure out Alice's score if she takes two stones and then what happens when Bob gets to play, and so on.
  3. Now it's Bob's turn. Assume Bob always plays perfectly to minimize Alice's score. For each of Alice's possible moves, figure out all the moves that Bob can make.
  4. Bob can also choose to take one, two, or three stones, and each of his choices will impact Alice's final score.
  5. Continue to alternate turns between Alice and Bob, exploring all possible combinations of choices until all stones are taken.
  6. Once all stones are gone, determine who has the higher score given the stone choices that occurred in that scenario.
  7. Repeat this process for every possible starting move by Alice, and every subsequent move by Bob and Alice until the game ends. This process allows us to find the optimal move for Alice.
  8. Finally, from all the game outcomes, determine whether Alice's score is greater than Bob's score. If it is, then Alice wins; otherwise, she loses or ties.

Code Implementation

def stone_game_iii_brute_force(stone_values):
    def get_winner(current_stone_values, alice_score, bob_score, turn):
        if not current_stone_values:
            if alice_score > bob_score:
                return 1  # Alice wins
            elif alice_score < bob_score:
                return -1 # Bob wins
            else:
                return 0  # Tie

        if turn == 'Alice':
            best_score = float('-inf')
            for stones_taken in range(1, min(4, len(current_stone_values) + 1)):
                new_alice_score = alice_score + sum(current_stone_values[:stones_taken])
                outcome = get_winner(current_stone_values[stones_taken:], new_alice_score, bob_score, 'Bob')
                best_score = max(best_score, outcome)
            return best_score

        else:
            best_score = float('inf')
            for stones_taken in range(1, min(4, len(current_stone_values) + 1)):
                new_bob_score = bob_score + sum(current_stone_values[:stones_taken])
                outcome = get_winner(current_stone_values[stones_taken:], alice_score, new_bob_score, 'Alice')
                best_score = min(best_score, outcome)
            return best_score

    # Alice always goes first
    result = get_winner(stone_values, 0, 0, 'Alice')

    # Determine the winner based on the final score
    if result == 1:
        return "Alice"
    elif result == -1:
        return "Bob"
    else:
        return "Tie"

Big(O) Analysis

Time Complexity
O(3^n)The brute force approach explores every possible combination of taking 1, 2, or 3 stones at each turn. In the worst-case scenario, where each player always has at least 3 stones to choose from, we have a branching factor of 3 at each level of the recursion tree. Since there are 'n' stones initially and each player takes at least 1 stone, the depth of the recursion is 'n'. Thus, the total number of possible game scenarios (and therefore the time complexity) grows exponentially with the number of stones, approximating 3^n. This makes the algorithm's runtime O(3^n).
Space Complexity
O(N)The brute force approach involves exploring all possible combinations of stone choices through recursion. The depth of the recursion can go as deep as N, where N is the number of stones, as each level represents a choice of taking 1, 2, or 3 stones. Each recursive call creates a new stack frame, contributing to auxiliary space. Therefore, the space complexity is proportional to the maximum depth of the recursion, which is O(N).

Optimal Solution

Approach

The core idea is to determine the maximum possible score Alice can achieve given that both Alice and Bob play optimally. We solve this using a strategy where we work backwards from the end, always picking the best possible move at each step.

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

  1. Start by thinking about the very end of the game. If there are only one, two, or three stones left, Alice simply takes all of them.
  2. Now, imagine you're a step earlier. Alice has a choice: take the next one stone, the next two stones, or the next three stones. She wants to maximize her score.
  3. To figure out which is the best choice, Alice needs to know what Bob will do *after* her turn. So, for each of Alice's possible choices, figure out Bob's best response (which is to maximize *his* score).
  4. Remember, Bob is also playing optimally, which means he'll try to minimize Alice's future score (since maximizing his own score is the same as minimizing Alice's).
  5. Alice will pick the move that leads to the *highest* possible score for her, assuming Bob always plays his best.
  6. Continue working backwards, repeating this process: figure out all of Alice's possible moves, figure out Bob's best response to each move, and then have Alice pick the move that gives her the best final score. Store the results of these calculations to avoid repeating work.
  7. Once you've worked all the way back to the beginning, you'll know the maximum score Alice can get. Compare that score to the total score of all stones and to Bob's maximum score to determine if Alice wins, loses, or ties.

Code Implementation

def stone_game_iii(stone_values):
    number_of_stones = len(stone_values)
    memo = {}

    def calculate_max_score(start_index):
        if start_index >= number_of_stones:
            return 0

        if start_index in memo:
            return memo[start_index]

        max_alice_score = float('-inf')

        # Alice can take 1, 2, or 3 stones
        for stones_to_take in range(1, 4):
            if start_index + stones_to_take <= number_of_stones:
                current_score = sum(stone_values[start_index:start_index + stones_to_take])

                # Bob plays optimally to minimize Alice's score
                remaining_score = calculate_max_score(start_index + stones_to_take)
                max_alice_score = max(max_alice_score, current_score - remaining_score)

        memo[start_index] = max_alice_score
        return max_alice_score

    alice_max_score = calculate_max_score(0)
    total_stone_value = sum(stone_values)
    bob_max_score = total_stone_value - alice_max_score

    # Determine the winner based on Alice's and Bob's scores
    if alice_max_score > bob_max_score:
        return "Alice"
    elif alice_max_score < bob_max_score:
        return "Bob"
    else:
        return "Tie"

Big(O) Analysis

Time Complexity
O(n)The algorithm iterates backwards from the end of the stone pile, considering at most three possible moves (taking 1, 2, or 3 stones) at each position. For each of the n positions in the stone pile, a constant amount of work is performed to determine Alice's optimal score from that position onwards. Because the number of possible moves at each step is fixed (at most 3) and independent of n, the overall time complexity is directly proportional to the number of stones n.
Space Complexity
O(N)The solution uses dynamic programming and works backwards to calculate the maximum score Alice can achieve. To avoid recomputation, the results of these calculations are stored, implying the use of an auxiliary data structure. Since the solution iterates and stores results for each possible game state from the end to the beginning, it requires storing intermediate scores for subproblems related to each index of the input stone array. Therefore, an array or similar data structure of size N is used, where N is the number of stones. The space complexity is thus O(N).

Edge Cases

Null or empty stoneValue array
How to Handle:
Return 0; Alice wins by default if there are no stones.
stoneValue array with only one element
How to Handle:
Alice takes the stone; return "Alice" if positive, "Bob" if negative, or "Tie" if zero.
stoneValue array with only two elements
How to Handle:
Calculate Alice's max score for taking either one or two stones and compare to remaining stones, determining the winner.
Maximum sized stoneValue array (e.g., 500 stones)
How to Handle:
Dynamic programming approach ensures the solution scales efficiently to avoid exceeding time limits.
stoneValue array contains all zeros
How to Handle:
Both players always tie since neither can gain an advantage; return "Tie".
stoneValue array contains large positive and negative numbers
How to Handle:
Ensure the DP table uses a data type (e.g., int) large enough to avoid integer overflow when calculating scores.
All stone values are negative
How to Handle:
Alice wants to minimize Bob's score, leading to a different optimal strategy that the solution must correctly handle.
stoneValue array contains only positive numbers.
How to Handle:
Alice tries to maximize her score.