LeetCode 464 - Can I Win
This problem describes a two-player turn-based game. Players take turns choosing a number from a shared pool of integers ranging from 1 to maxChoosableInteger. Once a number is chosen, it cannot be used again for the rest of the game.
Difficulty: 🟡 Medium
Topics: Math, Dynamic Programming, Bit Manipulation, Memoization, Game Theory, Bitmask
Solution
LeetCode 464 - Can I Win
Problem Understanding
This problem describes a two-player turn-based game. Players take turns choosing a number from a shared pool of integers ranging from 1 to maxChoosableInteger. Once a number is chosen, it cannot be used again for the rest of the game.
Each chosen number is added to a running total. The player who causes the running total to become greater than or equal to desiredTotal wins immediately.
The task is to determine whether the first player can force a win if both players play optimally.
The input consists of two integers:
maxChoosableIntegerdefines the maximum selectable number, meaning the available numbers are1throughmaxChoosableIntegerdesiredTotaldefines the target sum needed to win
The output is a boolean:
truemeans the first player can guarantee victoryfalsemeans the second player can always prevent the first player from winning
The constraints are important:
maxChoosableInteger <= 20desiredTotal <= 300
The small upper bound of 20 strongly suggests that bitmasking is feasible. Since there are at most 20 numbers, every game state can be represented using a 20-bit integer. This observation is critical for the optimal solution.
Several edge cases appear immediately:
If desiredTotal == 0, the first player already satisfies the condition before making a move, so the answer is automatically true.
If the sum of all available numbers is still smaller than desiredTotal, then nobody can ever reach the target. In that case, the first player cannot win.
The total sum of numbers from 1 to maxChoosableInteger is:
$1+2+\cdots+n=\frac{n(n+1)}{2}$
For this problem:
maxPossibleSum = maxChoosableInteger * (maxChoosableInteger + 1) / 2
If this value is less than desiredTotal, the answer must be false.
Another important challenge is repeated states. Different sequences of moves may produce the same set of remaining numbers. A naive recursive search would recompute the same game states repeatedly, causing exponential blowup.
Approaches
Brute Force Approach
A brute-force solution recursively simulates every possible game path.
At each turn, the current player tries every unused number. After choosing a number, the algorithm recursively checks whether the opponent can win from the resulting state.
The recursive logic works like this:
- If choosing a number immediately reaches
desiredTotal, the current player wins - Otherwise, recursively simulate the opponent's turn
- If any move causes the opponent to lose, then the current player can force a win
This approach is logically correct because it exhaustively explores the game tree under optimal play assumptions.
However, the same states are recomputed many times. For example, the state where numbers {1, 3, 5} are already used can be reached through multiple move orders:
1 -> 3 -> 53 -> 1 -> 55 -> 3 -> 1
Without caching, each state gets recalculated repeatedly, making the solution far too slow.
The worst-case branching factor is large, and the recursion tree grows factorially.
Optimal Approach, DFS + Memoization + Bitmasking
The key insight is that the game state is completely determined by which numbers have already been used.
The exact order of moves does not matter once we know the used numbers and the remaining target.
Because maxChoosableInteger <= 20, we can encode the used numbers inside a bitmask:
- Bit
irepresents whether numberi + 1has been chosen - A
1bit means the number is already used - A
0bit means the number is still available
This allows each unique game state to be represented efficiently as an integer.
We then use depth-first search with memoization:
- DFS explores all possible moves
- Memoization stores whether a particular bitmask state is winning or losing
The recursive strategy becomes:
- Try every available number
- If choosing that number wins immediately, return
true - Otherwise, recursively check the opponent's result
- If the opponent loses in any branch, the current player wins
Because each state is computed once, the algorithm becomes feasible.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n!) | O(n) | Explores all move permutations without caching |
| Optimal | O(2^n * n) | O(2^n) | Uses memoization and bitmask states |
Algorithm Walkthrough
- Handle trivial winning conditions.
If desiredTotal <= 0, the first player already satisfies the target condition without making any move, so return true.
2. Check whether reaching the target is mathematically possible.
Compute the sum of all numbers from 1 to maxChoosableInteger.
totalSum = n * (n + 1) / 2
If totalSum < desiredTotal, no sequence of moves can ever reach the target, so return false.
3. Represent game states using a bitmask.
Use an integer where each bit indicates whether a number has already been chosen.
For example, if maxChoosableInteger = 5:
| Bitmask | Used Numbers |
|---|---|
| 00000 | none |
| 00101 | 1 and 3 |
| 11111 | all numbers |
| 4. Define the recursive DFS function. |
The DFS function receives:
usedMask, representing which numbers are already takenremaining, representing how much more is needed to win
The function returns whether the current player can force a win from this state. 5. Try every available number.
For each number from 1 to maxChoosableInteger:
- Skip it if already used
- Otherwise, consider choosing it
- Check immediate winning moves.
If the chosen number is greater than or equal to remaining, the current player wins immediately.
7. Simulate the opponent.
Mark the chosen number as used and recursively evaluate the opponent's position.
If the recursive call returns false, that means the opponent loses from that state, so the current player can force a win.
8. Memoize the result.
Store the result for the current usedMask so future calls can reuse it instantly.
9. Return the final answer.
Start DFS from:
usedMask = 0remaining = desiredTotal
Why it works
The algorithm relies on optimal play and recursive game-state evaluation.
A state is winning if there exists at least one move that forces the opponent into a losing state.
A state is losing if every possible move allows the opponent to eventually win.
Memoization guarantees that each unique state is solved exactly once, and bitmasking ensures compact state representation.
Python Solution
class Solution:
def canIWin(self, maxChoosableInteger: int, desiredTotal: int) -> bool:
if desiredTotal <= 0:
return True
total_sum = maxChoosableInteger * (maxChoosableInteger + 1) // 2
if total_sum < desiredTotal:
return False
memo = {}
def dfs(used_mask: int, remaining: int) -> bool:
if used_mask in memo:
return memo[used_mask]
for number in range(1, maxChoosableInteger + 1):
bit = 1 << (number - 1)
if used_mask & bit:
continue
if number >= remaining:
memo[used_mask] = True
return True
next_mask = used_mask | bit
opponent_can_win = dfs(next_mask, remaining - number)
if not opponent_can_win:
memo[used_mask] = True
return True
memo[used_mask] = False
return False
return dfs(0, desiredTotal)
The implementation begins with two important edge-case checks.
If desiredTotal <= 0, the first player trivially wins because the target has already been met.
Next, the algorithm computes the maximum possible sum using all numbers. If even that total cannot reach desiredTotal, winning is impossible.
The memo dictionary stores previously computed game states. The key is the bitmask representing used numbers, and the value indicates whether the current player can force a win from that state.
The recursive dfs function explores every available move.
For each candidate number:
- A bit is computed using
1 << (number - 1) - If the bit is already set, the number has already been used
- Otherwise, the move is evaluated
If choosing the number immediately satisfies the target, the function returns True.
Otherwise, the algorithm recursively evaluates the opponent's position after updating the bitmask and decreasing the remaining target.
If the opponent cannot win from that next state, then the current player has found a winning strategy.
If every possible move still allows the opponent to win, the current state is losing.
Go Solution
func canIWin(maxChoosableInteger int, desiredTotal int) bool {
if desiredTotal <= 0 {
return true
}
totalSum := maxChoosableInteger * (maxChoosableInteger + 1) / 2
if totalSum < desiredTotal {
return false
}
memo := make(map[int]bool)
visited := make(map[int]bool)
var dfs func(int, int) bool
dfs = func(usedMask int, remaining int) bool {
if visited[usedMask] {
return memo[usedMask]
}
for number := 1; number <= maxChoosableInteger; number++ {
bit := 1 << (number - 1)
if usedMask&bit != 0 {
continue
}
if number >= remaining {
memo[usedMask] = true
visited[usedMask] = true
return true
}
nextMask := usedMask | bit
opponentCanWin := dfs(nextMask, remaining-number)
if !opponentCanWin {
memo[usedMask] = true
visited[usedMask] = true
return true
}
}
memo[usedMask] = false
visited[usedMask] = true
return false
}
return dfs(0, desiredTotal)
}
The Go implementation follows the same recursive strategy as the Python version.
One notable difference is memoization handling. Since Go maps return the zero value for missing keys, a separate visited map is used to distinguish between:
- a state that has not been computed
- a state that has been computed and evaluates to
false
Bitmask operations work naturally with integers in Go, and the recursion structure remains almost identical to Python.
Worked Examples
Example 1
Input:
maxChoosableInteger = 10
desiredTotal = 11
The total available sum is:
1 + 2 + ... + 10 = 55
So reaching 11 is possible.
Initial state:
| Variable | Value |
|---|---|
| usedMask | 0000000000 |
| remaining | 11 |
The first player tries all possible moves.
First player chooses 1
| Variable | Value |
|---|---|
| usedMask | 0000000001 |
| remaining | 10 |
Now the second player can choose 10 immediately.
1 + 10 = 11
Second player wins.
First player chooses 2
| Variable | Value |
|---|---|
| usedMask | 0000000010 |
| remaining | 9 |
Second player chooses 9 and wins immediately.
The same pattern continues:
| First Move | Second Winning Move |
|---|---|
| 1 | 10 |
| 2 | 9 |
| 3 | 8 |
| 4 | 7 |
| 5 | 6 |
Every opening move has a direct counter.
Therefore, the first player cannot force a win.
Output: false
Example 2
Input:
maxChoosableInteger = 10
desiredTotal = 0
Since the target is already satisfied before any move:
Output: true
The algorithm returns immediately during the edge-case check.
Example 3
Input:
maxChoosableInteger = 10
desiredTotal = 1
Initial state:
| Variable | Value |
|---|---|
| usedMask | 0000000000 |
| remaining | 1 |
The first player can choose 1.
Since:
1 >= remaining
the first player wins instantly.
Output: true
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(2^n * n) | There are at most 2^n bitmask states, and each state tries up to n numbers |
| Space | O(2^n) | Memoization stores results for each unique bitmask |
The key observation is that the algorithm computes each bitmask state once.
For every state, the algorithm iterates through all possible numbers to determine valid moves. Since there are at most 2^20 states, the solution is efficient enough for the problem constraints.
Test Cases
solution = Solution()
assert solution.canIWin(10, 11) == False # Provided example, first player loses
assert solution.canIWin(10, 0) == True # Target already reached
assert solution.canIWin(10, 1) == True # Immediate winning move
assert solution.canIWin(5, 50) == False # Impossible to reach desiredTotal
assert solution.canIWin(20, 210) == False # Sum exactly equals 210, parity matters
assert solution.canIWin(20, 0) == True # Zero target edge case
assert solution.canIWin(4, 6) == True # First player can force win
assert solution.canIWin(4, 10) == False # All numbers sum to target
assert solution.canIWin(1, 1) == True # Single number immediate win
assert solution.canIWin(1, 2) == False # Cannot reach target
assert solution.canIWin(2, 3) == False # Second player always wins
assert solution.canIWin(3, 5) == True # Recursive winning path
assert solution.canIWin(18, 79) == True # Larger state-space test
assert solution.canIWin(20, 300) == False # Maximum desiredTotal stress case
| Test | Why |
|---|---|
(10, 11) |
Validates classic losing configuration |
(10, 0) |
Tests immediate success edge case |
(10, 1) |
Tests immediate winning move |
(5, 50) |
Verifies impossible target detection |
(20, 210) |
Tests total sum exactly equal to target |
(20, 0) |
Confirms zero-target handling |
(4, 6) |
Validates recursive winning strategy |
(4, 10) |
Tests parity and forced-loss behavior |
(1, 1) |
Smallest valid winning input |
(1, 2) |
Smallest impossible input |
(2, 3) |
Simple forced-loss state |
(3, 5) |
Small recursive game tree |
(18, 79) |
Larger memoization scenario |
(20, 300) |
Stress test near upper bounds |
Edge Cases
One important edge case occurs when desiredTotal is zero or negative. In this scenario, the game is effectively already won before any moves occur. A naive implementation might incorrectly start recursion anyway. The solution handles this immediately with an early return.
Another critical edge case appears when the sum of all available numbers is still smaller than desiredTotal. Without checking this condition upfront, the recursion would explore many impossible states before eventually failing. The implementation prevents unnecessary computation by returning false immediately.
A more subtle edge case involves repeated states reached through different move orders. For example, choosing numbers {1, 3} leads to the same game state regardless of whether the order was 1 -> 3 or 3 -> 1. Without memoization, these states would be recomputed repeatedly, causing exponential slowdown. The bitmask representation ensures identical states map to the same memoized result.
Another tricky case occurs when a player can win immediately by selecting a number greater than or equal to the remaining target. If the implementation recurses before checking this condition, it may incorrectly continue exploring deeper states. The algorithm explicitly checks immediate-winning moves before recursion begins.