Can I Win
MedPrompt
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: trueExample 3:
Input: maxChoosableInteger = 10, desiredTotal = 1
Output: true
Constraints:
1 <= maxChoosableInteger <= 200 <= desiredTotal <= 300
Approaches
2 approaches with complexity analysis and trade-offs.
This approach models the game by exploring every possible sequence of moves using a recursive function. It directly translates the game's rules into a recursive structure without any optimization for repeated computations. It represents the game tree and explores every path to determine if a winning strategy exists for the first player.
Algorithm
- Create a recursive function
canWin(usedNumbers, currentTotal). - Loop from
i = 1tomaxChoosableInteger. - If
iis not inusedNumbers:- If
currentTotal + i >= desiredTotal, returntrueas this is a winning move. - Recursively call
canWinfor the opponent:opponentLoses = !canWin(usedNumbers + {i}, currentTotal + i). - If
opponentLosesistrue, it means we found a move that guarantees a win. Returntrue.
- If
- If the loop finishes without finding a winning move, it means all moves lead to the opponent winning. Return
false.
Walkthrough
This approach models the game by exploring every possible sequence of moves using a recursive function. It directly translates the game's rules into a recursive structure without any optimization for repeated computations.
- We define a recursive function, say
canWinRecursive(usedNumbers, currentTotal), which returnstrueif the current player can force a win given the set ofusedNumbersand thecurrentTotal. - In each call, the function iterates through all numbers from 1 to
maxChoosableInteger. - For each available number, it simulates picking that number.
- If picking a number
imakes thecurrentTotal + ireach or exceeddesiredTotal, the current player wins. - Otherwise, it makes a recursive call for the opponent's turn with the updated set of used numbers and the new total. The current player wins if the opponent loses in the subsequent state (i.e., the recursive call returns
false). - If after trying all available numbers, no winning move is found, the current player loses.
- This method explores the entire game tree, which includes many redundant calculations for the same game states reached through different sequences of moves.
// Conceptual code for brute-force recursionprivate boolean canWinRecursive(java.util.Set<Integer> used, int currentTotal, int maxChoosableInteger, int desiredTotal) { // Iterate through all possible numbers for (int i = 1; i <= maxChoosableInteger; i++) { // If the number is not used yet if (!used.contains(i)) { // Check if this move wins if (currentTotal + i >= desiredTotal) { return true; } // Make the move and recurse for the other player used.add(i); // If the other player CANNOT win, then this is a winning move for us if (!canWinRecursive(used, currentTotal + i, maxChoosableInteger, desiredTotal)) { used.remove(i); // Backtrack return true; } used.remove(i); // Backtrack } } // If no winning move is found return false;}Complexity
Time
O(N!), where N is `maxChoosableInteger`. The function explores all permutations of numbers, leading to a factorial number of paths in the game tree.
Space
O(N), where N is `maxChoosableInteger`. This space is used for the recursion stack depth and to store the set of used numbers.
Trade-offs
Pros
Simple to understand and implement as it directly models the game logic.
Cons
Extremely inefficient due to re-computation of the same subproblems.
The time complexity is factorial, making it infeasible for the given constraints.
Will result in a 'Time Limit Exceeded' error on most coding platforms.
This approach optimizes the recursive solution by using memoization to store and reuse the results of subproblems. A subproblem is uniquely defined by the set of numbers that have already been used. This avoids re-computing the outcome for the same game state multiple times, drastically improving efficiency.
Algorithm
- Handle edge cases: if
desiredTotal <= 0, returntrue. If the sum of all choosable numbers is less thandesiredTotal, returnfalse. - Create a memoization array
memoof size2^maxChoosableIntegerto store results of subproblems. - Define a recursive helper function
canWinHelper(mask, remainingTotal). - Inside the helper function:
- If
remainingTotal <= 0, it means the previous player won, so the current player loses. Returnfalse. - If
memo[mask]is already computed, return the stored value. - Loop
ifrom 1 tomaxChoosableInteger. - If number
ihas not been used (check themask):- Make a recursive call for the opponent:
canWinHelper(newMask, remainingTotal - i). - If the opponent loses (the call returns
false), it means the current move is a winning one. Setmemo[mask] = trueand returntrue.
- Make a recursive call for the opponent:
- If
- If the loop completes without finding a winning move, the current player cannot force a win from this state. Set
memo[mask] = falseand returnfalse. - Start the process by calling
canWinHelper(0, desiredTotal).
Walkthrough
The key observation is that the outcome of the game from any point depends only on the set of available numbers, not the order in which previous numbers were picked. This allows us to memoize results for game states.
- State Representation: We can represent the set of used numbers efficiently using a bitmask. A bitmask is an integer where the
i-1-th bit is set to 1 if the numberihas been used, and 0 otherwise. SincemaxChoosableIntegeris at most 20, a single 32-bit integer is sufficient to represent any state. - Memoization: We use a memoization table,
Boolean[] memo, of size2^maxChoosableInteger.memo[mask]will storetrueif the current player can win from the state represented bymask, andfalseotherwise. - Recursive Helper: A recursive function,
canWinHelper(mask, remainingTotal), is defined.maskrepresents the used numbers, andremainingTotalis the target sum to reach. - The function first checks the memoization table. If the result for the current
maskis already computed, it returns the stored value. - It then iterates from 1 to
maxChoosableInteger. For each available numberi, it simulates picking it. - The current player wins if they can find a move
isuch that the opponent cannot win from the resulting state. This is checked by a recursive call:!canWinHelper(newMask, remainingTotal - i). - If a winning move is found, the result (
true) is stored in the memoization table for the currentmask, andtrueis returned. - If all possible moves are explored and none lead to a win, the result (
false) is stored, andfalseis returned. - Initial edge cases are handled first: if the total sum of all numbers is less than
desiredTotal, it's impossible to win.
class Solution { public boolean canIWin(int maxChoosableInteger, int desiredTotal) { if (desiredTotal <= 0) { return true; } if ((maxChoosableInteger * (maxChoosableInteger + 1)) / 2 < desiredTotal) { return false; } Boolean[] memo = new Boolean[1 << maxChoosableInteger]; return canWinHelper(0, desiredTotal, maxChoosableInteger, memo); } private boolean canWinHelper(int mask, int remainingTotal, int maxChoosableInteger, Boolean[] memo) { if (remainingTotal <= 0) { return false; // Previous player won } if (memo[mask] != null) { return memo[mask]; } for (int i = 1; i <= maxChoosableInteger; i++) { // Check if the bit for number `i` is not set in the mask. // The bit for number `i` is at position `i-1`. if ((mask & (1 << (i - 1))) == 0) { // If the opponent cannot win from the next state, then we win. if (!canWinHelper(mask | (1 << (i - 1)), remainingTotal - i, maxChoosableInteger, memo)) { memo[mask] = true; return true; } } } memo[mask] = false; return false; }}Complexity
Time
O(N * 2^N), where N is `maxChoosableInteger`. Each of the `2^N` states is computed once. Inside each computation, we loop up to `N` times to find an available number.
Space
O(2^N), where N is `maxChoosableInteger`. This space is dominated by the memoization table. The recursion depth adds O(N), which is negligible.
Trade-offs
Pros
Efficient enough to pass within the given constraints by avoiding redundant computations.
Correctly solves the problem by applying the minimax principle with memoization.
The use of a bitmask for state representation is highly efficient in both time and space for small N.
Cons
The space complexity is exponential,
O(2^N), which might be a concern for larger values ofmaxChoosableInteger(though it's fine for N <= 20).
Solutions
Solution
class Solution {private Map<Integer, Boolean> memo = new HashMap<>();public boolean canIWin(int maxChoosableInteger, int desiredTotal) { int s = (1 + maxChoosableInteger) * maxChoosableInteger / 2; if (s < desiredTotal) { return false; } return dfs(0, 0, maxChoosableInteger, desiredTotal); }private boolean dfs(int state, int t, int maxChoosableInteger, int desiredTotal) { if (memo.containsKey(state)) { return memo.get(state); } boolean res = false; for (int i = 1; i <= maxChoosableInteger; ++i) { if (((state >> i) & 1) == 0) { if (t + i >= desiredTotal || !dfs(state | 1 << i, t + i, maxChoosableInteger, desiredTotal)) { res = true; break; } } } memo.put(state, res); return res; }}class Solution {public: bool canIWin(int maxChoosableInteger, int desiredTotal) { int s = (1 + maxChoosableInteger) * maxChoosableInteger / 2; if (s < desiredTotal) return false; unordered_map<int, bool> memo; return dfs(0, 0, maxChoosableInteger, desiredTotal, memo); } bool dfs(int state, int t, int maxChoosableInteger, int desiredTotal, unordered_map<int, bool> &memo) { if (memo.count(state)) return memo[state]; bool res = false; for (int i = 1; i <= maxChoosableInteger; ++i) { if ((state >> i) & 1) continue; if (t + i >= desiredTotal || !dfs(state | 1 << i, t + i, maxChoosableInteger, desiredTotal, memo)) { res = true; break; } } memo[state] = res; return res; }};class Solution: def canIWin(self, maxChoosableInteger: int, desiredTotal: int) -> bool: @ cache def dfs(state, t): for i in range(1, maxChoosableInteger + 1): if (state >> i) & 1: continue if t + i >= desiredTotal or not dfs(state | 1 << i, t + i): return True return False s = (1 + maxChoosableInteger) * maxChoosableInteger // 2 if s < desiredTotal: return False return dfs(0, 0)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.