Taro Logo

Predict the Winner

Medium
Asked by:
Profile picture
Profile picture
Profile picture
Profile picture
+5
More companies
Profile picture
Profile picture
Profile picture
Profile picture
Profile picture
60 views
Topics:
ArraysDynamic ProgrammingRecursion

You are given an integer array nums. Two players are playing a game with this array: player 1 and player 2.

Player 1 and player 2 take turns, with player 1 starting first. Both players start the game with a score of 0. At each turn, the player takes one of the numbers from either end of the array (i.e., nums[0] or nums[nums.length - 1]) which reduces the size of the array by 1. The player adds the chosen number to their score. The game ends when there are no more elements in the array.

Return true if Player 1 can win the game. If the scores of both players are equal, then player 1 is still the winner, and you should also return true. You may assume that both players are playing optimally.

Example 1:

Input: nums = [1,5,2]
Output: false
Explanation: Initially, player 1 can choose between 1 and 2. 
If he chooses 2 (or 1), then player 2 can choose from 1 (or 2) and 5. If player 2 chooses 5, then player 1 will be left with 1 (or 2). 
So, final score of player 1 is 1 + 2 = 3, and player 2 is 5. 
Hence, player 1 will never be the winner and you need to return false.

Example 2:

Input: nums = [1,5,233,7]
Output: true
Explanation: Player 1 first chooses 1. Then player 2 has to choose between 5 and 7. No matter which number player 2 choose, player 1 can choose 233.
Finally, player 1 has more score (234) than player 2 (12), so you need to return True representing player1 can win.

Constraints:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 107

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 the numbers in the input array? Can they be negative, zero, or floating-point numbers?
  2. What should the function return if the input array is empty or contains only one element?
  3. Is the input array guaranteed to be non-null?
  4. In the case that player 1 and player 2 have the same total score at the end, should I return true (player 1 wins) or false (player 2 wins)?
  5. Can you provide an example of an input where player 1 does *not* win, to ensure my understanding of the losing condition is correct?

Brute Force Solution

Approach

We want to see if the first player can win a game where they and another player take turns picking numbers from the ends of a list. The brute force method explores every possible sequence of moves to see if the first player can achieve a score greater than or equal to the second player's.

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

  1. Imagine Player 1 picks either the number at the beginning or the number at the end of the list.
  2. Now, consider both of those possibilities separately. For each of Player 1's choices, imagine Player 2 also picks either the number at the beginning or the number at the end of the remaining list.
  3. Continue this process, alternating between Player 1 and Player 2, and considering all possible choices each player can make at each step.
  4. Eventually, the list will be empty. At this point, calculate the total score for Player 1 and the total score for Player 2.
  5. Repeat the entire process, starting with each initial choice Player 1 could make, until every possible game scenario is explored.
  6. After exploring all game scenarios, check if there is even one scenario where Player 1's score is greater than or equal to Player 2's score. If such a scenario exists, then Player 1 can win.

Code Implementation

def predict_the_winner_brute_force(numbers):
    def can_player_one_win(nums, player_one_score, player_two_score, turn):
        if not nums:
            # Base case: no numbers left, check the scores.
            return player_one_score >= player_two_score

        if turn == 1:
            # Player 1's turn: maximize their score.
            
            choose_first =
            can_player_one_win(nums[1:], player_one_score + nums[0],
                                 player_two_score, 2)

            choose_last =
            can_player_one_win(nums[:-1], player_one_score + nums[-1],
                                 player_two_score, 2)

            return choose_first or choose_last
        else:
            # Player 2's turn: minimize Player 1's chances.
            
            choose_first =
            can_player_one_win(nums[1:], player_one_score,
                                 player_two_score + nums[0], 1)

            choose_last =
            can_player_one_win(nums[:-1], player_one_score,
                                 player_two_score + nums[-1], 1)

            return choose_first and choose_last

    #Initial call to the recursive function
    return can_player_one_win(numbers, 0, 0, 1)

Big(O) Analysis

Time Complexity
O(2^n)The brute force approach explores every possible sequence of picks. At each step, the current player has two choices: pick the first or the last element. Since there are n elements initially, there will be n steps in the game. Each step branches into two possibilities, leading to a binary tree exploration. Thus, the total number of game scenarios explored grows exponentially with n. Therefore, the time complexity is O(2^n).
Space Complexity
O(2^N)The brute force approach explores every possible sequence of moves using recursion. At each step, a player chooses between two options (the beginning or the end of the list), leading to a branching factor of 2. With N numbers in the list, the maximum depth of the recursion is N. Therefore, in the worst-case scenario, where all branches are fully explored, the call stack can grow to a depth of N, and each recursive call creates a new stack frame, resulting in 2^N total function calls. The space complexity is then O(2^N) due to the exponential nature of the recursion tree.

