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
tomaxChoosableInteger
- 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 to10
- 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
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:
- If they can choose a number that immediately reaches or exceeds the target, they win
- 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 biti
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 (biti
is 1 if numberi
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 byi
positions& 1
extracts the least significant bit^ 1
flips the bit (so we getTrue
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
- Immediate win: If
s + i >= desiredTotal
, choosingi
immediately wins the game - Opponent loses: If
not dfs(mask | 1 << i, s + i)
returnsTrue
, it means after we choosei
, 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 EvaluatorExample 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:
-
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 ✓
-
Choose 2: New sum = 2, mask = 0010
- Player 2 could choose 4 → sum becomes 6 (Player 2 wins immediately) ✗
-
Choose 3: New sum = 3, mask = 0100
- Player 2 could choose 4 → sum becomes 7 (Player 2 wins immediately) ✗
-
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)
returnsTrue
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
What's the relationship between a tree and a graph?
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!