Facebook Pixel

464. Can I Win

Problem Description

This problem is about a two-player game where players take turns choosing numbers to add to a running total, with the goal of being the first to reach or exceed a target value.

Game Rules:

  • Two players take turns choosing integers from 1 to maxChoosableInteger
  • Each integer can only be used once throughout the entire game (no replacement)
  • Players add their chosen number to a running total that starts at 0
  • The first player to make the total reach or exceed desiredTotal wins
  • Both players play optimally (always make the best possible move)

Your Task: Given two integers:

  • maxChoosableInteger: The maximum number that can be chosen (players can pick from 1 to this number)
  • desiredTotal: The target sum to reach or exceed to win

Return true if the first player (the one who starts) can force a win, otherwise return false.

Example Scenario: If maxChoosableInteger = 10 and desiredTotal = 11:

  • Player 1 could choose 10, bringing the total to 10
  • Player 2 would need to choose from the remaining numbers (1-9), and any choice would make the total ≥ 11
  • Therefore, Player 2 would win, so the function should return false

Key Points:

  • Numbers cannot be reused once chosen
  • "Optimal play" means each player always makes the move that gives them the best chance to win
  • The first player wins if they have a guaranteed winning strategy regardless of how the second player plays
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that this is a classic game theory problem where we need to determine if the current player has a winning strategy. Since both players play optimally, we need to think recursively: a player can win if they can make a move that puts their opponent in a losing position.

Let's think about when the first player can guarantee a win:

  1. If they can choose a number that immediately reaches or exceeds the target, they win
  2. If they can choose a number that leaves the opponent in a position where the opponent cannot win no matter what they choose

This naturally leads to a recursive approach where we try all possible moves and check if any move leads to the opponent losing.

However, there's an important edge case to consider first: What if the sum of all available numbers (1 + 2 + ... + maxChoosableInteger) is less than desiredTotal? In this case, even if both players use all numbers, they can't reach the target, so nobody can win. We can calculate this sum using the formula (1 + maxChoosableInteger) * maxChoosableInteger / 2.

For the main game logic, we need to track:

  • Which numbers have been used (since each number can only be used once)
  • The current running total

We can represent the used numbers as a bitmask where bit i indicates whether number i has been used. For example, if numbers 1 and 3 are used, the mask would have bits 1 and 3 set.

The recursive strategy works as follows:

  • For each available number i (where bit i in mask is 0)
  • If choosing i makes the total ≥ desiredTotal, current player wins
  • Otherwise, simulate choosing i and check if the opponent loses in the resulting state
  • If the opponent loses, then the current player wins by choosing i
  • If no choice leads to a win, the current player loses

Since the same game state (same used numbers and same total) can occur through different sequences of moves, we use memoization to avoid recalculating the same states.

Solution Approach

The solution uses state compression with bitmask and memoization to efficiently solve this game theory problem.

Step 1: Check if winning is possible

First, we calculate if the sum of all available numbers is sufficient to reach the target:

if (1 + maxChoosableInteger) * maxChoosableInteger // 2 < desiredTotal:
    return False

If the total sum is less than desiredTotal, nobody can win, so we return False.

Step 2: Define the recursive function with memoization

We define dfs(mask, s) where:

  • mask: A bitmask representing which numbers have been used (bit i is 1 if number i is used)
  • s: The current cumulative sum
  • Returns: True if the current player can win from this state

The @cache decorator provides automatic memoization to avoid recalculating the same states.

Step 3: Try all possible moves

Inside dfs, we iterate through each number from 1 to maxChoosableInteger:

for i in range(1, maxChoosableInteger + 1):
    if mask >> i & 1 ^ 1:  # Check if number i is not used