Optimal Solution

Approach

To figure out who wins, we'll use a method where each player aims to maximize their own score, anticipating the opponent's best moves. It's like planning several steps ahead in a game of chess to always choose the option that gives you the best possible outcome, even if the other player is also playing perfectly.

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

  1. Think of the problem as each player making a series of choices to pick numbers from the beginning or end of a line.
  2. Imagine a function that calculates the best possible score difference a player can achieve starting from a particular section of the line, assuming both players play perfectly.
  3. This function will consider two options: the player picks the number at the start of the section or the number at the end.
  4. For each option, the function will assume the other player will then make their best possible move from the remaining section, which minimizes the first player's score.
  5. The function chooses the option (picking from the start or end) that leads to the highest possible score difference for the current player, considering the opponent's best response.
  6. Start applying this function to smaller sections of the number line, and gradually increase the size of the section until you've considered the entire line.
  7. The result of applying the function to the whole line tells you the maximum score difference the first player can achieve. If it's positive, the first player wins. If it's zero or negative, the second player wins.

Code Implementation

def predict_the_winner(nums):    number_of_nums = len(nums)
    # Memoization table stores max score difference for subproblems.
    memo = {}
    def calculate_max_score_difference(left_index, right_index):
        if (left_index, right_index) in memo:
            return memo[(left_index, right_index)]
        if left_index == right_index:
            return nums[left_index]
        
        if left_index > right_index:
            return 0
        # Player 1 chooses the left number.
        score_if_choose_left =
        nums[left_index] - calculate_max_score_difference(left_index + 1, right_index)
        
        # Player 1 chooses the right number.
        score_if_choose_right =
        nums[right_index] - calculate_max_score_difference(left_index, right_index - 1)
        # Take the maximum score.
        memo[(left_index, right_index)] = max(score_if_choose_left, score_if_choose_right)
        return memo[(left_index, right_index)]
    
    # If player 1's max score diff is >= 0, player 1 wins
    player_one_score_advantage = calculate_max_score_difference(0, number_of_nums - 1)
    return player_one_score_advantage >= 0

Big(O) Analysis

Time Complexity
O(n²)The core of this algorithm involves a function that calculates the best score difference for a subarray. The algorithm effectively explores all possible subarrays within the input array of size 'n'. This exploration can be visualized as a table where each cell (i, j) represents the subarray from index i to j. Filling this table requires examining all possible combinations of i and j where 0 <= i <= j < n. The number of such combinations and therefore the number of subarrays is proportional to n squared, specifically n * (n + 1) / 2. Therefore, the overall time complexity is O(n²).
Space Complexity
O(N^2)The algorithm, as described, relies on a function to calculate the best score difference. This suggests dynamic programming with memoization to avoid recomputation. To store these intermediate results for each possible sub-section of the input array, a 2D array (or equivalent data structure) is used. The size of this array will be N x N, where N is the number of elements in the input array 'nums'. Therefore, the auxiliary space complexity is O(N^2).

Edge Cases

Null or empty input array
How to Handle:
Return true if the array is null or empty, as player 1 trivially wins with no choices.
Array with one element
How to Handle:
Return true, as player 1 automatically wins by selecting that single element.
Array with two elements and equal values
How to Handle:
Player 1 will pick the element, player 2 will get the other element, thus player 1 wins if and only if the first element is >= than the second, which is true in this case.
Large array size exceeding reasonable memory limits for recursion or dynamic programming
How to Handle:
Ensure the solution uses an iterative dynamic programming approach to avoid stack overflow and improve memory efficiency.
Array with all identical values
How to Handle:
The first player will always win or tie if they play optimally, so return True.
Array with a very large number at the beginning
How to Handle:
The dynamic programming approach will correctly handle the calculation of scores and ensure the optimal play is evaluated.
Array with negative numbers
How to Handle:
The algorithm should correctly handle negative numbers as the score difference can be negative.
Integer overflow when summing elements in very large arrays
How to Handle:
Use 64 bit integers (long) to prevent potential overflows when calculating the player scores.