Nim Game: Complete Journey
Problem: Given
nstones, two players take turns removing 1-3 stones. The player who takes the last stone wins. Both play optimally. Can the first player win?
Problem Statement
LeetCode 292 - Nim Game
You are playing a Nim Game with your friend:
- Initially, there are
nstones in a pile - You and your friend take turns removing stones
- On each turn, you can remove 1, 2, or 3 stones
- The one who removes the last stone wins
- Both players play optimally
Given n, return true if you can win the game assuming both players play optimally, otherwise return false.
Example 1:
Input: n = 4
Output: false
Explanation:
- You take 1 → 3 left → opponent takes 3 → opponent wins
- You take 2 → 2 left → opponent takes 2 → opponent wins
- You take 3 → 1 left → opponent takes 1 → opponent wins
All moves lead to opponent winning!
Example 2:
Input: n = 1
Output: true
Explanation: You take 1 stone and win immediately.
Common Mistakes & Solutions
This problem is excellent for learning game theory DP because it exposes three critical mistakes beginners make.
❌ Mistake 1: Incorrect Base Case Logic
// WRONG!
if (number == 0 && whoisme == true) {
return true; // "If it's my turn and 0 stones left, I won"
}
Why it's wrong: If it's your turn and there are 0 stones left, you've already lost! The opponent took the last stone on their previous turn. The game is over, and you're staring at an empty pile.
Think about it:
- The person who takes the last stone wins
- If
n = 0on your turn, the last stone was taken by your opponent - You have no moves available → you lose
✅ Correct Base Case:
// If no stones left, current player has already lost
if (n == 0) {
return false;
}
// Alternative: More explicit base cases
if (n <= 3 && isMyTurn) {
return true; // I can take all remaining stones and win
}
if (n <= 3 && !isMyTurn) {
return false; // Opponent can take all remaining stones and win
}
❌ Mistake 2: Not Subtracting Stones from State
// WRONG!
a = func(1, !whoisme) // Should be: func(number - 1, !whoisme)
b = func(2, !whoisme) // Should be: func(number - 2, !whoisme)
c = func(3, !whoisme) // Should be: func(number - 3, !whoisme)
Why it's wrong: You're trying to explore what happens when you REMOVE stones. The parameters to the recursive call should represent the NEW STATE after your move.
Think about it:
- If there are 10 stones and you take 3, how many are left? 7!
- The next state should have
n - 3stones - You're calling
func(3, ...)which means "3 stones left", not "take 3 stones"
✅ Correct Recursion:
// Try all possible moves
if (n >= 1) a = canWinNim(n - 1, !isMyTurn); // Take 1 → (n-1) left
if (n >= 2) b = canWinNim(n - 2, !isMyTurn); // Take 2 → (n-2) left
if (n >= 3) c = canWinNim(n - 3, !isMyTurn); // Take 3 → (n-3) left
Pro tip: Always validate moves before making them (if (n >= 1), if (n >= 2), etc.) to avoid invalid states.
❌ Mistake 3: Wrong Return Logic (The Most Subtle!)
// WRONG!
return a || b || c; // "I win if opponent wins from any move??"
Why it's wrong: This is the most confusing part of game theory DP. Let's break it down:
What do a, b, c represent?
a = canWinNim(n-1, false)means "Can opponent win with n-1 stones?"b = canWinNim(n-2, false)means "Can opponent win with n-2 stones?"c = canWinNim(n-3, false)means "Can opponent win with n-3 stones?"
So return a || b || c means:
- "I win if opponent wins from at least one of my moves"
- That makes NO sense! If opponent wins, YOU lose!
✅ Correct Return Logic:
// On my turn: I win if ANY move puts opponent in a losing position
return !a || !b || !c; // At least one move makes opponent lose
// Equivalently (using De Morgan's Law):
return !(a && b && c); // I lose only if ALL moves lead to opponent winning
Think about it:
!ameans "opponent CANNOT win after I take 1 stone"!bmeans "opponent CANNOT win after I take 2 stones"!cmeans "opponent CANNOT win after I take 3 stones"!a || !b || !cmeans "There exists at least one move that makes opponent lose"- If opponent loses, you WIN! ✅
Mental Model:
Winning = "I have at least ONE move that puts opponent in a losing position"
Losing = "ALL my moves put opponent in a winning position"
Step-by-Step Solution Development
Step 1: Unoptimized Recursion
public boolean canWinNim(int n, boolean isMyTurn) {
// Base case: no stones left means current player loses
if (n == 0) {
return false;
}
// Base case: can take all remaining stones
if (n <= 3 && isMyTurn) {
return true; // I win by taking all stones
}
if (n <= 3 && !isMyTurn) {
return false; // Opponent wins by taking all stones
}
if (isMyTurn) {
// Try all possible moves (take 1, 2, or 3 stones)
boolean take1 = canWinNim(n - 1, false); // After I take 1
boolean take2 = canWinNim(n - 2, false); // After I take 2
boolean take3 = canWinNim(n - 3, false); // After I take 3
// I win if ANY move puts opponent in losing position
return !take1 || !take2 || !take3;
} else {
// Opponent's turn
boolean take1 = canWinNim(n - 1, true);
boolean take2 = canWinNim(n - 2, true);
boolean take3 = canWinNim(n - 3, true);
// Opponent will make best move for them (worst for me)
// I lose if they have ANY winning move
return !(take1 || take2 || take3);
}
}
Complexity:
- Time: O(3^n) - exponential! Each call branches into 3 more calls
- Space: O(n) - recursion depth
Call this with: canWinNim(n, true) (you go first)
Step 2: Add Memoization (Top-Down DP)
public boolean canWinNim(int n) {
// Cache: [n][turn] where turn: 0 = my turn, 1 = opponent's turn
Boolean[][] memo = new Boolean[n + 1][2];
return canWin(n, true, memo);
}
private boolean canWin(int n, boolean isMyTurn, Boolean[][] memo) {
// Base cases
if (n == 0) return false;
if (n <= 3 && isMyTurn) return true;
if (n <= 3 && !isMyTurn) return false;
// Check cache
int turn = isMyTurn ? 0 : 1;
if (memo[n][turn] != null) {
return memo[n][turn]; // Already computed!
}
boolean result;
if (isMyTurn) {
boolean take1 = canWin(n - 1, false, memo);
boolean take2 = canWin(n - 2, false, memo);
boolean take3 = canWin(n - 3, false, memo);
result = !take1 || !take2 || !take3;
} else {
boolean take1 = canWin(n - 1, true, memo);
boolean take2 = canWin(n - 2, true, memo);
boolean take3 = canWin(n - 3, true, memo);
result = !(take1 || take2 || take3);
}
memo[n][turn] = result; // Cache result
return result;
}
Complexity:
- Time: O(n) - compute each state only once
- Space: O(n) - cache size
Huge improvement: Exponential → Linear time!
Step 3: Bottom-Up DP (Iterative)
public boolean canWinNim(int n) {
if (n <= 3) return true;
// dp[i][0] = can I win with i stones on my turn
// dp[i][1] = can I win with i stones on opponent's turn
boolean[][] dp = new boolean[n + 1][2];
// Base cases
for (int i = 1; i <= 3 && i <= n; i++) {
dp[i][0] = true; // My turn: I can take all stones and win
dp[i][1] = false; // Opponent's turn: they take all and I lose
}
// Fill table bottom-up
for (int i = 4; i <= n; i++) {
// My turn: I win if ANY move puts opponent in losing position
dp[i][0] = !dp[i-1][1] || !dp[i-2][1] || !dp[i-3][1];
// Opponent's turn: I win only if ALL their moves lead to my winning
dp[i][1] = dp[i-1][0] && dp[i-2][0] && dp[i-3][0];
}
return dp[n][0];
}
Complexity:
- Time: O(n)
- Space: O(n)
No recursion overhead: Iterative is slightly faster in practice.
Step 4: Mathematical Insight ⚡ (O(1) Solution!)
After building the DP table, you'll notice a pattern:
n=1: true (take 1, win)
n=2: true (take 2, win)
n=3: true (take 3, win)
n=4: false (opponent wins)
n=5: true (take 1 → leave 4 for opponent)
n=6: true (take 2 → leave 4 for opponent)
n=7: true (take 3 → leave 4 for opponent)
n=8: false (opponent wins)
n=9: true
...
Pattern: You lose if and only if n % 4 == 0!
Why?
- If
n % 4 == 0: Whatever you take (1, 2, or 3), opponent can take4 - your_moveto maintain the pattern- You take 1 → they take 3 → back to multiple of 4
- You take 2 → they take 2 → back to multiple of 4
- You take 3 → they take 1 → back to multiple of 4
- If
n % 4 != 0: You can always make it divisible by 4 for your opponentn % 4 == 1→ take 1 → leave multiple of 4n % 4 == 2→ take 2 → leave multiple of 4n % 4 == 3→ take 3 → leave multiple of 4
✅ Final O(1) Solution:
public boolean canWinNim(int n) {
return n % 4 != 0;
}
Complexity:
- Time: O(1) - single modulo operation!
- Space: O(1) - no extra space
Mind-blowing optimization: From exponential to constant time!
Drawing Decision Trees (Highly Recommended!)
Always draw the game tree for small inputs to understand the pattern.
Example: n = 4
n=4 (MY turn)
/ | \
take 1 take 2 take 3
/ | \
n=3(OPP) n=2(OPP) n=1(OPP)
opp takes opp takes opp takes
all 3 all 2 1 stone
OPP WINS OPP WINS OPP WINS
✗ ✗ ✗
All my moves → opponent wins → I LOSE → return false ✅
Example: n = 5
n=5 (MY turn)
/ | \
take 1 take 2 take 3
/ | \
n=4(OPP) n=3(OPP) n=2(OPP)
opp loses opp takes opp takes
(from all 3 all 2
above) OPP WINS OPP WINS
I WIN! ✗ ✗
✓
At least ONE move → opponent loses → I WIN → return true ✅
Key Takeaways
🎯 Perspective Switching
- Recursive calls return result from opponent's perspective
- You must negate to get your perspective
!oppWinsmeans "opponent doesn't win" = "you win"
🎯 Base Cases Matter
n == 0: Current player has lost (opponent took last stone)n <= 3 && myTurn: I can take all and win- Think carefully about who wins in terminal states
🎯 State Transitions
- Subtract stones you're taking:
n - move - Flip turn:
!isMyTurn - Always validate move is legal before making it
🎯 Return Logic Pattern
// I win if ANY move makes opponent lose
return !move1_opp_wins || !move2_opp_wins || !move3_opp_wins;
// Equivalently
return !(move1_opp_wins && move2_opp_wins && move3_opp_wins);
🎯 Optimization Progression
- Unoptimized Recursion: Understand the logic (O(3^n))
- Memoization: Cache results (O(n))
- Bottom-Up: Remove recursion overhead (O(n))
- Mathematical Pattern: Look for shortcuts (O(1))
Practice Challenge
Try implementing this yourself from scratch:
- Draw the decision tree for n=1,2,3,4,5,6
- Write the unoptimized recursive solution
- Add memoization
- Convert to bottom-up DP
- Spot the pattern and write O(1) solution
Debugging tip: If your solution is wrong, print the decision tree for small n and trace through your logic step by step.
Related Problems
Once you master Nim Game, try these variants:
- Stone Game (LC 877) - Pick from ends of array instead of taking arbitrary amounts
- Stone Game II (LC 1140) - Variable number of stones you can take (controlled by M)
- Stone Game III (LC 1406) - Can take 1, 2, or 3 stones from beginning of array
- Can I Win (LC 464) - Similar but with number selection and target sum
Remember: Game theory DP is all about perspective. You're optimizing for yourself while assuming your opponent does the same. The key insight is that return !opponent_wins means YOU win! 🎯