The "Can I Win" problem is a two-player game where players take turns picking unique integers from 1
to maxChoosableInteger
. Each number can only be chosen once. The chosen numbers are added together, and the goal is to be the first to reach or exceed a given desiredTotal
. The function must determine if the first player can guarantee a win, assuming both players play optimally.
1
and maxChoosableInteger
.desiredTotal
wins.true
if the first player can guarantee a win, false
otherwise.At first glance, the problem seems to require checking all possible sequences of moves, which grows exponentially as the number of choices increases. A brute-force approach would try every possible pick for each player at every turn, but this quickly becomes infeasible for larger inputs.
The key insight is to recognize that the game's state can be represented by the set of unused numbers and the current total. If we know which numbers are left and what the target is, the rest of the game is deterministic. This allows us to use memoization (caching) to avoid recomputing the same situations. We also realize that if the sum of all available numbers is less than the desiredTotal
, it is impossible for any player to win, so we can return false
immediately.
Instead of thinking in terms of all possible move sequences, we can reframe the problem as: "Can the current player force a win from this state?" We recursively check if there's any move that leaves the opponent in a losing position.
To solve the problem efficiently, we use recursion with memoization. The main steps are:
1
to maxChoosableInteger
is less than desiredTotal
, return false
immediately.desiredTotal
is 0 or less, the first player wins by default.desiredTotal
, the current player wins.This approach ensures that each unique state is only computed once, making the solution efficient even for larger inputs.
Let's walk through an example with maxChoosableInteger = 4
and desiredTotal = 6
.
This step-by-step analysis is handled efficiently by recursion and memoization in the code.
maxChoosableInteger
, since each move branches to all unused numbers.This is efficient for N up to 20, which is the constraint in the original problem.
The "Can I Win" problem is an example of recursive game theory with memoization. By representing the state as a bitmask and caching results, we avoid redundant calculations and efficiently determine if the first player can force a win. The key insights are recognizing the game's state structure and using memoization to optimize the search space. This approach transforms an exponential-time brute-force problem into a manageable solution, making it both elegant and practical.
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 can_win(used, curr_total):
if used in memo:
return memo[used]
for i in range(1, maxChoosableInteger + 1):
if not (used >> (i-1)) & 1:
if curr_total + i >= desiredTotal:
memo[used] = True
return True
if not can_win(used | (1 << (i-1)), curr_total + i):
memo[used] = True
return True
memo[used] = False
return False
return can_win(0, 0)
class Solution {
public:
bool canIWin(int maxChoosableInteger, int desiredTotal) {
if (desiredTotal <= 0) return true;
int totalSum = (maxChoosableInteger + 1) * maxChoosableInteger / 2;
if (totalSum < desiredTotal) return false;
unordered_map<int, bool> memo;
function<bool(int, int)> canWin = [&](int used, int currTotal) {
if (memo.count(used)) return memo[used];
for (int i = 1; i <= maxChoosableInteger; ++i) {
if (!(used & (1 << (i - 1)))) {
if (currTotal + i >= desiredTotal) {
memo[used] = true;
return true;
}
if (!canWin(used | (1 << (i - 1)), currTotal + i)) {
memo[used] = true;
return true;
}
}
}
memo[used] = false;
return false;
};
return canWin(0, 0);
}
};
import java.util.*;
class Solution {
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
if (desiredTotal <= 0) return true;
int totalSum = (maxChoosableInteger + 1) * maxChoosableInteger / 2;
if (totalSum < desiredTotal) return false;
Map<Integer, Boolean> memo = new HashMap<>();
return canWin(0, 0, maxChoosableInteger, desiredTotal, memo);
}
private boolean canWin(int used, int currTotal, int maxChoosable, int target, Map<Integer, Boolean> memo) {
if (memo.containsKey(used)) return memo.get(used);
for (int i = 1; i <= maxChoosable; i++) {
if ((used & (1 << (i - 1))) == 0) {
if (currTotal + i >= target) {
memo.put(used, true);
return true;
}
if (!canWin(used | (1 << (i - 1)), currTotal + i, maxChoosable, target, memo)) {
memo.put(used, true);
return true;
}
}
}
memo.put(used, false);
return false;
}
}
var canIWin = function(maxChoosableInteger, desiredTotal) {
if (desiredTotal <= 0) return true;
let totalSum = (maxChoosableInteger + 1) * maxChoosableInteger / 2;
if (totalSum < desiredTotal) return false;
const memo = new Map();
function canWin(used, currTotal) {
if (memo.has(used)) return memo.get(used);
for (let i = 1; i <= maxChoosableInteger; i++) {
if (((used >> (i - 1)) & 1) === 0) {
if (currTotal + i >= desiredTotal) {
memo.set(used, true);
return true;
}
if (!canWin(used | (1 << (i - 1)), currTotal + i)) {
memo.set(used, true);
return true;
}
}
}
memo.set(used, false);
return false;
}
return canWin(0, 0);
};