Can I Win solution and explanation
9 more videos on this problem
花花酱 LeetCode 464. Can I Win - 刷题找工作 EP165Hua Hua · 20:24 · 10K views
Can I win ? Dynamic Programming , Bitmask Easy Approach Explanation #leetcode #leetcodesolutionServer Side Truths · 9:18 · 8K views
Leetcode 464 Can I Win | Python Solution | LinkedIn Interview QuestiomDonald Dang · 19:11 · 7K views
Baozi Training Leetcode 464 solution: Can I WinBaozi Training · 10:33 · 4K views
【每日一题:小Fu讲解】LeetCode 464. Can I Win傅码爷 · 24:15 · 2K views
464. Can I Win || LeetCode The Hard WayLeetCode The Hard Way · 11:04 · 1K views
【每日一题】464. Can I Win, 4/9/2021Huifeng Guan · 21:37 · 1K views
小旭讲解 LeetCode 464. Can I Win 状态压缩 - EP22HU Xiaoxu · 8:58 · 516 views
Разбор задачи 464 leetcode.com Can I Win. Решение на C++3.5 задачи в неделю · 53:09 · 381 views
LeetCode 464: Can I WinAlitaCode · 27:45 · 248 views
The problem
In the "100 game" two players take turns adding, to a running total, any integer from 1 to 10. The player who first causes the running total to reach or exceed 100 wins.
What if we change the game so that players cannot re-use integers?
For example, two players might take turns drawing from a common pool of numbers from 1 to 15 without replacement until they reach a total >= 100.
Given two integers maxChoosableInteger and desiredTotal, return true if the first player to move can force a win, otherwise, return false. Assume both players play optimally.
Example 1:
Input: maxChoosableInteger = 10, desiredTotal = 11 Output: false Explanation: No matter which integer the first player choose, the first player will lose. The first player can choose an integer from 1 up to 10. If the first player choose 1, the second player can only choose integers from 2 up to 10. The second player will win by choosing 10 and get a total = 11, which is >= desiredTotal. Same with other integers chosen by the first player, the second player will always win.
Example 2:
Input: maxChoosableInteger = 10, desiredTotal = 0 Output: true
Example 3:
Input: maxChoosableInteger = 10, desiredTotal = 1 Output: true
Constraints:
1 <= maxChoosableInteger <= 200 <= desiredTotal <= 300
Approach overview
Problem Overview: Two players take turns picking numbers from 1..maxChoosableInteger without reuse. Each picked number adds to a running total. The player who first makes the total reach or exceed desiredTotal wins. The task is to determine if the first player can force a win assuming both players play optimally.
Approach 1: Dynamic Programming with Bitmask (O(n · 2^n) time, O(2^n) space)
Represent the game state using a bitmask where each bit indicates whether a number has already been chosen. A mask of size n uniquely defines which moves remain. From any state, iterate through numbers 1..n; if number i is unused, simulate picking it and check whether the opponent loses from the resulting state. Memoize results for each mask so the same state is never recomputed. The key insight: if any move forces the opponent into a losing state, the current state is winning. This transforms the exponential search tree into a manageable 2^n state DP using Bit Manipulation and Dynamic Programming.
Approach 2: Minimax with Memoization (O(n · 2^n) time, O(2^n) space)
This models the problem as a classic two-player optimal strategy game. Use a recursive minimax search where the current player tries to maximize their chance of winning while the opponent tries to minimize it. At each step, iterate through available numbers and check if choosing one either immediately reaches the target or leads to a state where the opponent cannot win. Cache results using memoization keyed by the bitmask of chosen numbers. Without caching the recursion would explore up to n! paths, but memoization collapses equivalent states to 2^n. This approach highlights the Game Theory reasoning behind the optimal play.
Approach 3: Recursive DP with State Pruning (O(n · 2^n) time, O(2^n) space)
Another formulation stores the remaining total instead of the running sum and recursively tries available numbers. Before recursion, perform quick pruning checks. If the sum of all numbers 1..n is smaller than desiredTotal, winning is impossible. If any single number already reaches the target, the first player wins immediately. During recursion, subtract the chosen number from the remaining total and continue exploring unused numbers using a memo table indexed by the bitmask state. This is essentially top‑down DP with memoization but framed around remaining score rather than accumulated score.
Recommended for interviews: Use the minimax or bitmask DP approach with memoization. Interviewers expect you to recognize the game-state search and compress it using a bitmask. A naive recursive search demonstrates understanding of the game tree, but adding memoization and representing states with bit operations shows strong problem‑solving skill and reduces the complexity to O(n · 2^n).
Approach 1: Dynamic Programming with Bitmask
This approach constructs a dynamic programming solution using bitmasking to represent the state of available numbers. We use a bottom-up strategy to determine if the player can force a winning move given the current pool of available numbers.
def canIWin(maxChoosableInteger, desiredTotal):
if desiredTotal <= 0:
return True
if (1 + maxChoosableInteger) * maxChoosableInteger // 2 < desiredTotal:
return False
dp = [-1] * (1 << maxChoosableInteger)
def canWin(used):
if dp[used] != -1:
return dp[used] == 1
for i in range(maxChoosableInteger):
curr = 1 << i
if curr & used == 0:
if desiredTotal <= i + 1 or not canWin(used | curr):
dp[used] = 1
return True
dp[used] = 0
return False
return canWin(0)This Python solution uses a bottom-up dynamic programming approach with bitmasking. The array `dp` stores win/lose states for each possible combination of used integers, which is traversed and updated iteratively.
Time Complexity: O(2^n) where n is the maxChoosableInteger. Space Complexity: O(2^n) due to the storage of states in the dp array.
Approach 2: Minimax with Memoization
This approach uses the Minimax algorithm with memoization to evaluate all possible game states, efficiently determining if the first player can guarantee a win. The idea is to explore all possible moves by the current player, then recursively simulate the opponent's optimal response. Memoization is employed to cache results of subproblems to avoid redundant calculations.
def canIWin(maxChoosableInteger, desiredTotal):
if desiredTotal <= maxChoosableInteger:
return True
if sum(range(1, maxChoosableInteger + 1)) < desiredTotal:
return False
memo = {}
def can_win(used_numbers, current_total):
if tuple(used_numbers) in memo:
return memo[tuple(used_numbers)]
for i in range(maxChoosableInteger):
if used_numbers[i]:
continue
if current_total + i + 1 >= desiredTotal:
return True
used_numbers[i] = True
if not can_win(used_numbers, current_total + i + 1):
used_numbers[i] = False
memo[tuple(used_numbers)] = True
return True
used_numbers[i] = False
memo[tuple(used_numbers)] = False
return False
return can_win([False] * maxChoosableInteger, 0)import java.util.HashMap;
import java.util.Map;
class Solution {
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
if (desiredTotal <= maxChoosableInteger) return true;
if ((maxChoosableInteger * (maxChoosableInteger + 1) / 2) < desiredTotal) return false;
Map<Integer, Boolean> memo = new HashMap<>();
return canWin(0, 0, maxChoosableInteger, desiredTotal, memo);
}
private boolean canWin(int usedNumbers, int currentTotal, int maxChoosableInteger, int desiredTotal, Map<Integer, Boolean> memo) {
if (memo.containsKey(usedNumbers)) return memo.get(usedNumbers);
for (int i = 0; i < maxChoosableInteger; i++) {
int currentMask = 1 << i;
if ((usedNumbers & currentMask) != 0) continue;
int nextUsedNumbers = usedNumbers | currentMask;
if (currentTotal + i + 1 >= desiredTotal || !canWin(nextUsedNumbers, currentTotal + i + 1, maxChoosableInteger, desiredTotal, memo)) {
memo.put(usedNumbers, true);
return true;
}
}
memo.put(usedNumbers, false);
return false;
}
}#include <stdbool.h>
#include <string.h>
int memo[1 << 20];
bool can_win(int used_numbers, int current_total, int maxChoosableInteger, int desiredTotal) {
if (memo[used_numbers] != -1) return memo[used_numbers];
for (int i = 0; i < maxChoosableInteger; ++i) {
if (used_numbers & (1 << i)) continue;
if (current_total + i + 1 >= desiredTotal || !can_win(used_numbers | (1 << i), current_total + i + 1, maxChoosableInteger, desiredTotal)) {
return memo[used_numbers] = true;
}
}
return memo[used_numbers] = false;
}
bool canIWin(int maxChoosableInteger, int desiredTotal) {
if (desiredTotal <= maxChoosableInteger) return true;
if ((maxChoosableInteger * (maxChoosableInteger + 1) / 2) < desiredTotal) return false;
memset(memo, -1, sizeof(memo));
return can_win(0, 0, maxChoosableInteger, desiredTotal);
}The function canIWin decides if a player can win given the constraints. It first checks for immediate game-ending scenarios. The recursive function can_win manages game state using an array to track used numbers, utilizing memoization to store and reuse results of previously computed states.
The time complexity is O(2^n) due to exploring all possible states, where n is maxChoosableInteger. The space complexity is also O(2^n) because of the memory used in the memoization map.
Approach 3: Dynamic Programming
This approach uses dynamic programming to solve the problem. We create a table that keeps track of possible outcomes based on the current state of integers used. It optimizes the recursive solution by considering only valid states and storing their outcomes more directly.
def canIWin(maxChoosableInteger, desiredTotal):
if desiredTotal <= maxChoosableInteger:
return True
if maxChoosableInteger * (maxChoosableInteger + 1) // 2 < desiredTotal:
return False
from functools import lru_cache
@lru_cache(None)
def dp(used):
total = sum(i + 1 for i in range(maxChoosableInteger) if (used >> i) & 1)
for i in range(maxChoosableInteger):
if not (used >> i) & 1:
if total + i + 1 >= desiredTotal or not dp(used | (1 << i)):
return True
return False
return dp(0)#include <unordered_map>
#include <vector>
class Solution {
public:
bool canIWin(int maxChoosableInteger, int desiredTotal) {
if (desiredTotal <= maxChoosableInteger) return true;
if (maxChoosableInteger * (maxChoosableInteger + 1) / 2 < desiredTotal) return false;
std::unordered_map<int, bool> memo;
return dp(0, maxChoosableInteger, desiredTotal, memo);
}
private:
bool dp(int used, int maxCI, int total, std::unordered_map<int, bool>& memo) {
if (memo.count(used)) return memo[used];
int current = 0;
for (int i = 0; i < maxCI; ++i) {
if (!(used & (1 << i))) {
if (current + i + 1 >= total || !dp(used | (1 << i), maxCI, total - i - 1, memo)) {
return memo[used] = true;
}
}
}
return memo[used] = false;
}
};This Python solution employs a dynamic programming strategy with memoization to track used numbers as binary masks. It efficiently avoids recomputation of previously evaluated states, thanks to the lru_cache mechanism.
Time complexity is O(2^n) similar to the minimax approach due to bitmasking possibilities, but this is managed efficiently via memoization. Space complexity is also O(2^n).
Approach 4: State Compression + Memoization
First, we check if the sum of all selectable integers is less than the target value. If so, it means that we cannot win no matter what, so we directly return false.
Then, we design a function dfs(mask, s), where mask represents the current state of the selected integers, and s represents the current cumulative sum. The return value of the function is whether the current player can win.
The execution process of the function dfs(mask, s) is as follows:
We iterate through each integer i from 1 to maxChoosableInteger. If i has not been selected, we can choose i. If the cumulative sum s + i after choosing i is greater than or equal to the target value desiredTotal, or if the result of the opponent choosing i is losing, then the current player is winning, return true.
If no choice can make the current player win, then the current player is losing, return false.
To avoid repeated calculations, we use a hash table f to record the calculated states, where the key is mask, and the value is whether the current player can win.
The time complexity is O(2^n), and the space complexity is O(2^n). Where n is maxChoosableInteger.
function canIWin(maxChoosableInteger: number, desiredTotal: number): boolean {
if (((1 + maxChoosableInteger) * maxChoosableInteger) / 2 < desiredTotal) {
return false;
}
const f: Record<string, boolean> = {};
const dfs = (mask: number, s: number): boolean => {
if (f.hasOwnProperty(mask)) {
return f[mask];
}
for (let i = 1; i <= maxChoosableInteger; ++i) {
if (((mask >> i) & 1) ^ 1) {
if (s + i >= desiredTotal || !dfs(mask ^ (1 << i), s + i)) {
return (f[mask] = true);
}
}
}
return (f[mask] = false);
};
return dfs(0, 0);
}func canIWin(maxChoosableInteger int, desiredTotal int) bool {
if (1+maxChoosableInteger)*maxChoosableInteger/2 < desiredTotal {
return false
}
f := map[int]bool{}
var dfs func(int, int) bool
dfs = func(mask, s int) bool {
if v, ok := f[mask]; ok {
return v
}
for i := 1; i <= maxChoosableInteger; i++ {
if mask>>i&1 == 0 {
if s+i >= desiredTotal || !dfs(mask|1<<i, s+i) {
f[mask] = true
return true
}
}
}
f[mask] = false
return false
}
return dfs(0, 0)
}Complexity comparison
| Approach | Complexity |
|---|---|
| Dynamic Programming with Bitmask | Time Complexity: O(2^n) where n is the |
| Minimax with Memoization | The time complexity is O(2^n) due to exploring all possible states, where n is maxChoosableInteger. The space complexity is also O(2^n) because of the memory used in the memoization map. |
| Dynamic Programming | Time complexity is O(2^n) similar to the minimax approach due to bitmasking possibilities, but this is managed efficiently via memoization. Space complexity is also O(2^n). |
| State Compression + Memoization | – |
Complexity analysis
| Approach | Time | Space | When to Use |
|---|---|---|---|
| Dynamic Programming with Bitmask | O(n · 2^n) | O(2^n) | Best general solution when n ≤ 20 and states can be represented as used-number masks |
| Minimax with Memoization | O(n · 2^n) | O(2^n) | When reasoning about optimal play in two-player games |
| Recursive DP with State Pruning | O(n · 2^n) | O(2^n) | Useful when framing the state as remaining target and applying early pruning checks |
Frequently asked questions
Is Can I Win easy or hard?
LeetCode classifies it as Medium, but many candidates find it closer to medium-hard because it requires combining recursion, bitmask state compression, and memoization. Recognizing the 2^n state space is the key insight.
Can I Win Python/Java solution
Implement a recursive function that receives the current bitmask of used numbers and the remaining target. Try every unused number, update the mask with bit operations, and memoize whether that state leads to a win. The same logic works in Python, Java, and C++ with a map or array cache.
How to solve Can I Win in O(n)?
The problem cannot be solved in O(n) because every subset of chosen numbers can represent a different game state. The best known approach explores these states with memoization using bitmask DP, leading to O(n · 2^n) time.
What is the best approach for Can I Win?
The standard solution uses minimax with memoization and a bitmask to represent which numbers are already chosen. Each game state is cached so it is evaluated once. This reduces the search space to about 2^n states, giving O(n · 2^n) time and O(2^n) space complexity.
Is Can I Win asked at Google/Amazon/Meta?
This style of problem appears in interviews at companies like Google, Amazon, and Meta because it tests recursion, memoization, and game theory reasoning. Candidates must model the game state efficiently and avoid recomputing states using bitmask caching.
What data structure is used in Can I Win?
The key structure is a bitmask representing which numbers from 1..n are already used. A hash map or array memo table stores results for each mask so the algorithm does not recompute the same state during recursion.
What is the time complexity of Can I Win?
The optimized solution runs in O(n · 2^n) time. There are at most 2^n possible subsets of chosen numbers, and for each state the algorithm may try up to n candidate moves. Space complexity is O(2^n) for the memoization table.