The bit operation mask >> i & 1 ^ 1 checks if bit i is 0 (number i hasn't been used):

  • mask >> i shifts the mask right by i positions
  • & 1 extracts the least significant bit
  • ^ 1 flips the bit (so we get True if the bit was 0)

Step 4: Check winning conditions

For each available number i, we check two winning conditions:

if s + i >= desiredTotal or not dfs(mask | 1 << i, s + i):
    return True
  1. Immediate win: If s + i >= desiredTotal, choosing i immediately wins the game
  2. Opponent loses: If not dfs(mask | 1 << i, s + i) returns True, it means after we choose i, the opponent will be in a losing position

The expression mask | 1 << i creates a new mask with bit i set to 1, marking number i as used.

Step 5: Return the result

  • If any move leads to a win, we return True
  • If no move leads to a win after trying all possibilities, we return False

The initial call dfs(0, 0) starts with no numbers used (mask = 0) and sum = 0.

Time Complexity: O(2^n * n) where n = maxChoosableInteger, as there are 2^n possible states and each state requires checking up to n numbers.

Space Complexity: O(2^n) for the memoization cache.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a small example with maxChoosableInteger = 4 and desiredTotal = 6.

Initial Setup:

  • Available numbers: {1, 2, 3, 4}
  • Sum of all numbers: 1 + 2 + 3 + 4 = 10 ≥ 6 (game is winnable)
  • Starting state: mask = 0000 (binary), sum = 0

Player 1's Turn (First Move): Player 1 evaluates each possible choice:

  1. Choose 1: New sum = 1, mask = 0001

    • Player 2 could then choose 4 → sum becomes 5
    • Player 1 would need to win from (mask=1001, sum=5)
    • Any remaining choice (2 or 3) would make sum ≥ 6, so Player 1 wins
    • This path eventually leads to Player 1 winning ✓
  2. Choose 2: New sum = 2, mask = 0010

    • Player 2 could choose 4 → sum becomes 6 (Player 2 wins immediately) ✗
  3. Choose 3: New sum = 3, mask = 0100

    • Player 2 could choose 4 → sum becomes 7 (Player 2 wins immediately) ✗
  4. Choose 4: New sum = 4, mask = 1000

    • Player 2 could choose 2 → sum becomes 6 (Player 2 wins immediately) ✗

Analysis: Player 1 can force a win by choosing 1. Here's why:

  • After Player 1 chooses 1 (sum = 1), Player 2 has three options:
    • Choose 2 (sum = 3): Player 1 chooses 3 or 4 and wins
    • Choose 3 (sum = 4): Player 1 chooses 2 or 4 and wins
    • Choose 4 (sum = 5): Player 1 chooses 2 or 3 and wins

Since Player 1 has at least one move (choosing 1) that guarantees a win regardless of Player 2's response, the function returns true.

How the Algorithm Works: The recursive function dfs(mask, s) explores this game tree:

  • At each state, it tries all unused numbers
  • If any choice leads to immediate victory (sum ≥ 6) or puts the opponent in a losing position, it returns True
  • The memoization prevents re-evaluating identical game states (same used numbers and sum)
  • The initial call dfs(0, 0) returns True because Player 1 can force a win by choosing 1

Solution Implementation

1class Solution:
2    def canIWin(self, maxChoosableInteger: int, desiredTotal: int) -> bool:
3        """
4        Determines if the first player can force a win in a game where players
5        take turns choosing integers from 1 to maxChoosableInteger (without replacement)
6        and the first to reach or exceed desiredTotal wins.
7      
8        Args:
9            maxChoosableInteger: The maximum integer that can be chosen (1 to maxChoosableInteger)
10            desiredTotal: The target sum to reach or exceed to win
11          
12        Returns:
13            True if the first player can guarantee a win, False otherwise
14        """
15        from functools import cache
16      
17        @cache
18        def can_win_from_state(used_numbers_mask: int, current_sum: int) -> bool:
19            """
20            Recursively determines if the current player can win from the given state.
21          
22            Args:
23                used_numbers_mask: Bitmask representing which numbers have been used
24                                   (bit i represents whether number i has been used)
25                current_sum: The current running total
26              
27            Returns:
28                True if the current player can force a win from this state
29            """
30            # Try each possible number from 1 to maxChoosableInteger
31            for number in range(1, maxChoosableInteger + 1):
32                # Check if this number hasn't been used yet
33                # (bit at position 'number' should be 0)
34                if (used_numbers_mask >> number) & 1 == 0:
35                    # Calculate new sum after choosing this number
36                    new_sum = current_sum + number
37                  
38                    # Current player wins if either:
39                    # 1. This move reaches or exceeds the desired total
40                    # 2. The opponent cannot win from the resulting state
41                    if new_sum >= desiredTotal:
42                        return True
43                  
44                    # Mark this number as used and check opponent's position
45                    new_mask = used_numbers_mask | (1 << number)
46                    if not can_win_from_state(new_mask, new_sum):
47                        return True
48          
49            # If no winning move exists, current player loses
50            return False
51      
52        # Early termination: if the sum of all available numbers is less than
53        # the desired total, nobody can win (first player loses)
54        total_available_sum = (1 + maxChoosableInteger) * maxChoosableInteger // 2
55        if total_available_sum < desiredTotal:
56            return False
57      
58        # Start the game with no numbers used and sum of 0
59        return can_win_from_state(0, 0)
60
1class Solution {
2    // Memoization cache: maps game state (bitmask) to whether current player can win
3    private Map<Integer, Boolean> memo = new HashMap<>();
4    private int maxNumber;
5    private int targetSum;
6  
7    /**
8     * Determines if the first player can guarantee a win in the game.
9     * Players take turns choosing numbers from 1 to maxChoosableInteger (without replacement).
10     * The first player to reach or exceed desiredTotal wins.
11     * 
12     * @param maxChoosableInteger The maximum number that can be chosen (inclusive)
13     * @param desiredTotal The target sum to reach or exceed to win
14     * @return true if the first player can force a win, false otherwise
15     */
16    public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
17        // Check if the sum of all available numbers is less than desired total
18        // If so, neither player can win
19        int totalSum = (1 + maxChoosableInteger) * maxChoosableInteger / 2;
20        if (totalSum < desiredTotal) {
21            return false;
22        }
23      
24        // Initialize instance variables for use in DFS
25        this.maxNumber = maxChoosableInteger;
26        this.targetSum = desiredTotal;
27      
28        // Start DFS with empty bitmask (no numbers chosen) and current sum of 0
29        return dfs(0, 0);
30    }
31  
32    /**
33     * Recursive DFS with memoization to determine if current player can win.
34     * Uses bitmask to track which numbers have been chosen.
35     * 
36     * @param usedMask Bitmask representing which numbers have been used (bit i = 1 means number i+1 is used)
37     * @param currentSum The current running sum of all chosen numbers
38     * @return true if the current player can force a win from this state
39     */
40    private boolean dfs(int usedMask, int currentSum) {
41        // Check if we've already computed this game state
42        if (memo.containsKey(usedMask)) {
43            return memo.get(usedMask);
44        }
45      
46        // Try each available number from 1 to maxNumber
47        for (int i = 0; i < maxNumber; i++) {
48            // Check if number (i+1) has not been used yet
49            if ((usedMask >> i & 1) == 0) {
50                int chosenNumber = i + 1;
51              
52                // Current player wins if:
53                // 1. Choosing this number reaches/exceeds the target, OR
54                // 2. After choosing this number, the opponent cannot win
55                if (currentSum + chosenNumber >= targetSum || 
56                    !dfs(usedMask | (1 << i), currentSum + chosenNumber)) {
57                  
58                    // Current player can win from this state
59                    memo.put(usedMask, true);
60                    return true;
61                }
62            }
63        }
64      
65        // No winning move found for current player from this state
66        memo.put(usedMask, false);
67        return false;
68    }
69}
70
1class Solution {
2public:
3    bool canIWin(int maxChoosableInteger, int desiredTotal) {
4        // If the sum of all numbers is less than desiredTotal, no one can win
5        if ((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal) {
6            return false;
7        }
8      
9        // Memoization map: key is the bitmask of used numbers, value is whether current player wins
10        unordered_map<int, bool> memo;
11      
12        // DFS function to determine if current player can win
13        // mask: bitmask representing which numbers have been used (bit i = 1 means number i+1 is used)
14        // currentSum: current running sum of all chosen numbers
15        function<bool(int, int)> dfs = [&](int mask, int currentSum) -> bool {
16            // Check if this state has been computed before
17            if (memo.find(mask) != memo.end()) {
18                return memo[mask];
19            }
20          
21            // Try each available number from 1 to maxChoosableInteger
22            for (int i = 0; i < maxChoosableInteger; ++i) {
23                // Check if number (i+1) is available (bit i should be 0)
24                if (((mask >> i) & 1) == 0) {
25                    int chosenNumber = i + 1;
26                  
27                    // Current player wins if:
28                    // 1. Choosing this number reaches or exceeds desiredTotal, OR
29                    // 2. After choosing this number, the opponent cannot win
30                    if (currentSum + chosenNumber >= desiredTotal || 
31                        !dfs(mask | (1 << i), currentSum + chosenNumber)) {
32                        memo[mask] = true;
33                        return true;
34                    }
35                }
36            }
37          
38            // If no winning move is found, current player loses
39            memo[mask] = false;
40            return false;
41        };
42      
43        // Start the game with no numbers used (mask = 0) and sum = 0
44        return dfs(0, 0);
45    }
46};
47
1/**
2 * Determines if the first player can guarantee a win in the "100 game"
3 * @param maxChoosableInteger - The maximum integer that can be chosen (1 to maxChoosableInteger)
4 * @param desiredTotal - The target sum to reach or exceed to win
5 * @returns true if the first player can force a win, false otherwise
6 */
7function canIWin(maxChoosableInteger: number, desiredTotal: number): boolean {
8    // Check if the sum of all possible integers is less than the desired total
9    // If so, neither player can win
10    const sumOfAllIntegers = ((1 + maxChoosableInteger) * maxChoosableInteger) / 2;
11    if (sumOfAllIntegers < desiredTotal) {
12        return false;
13    }
14  
15    // Memoization cache to store computed game states
16    // Key: bitmask representing used numbers, Value: whether current player can win
17    const memoCache: Record<number, boolean> = {};
18  
19    /**
20     * Recursive function to determine if the current player can win
21     * @param usedNumbersMask - Bitmask representing which numbers have been used
22     * @param currentSum - Current sum of all chosen numbers
23     * @returns true if the current player can guarantee a win from this state
24     */
25    const canCurrentPlayerWin = (usedNumbersMask: number, currentSum: number): boolean => {
26        // Check if this game state has already been computed
27        if (memoCache.hasOwnProperty(usedNumbersMask)) {
28            return memoCache[usedNumbersMask];
29        }
30      
31        // Try each possible number from 1 to maxChoosableInteger
32        for (let number = 1; number <= maxChoosableInteger; number++) {
33            // Check if this number hasn't been used yet
34            // (bit at position 'number' should be 0)
35            const isNumberAvailable = ((usedNumbersMask >> number) & 1) === 0;
36          
37            if (isNumberAvailable) {
38                // Mark this number as used by setting its bit to 1
39                const newMask = usedNumbersMask | (1 << number);
40                const newSum = currentSum + number;
41              
42                // Current player wins if:
43                // 1. Choosing this number reaches or exceeds the desired total, OR
44                // 2. After choosing this number, the opponent cannot win
45                if (newSum >= desiredTotal || !canCurrentPlayerWin(newMask, newSum)) {
46                    memoCache[usedNumbersMask] = true;
47                    return true;
48                }
49            }
50        }
51      
52        // If no winning move is found, current player loses
53        memoCache[usedNumbersMask] = false;
54        return false;
55    };
56  
57    // Start the game with no numbers used (mask = 0) and sum = 0
58    return canCurrentPlayerWin(0, 0);
59}
60

Time and Space Complexity

The time complexity is O(2^n × n), where n is maxChoosableInteger.

The function uses memoization with @cache decorator to store results for different game states. Each state is represented by a bitmask that indicates which numbers have been used. There are 2^n possible states (each number from 1 to n can be either used or unused). For each state, the function iterates through all n numbers to check if they can be chosen, resulting in O(n) work per state. Therefore, the total time complexity is O(2^n × n).

The space complexity is O(2^n), where n is maxChoosableInteger.

The space is dominated by the memoization cache, which stores results for up to 2^n different states (all possible combinations of used/unused numbers). Additionally, the recursion depth can be at most n (when all numbers are chosen one by one), contributing O(n) to the call stack. Since O(2^n) + O(n) = O(2^n), the overall space complexity is O(2^n).

Common Pitfalls

1. Forgetting the Early Termination Check

Pitfall: Not checking if it's mathematically possible to reach the desiredTotal with all available numbers combined. Without this check, the algorithm will unnecessarily explore all game states even when nobody can win.

Problem Example:

# If maxChoosableInteger = 5, desiredTotal = 100
# Sum of 1+2+3+4+5 = 15, which is less than 100
# The game will never end, but the algorithm might still try to compute states

Solution: Always include the early termination check before starting the recursive computation:

total_available_sum = (1 + maxChoosableInteger) * maxChoosableInteger // 2
if total_available_sum < desiredTotal:
    return False

2. Incorrect Bit Manipulation for Checking Used Numbers

Pitfall: Using wrong bit operations to check if a number has been used. A common mistake is confusing the bit positions or using incorrect operators.

Incorrect implementations:

# Wrong: Using bit position 0 for number 1 (off-by-one error)
if (used_numbers_mask >> (number - 1)) & 1 == 0:  # WRONG!

# Wrong: Checking if bit is set instead of unset
if (used_numbers_mask >> number) & 1 == 1:  # WRONG! This checks if number IS used

# Wrong: Not properly isolating the bit
if used_numbers_mask >> number == 0:  # WRONG! This checks all bits from position number onwards

Solution: Use the correct bit manipulation pattern:

# Correct: Check if bit at position 'number' is 0 (number hasn't been used)
if (used_numbers_mask >> number) & 1 == 0:
    # Number is available to use

3. Not Using Memoization or Using It Incorrectly

Pitfall: Implementing the solution without memoization leads to exponential time complexity with massive redundant calculations. The same game state (same used numbers and sum) can be reached through different move sequences.

Problem Example:

# Without memoization - this will time out for larger inputs
def can_win_from_state(used_numbers_mask, current_sum):
    # ... recursive logic without caching ...
  
# Or incorrect memoization key (forgetting current_sum)
memo = {}
def can_win_from_state(used_numbers_mask, current_sum):
    if used_numbers_mask in memo:  # WRONG! Ignores current_sum
        return memo[used_numbers_mask]

Solution: Use proper memoization with both state parameters:

from functools import cache

@cache
def can_win_from_state(used_numbers_mask: int, current_sum: int) -> bool:
    # The @cache decorator automatically handles memoization with both parameters

Or manually with a dictionary:

memo = {}
def can_win_from_state(used_numbers_mask, current_sum):
    key = (used_numbers_mask, current_sum)
    if key in memo:
        return memo[key]
    # ... compute result ...
    memo[key] = result
    return result

4. Misunderstanding the Win Condition Logic

Pitfall: Incorrectly implementing the logic for determining when the current player wins. A common mistake is only checking for immediate wins and forgetting about forcing the opponent into a losing position.

Incorrect implementation:

# Wrong: Only checking for immediate win
if new_sum >= desiredTotal:
    return True
# Forgot to check if opponent loses!

# Wrong: Incorrect opponent check
if can_win_from_state(new_mask, new_sum):  # WRONG! Should be NOT
    return True

Solution: Check both winning conditions:

if new_sum >= desiredTotal:  # Immediate win
    return True

if not can_win_from_state(new_mask, new_sum):  # Opponent loses
    return True

5. Edge Case: desiredTotal ≤ 0

Pitfall: Not handling the edge case where desiredTotal is 0 or negative. The first player would win immediately without choosing any number.

Solution: Add a check at the beginning:

if desiredTotal <= 0:
    return True  # First player wins immediately
Discover Your Strengths and Weaknesses: Take Our 5-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