Chalkboard XOR Game
HardPrompt
You are given an array of integers nums represents the numbers written on a chalkboard.
Alice and Bob take turns erasing exactly one number from the chalkboard, with Alice starting first. If erasing a number causes the bitwise XOR of all the elements of the chalkboard to become 0, then that player loses. The bitwise XOR of one element is that element itself, and the bitwise XOR of no elements is 0.
Also, if any player starts their turn with the bitwise XOR of all the elements of the chalkboard equal to 0, then that player wins.
Return true if and only if Alice wins the game, assuming both players play optimally.
Example 1:
Input: nums = [1,1,2]
Output: false
Explanation:
Alice has two choices: erase 1 or erase 2.
If she erases 1, the nums array becomes [1, 2]. The bitwise XOR of all the elements of the chalkboard is 1 XOR 2 = 3. Now Bob can remove any element he wants, because Alice will be the one to erase the last element and she will lose.
If Alice erases 2 first, now nums become [1, 1]. The bitwise XOR of all the elements of the chalkboard is 1 XOR 1 = 0. Alice will lose.Example 2:
Input: nums = [0,1]
Output: trueExample 3:
Input: nums = [1,2,3]
Output: true
Constraints:
1 <= nums.length <= 10000 <= nums[i] < 216
Approaches
2 approaches with complexity analysis and trade-offs.
A standard approach for solving game theory problems is to model the game as a state graph and use recursion with memoization (a form of dynamic programming) to determine the outcome. A state is defined by the numbers currently on the chalkboard. The function canWin determines if the current player can win from a given state. We explore all possible moves from the current state. If any move leads to a state where the opponent is guaranteed to lose, the current player has a winning strategy. Memoization is crucial to cache the results for already-visited states, pruning the search space, but the number of states is still prohibitively large.
Algorithm
- Define a recursive function, say
canWin(currentNums, memo), that returnstrueif the current player can win from the state defined bycurrentNums. - The state can be uniquely identified by the list of numbers. To use it as a key in a memoization map, we can sort the list and convert it to a string or use the list object itself (if its
hashCodeandequalsmethods are handled correctly). - Base Case/Winning Condition: Calculate the bitwise XOR sum
Xof all numbers incurrentNums. IfX == 0, the current player wins immediately. Returntrue. - Recursive Step: The current player needs to find a move that leads to a state from which the opponent cannot win.
- Iterate through each number
numincurrentNums. - A move consists of removing
num. This move is a losing move for the current player if the new XOR sum becomes 0. The new XOR sum isX ^ num. So, the player must choose anumsuch thatX ^ num != 0. - For each such valid move, create the next state
nextNums = currentNums - {num}. - Recursively call
canWin(nextNums, memo). If this call returnsfalse, it means we found a move where the opponent loses. Therefore, the current player wins. We can returntrue. - If after trying all valid moves, none lead to a state where the opponent loses (i.e., all recursive calls return
true), then the current player has no winning move and will lose. Returnfalse. - Before returning, store the result in the memoization table to avoid recomputing for the same state.
Walkthrough
This approach simulates the game turn by turn. We define a function canWin which takes the current list of numbers on the chalkboard. To avoid re-calculating the result for the same set of numbers, we use a map for memoization, where the key represents the state (the list of numbers) and the value is the boolean result.
The logic inside canWin is as follows:
- First, check if the result for the current state is already in our memoization table. If so, return the cached value.
- Calculate the XOR sum of the current numbers. If it's 0, the current player wins by the game's rules. We store
trueand return it. - If the XOR sum is not 0, the player must make a move. A move is picking a number
numto erase. The player loses if this move makes the new XOR sum 0. So, a player must pick anumsuch thatcurrent_xor_sum ^ num != 0. - We iterate through all possible valid moves. For each move, we check if the opponent can win from the resulting state by making a recursive call. If we find any move for which the opponent cannot win (the recursive call returns
false), it means we have found a winning line of play. We can then cache and returntrue. - If we iterate through all valid moves and find that the opponent always has a winning response, then the current player will lose from this state. We cache and return
false.
import java.util.*; class Solution { public boolean xorGame(int[] nums) { List<Integer> initialNums = new ArrayList<>(); for (int num : nums) { initialNums.add(num); } // Sorting is important for consistent map keys if lists are used directly // A better key would be a canonical string representation. Collections.sort(initialNums); return canWin(initialNums, new HashMap<>()); } private boolean canWin(List<Integer> nums, Map<List<Integer>, Boolean> memo) { if (nums.isEmpty()) { return false; // XOR sum is 0, previous player lost, so current player wins. // This case is handled by the xorSum == 0 check. } if (memo.containsKey(nums)) { return memo.get(nums); } int xorSum = 0; for (int num : nums) { xorSum ^= num; } if (xorSum == 0) { memo.put(nums, true); return true; } // Try all possible moves for (int i = 0; i < nums.size(); i++) { int numToRemove = nums.get(i); // A player loses if their move makes the XOR sum 0. // So, a player must make a move that does NOT result in 0. if ((xorSum ^ numToRemove) != 0) { List<Integer> nextNums = new ArrayList<>(nums); nextNums.remove(i); // If the opponent cannot win from the next state, we win. if (!canWin(nextNums, memo)) { memo.put(nums, true); return true; } } } // If all moves lead to a state where the opponent wins, we lose. memo.put(nums, false); return false; }}Note: The above code is conceptually correct but will be too slow for the given constraints. Using a sorted list as a map key is also inefficient.
Complexity
Time
O(N^2 * 2^N). There are `2^N` possible subsets (states). For each state, we iterate through up to `N` numbers. Creating the new list for the recursive call takes O(N). This makes the complexity for each state O(N^2), leading to an overall exponential time complexity.
Space
O(N * 2^N), where N is the number of elements. The memoization table can store up to `2^N` states, and each state (the list of numbers) can take up to O(N) space.
Trade-offs
Pros
It's a general and intuitive method for solving impartial games.
It correctly models the game logic and rules.
Cons
The number of possible subsets of
numsis2^N, leading to an exponential number of states.The time and space complexity are too high for the given constraints (
N <= 1000), resulting in a 'Time Limit Exceeded' or 'Memory Limit Exceeded' error.
Instead of simulating the game, we can analyze its mathematical properties to find a direct solution. This is a common theme in combinatorial game theory. The winner of the game can be determined by two simple properties of the initial state: the bitwise XOR sum of all numbers and the parity (even or odd) of the number of elements. By analyzing the winning and losing conditions, we can deduce a simple rule that predicts the winner without exploring the game tree at all.
Algorithm
- Calculate the bitwise XOR sum of all elements in the
numsarray. - Get the total number of elements,
N, from the length of the array. - Apply the winning condition logic:
- If the initial XOR sum is
0, Alice wins immediately on her first turn. - If the initial XOR sum is not
0, the outcome depends on the number of elementsN. - If
Nis even, Alice is guaranteed to win. - If
Nis odd, Alice will lose (assuming optimal play from Bob).
- If the initial XOR sum is
- Return
trueifxor_sum == 0orN % 2 == 0, andfalseotherwise.
Walkthrough
Let's analyze the game rules to find a winning strategy.
Winning/Losing Conditions:
- A player wins if, at the start of their turn, the XOR sum of all numbers on the board is
0. - A player loses if they make a move (erase a number
num) that causes the XOR sum of the remaining numbers to become0.
Let X be the XOR sum of the numbers on the board at the start of a player's turn.
Case 1: Initial XOR sum is 0.
If the XOR sum of the initial nums array is 0, Alice starts her turn and wins immediately. So, if X_initial == 0, Alice wins.
Case 2: Initial XOR sum is not 0.
Alice must make a move. Let the current XOR sum be X and the number of elements be n. Alice must choose a number num to erase. The new XOR sum will be X ^ num. To not lose, she must choose num such that X ^ num != 0, which means num != X.
Can a player always make a non-losing move?
A player is forced to lose if, for every number num_i on the board, num_i = X. Let's see when this can happen.
If all n numbers on the board are equal to X, their total XOR sum is X ^ X ^ ... ^ X (n times).
- If
nis even, this sum is0. This contradicts our assumption thatX != 0. - If
nis odd, this sum isX. This is consistent.
This means a player can only be forced into a losing position if they face a board with an odd number of elements.
If a player faces a board with an even number of elements and a non-zero XOR sum, it's impossible for all numbers to be equal to the XOR sum. Thus, there must be at least one number num such that num != X. The player can safely remove this num and pass the turn.
Applying this to Alice and Bob:
-
If
nums.lengthis even: Alice starts with an even number of elements. Since the initial XOR sum is not 0, she can always make a non-losing move. After her move, the board will have an odd number of elements for Bob. Bob will then make a move, leaving an even number for Alice. This continues. Alice always plays on a board with an even number of elements, and Bob on a board with an odd number. Since Alice can never be forced to lose, she will continue playing until the board is small. Eventually, Bob will face a board with 1 element. He must remove it, making the XOR sum 0, and he will lose. Thus, ifNis even, Alice wins. -
If
nums.lengthis odd: Alice starts with an odd number of elements. She might be forced to lose on her first move (e.g.,[7,7,7]). Even if she can make a non-losing move, she will leave a board with an even number of elements for Bob. Now, Bob is in the winning position described above. Bob has a strategy to never lose, so Alice will eventually lose. Thus, ifNis odd, Alice loses.
Conclusion:
Alice wins if initial_xor_sum == 0 OR nums.length % 2 == 0.
class Solution { public boolean xorGame(int[] nums) { int xorSum = 0; for (int num : nums) { xorSum ^= num; } // Alice wins if the initial XOR sum is 0. if (xorSum == 0) { return true; } // If the XOR sum is not 0, the game depends on the number of elements. // If N is even, Alice can always make a move such that the XOR sum // of the remaining elements is not 0. Bob will be left in a state // with an odd number of elements. Eventually, Bob will be forced to lose. // If N is odd, Alice makes a move and leaves Bob with an even number of // elements, putting Bob in a winning position. return nums.length % 2 == 0; }}Complexity
Time
O(N), where N is the length of the `nums` array. We need to iterate through the array once to calculate the bitwise XOR sum.
Space
O(1). We only use a single integer variable to store the running XOR sum.
Trade-offs
Pros
Extremely efficient with linear time complexity.
Requires constant extra space.
Provides a complete solution that passes all constraints.
Cons
The solution is not immediately obvious and requires a deep understanding of the game's properties and bitwise XOR.
Solutions
Solution
class Solution {public boolean xorGame(int[] nums) { return nums.length % 2 == 0 || Arrays.stream(nums).reduce(0, (a, b)->a ^ b) == 0; }}class Solution {public: bool xorGame(vector<int> &nums) { if (nums.size() % 2 == 0) return true; int x = 0; for (int &v : nums) x ^= v; return x == 0; }};class Solution: def xorGame( self, nums: List[int]) -> bool: return len(nums) % 2 == 0 or reduce(xor, nums) == 0Video 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.