Game Theory - Nim
Key Points
- •Nim is a two-player impartial game with several piles where a move removes any positive number of stones from exactly one pile.
- •The key invariant is the nim-sum, which is the bitwise XOR of all pile sizes; positions with nim-sum 0 are losing (P-positions), otherwise winning (N-positions).
- •An optimal move from a winning position sets one chosen pile to ' = XOR S, where S is the nim-sum; this makes the new nim-sum 0.
- •The terminal position (all piles zero) has nim-sum 0 and is losing under normal play (last move wins).
- •Misère Nim is identical except when all piles are size 1; in that special case, the first player wins iff the number of piles is even.
- •Bouton’s Theorem formally proves the XOR rule for normal-play Nim.
- •Many impartial games reduce to Nim via Sprague–Grundy theory by computing Grundy numbers and XORing them.
- •In code, deciding the winner is O(n) by computing one XOR; maintaining it under point updates is O(1) per update.
Prerequisites
- →Binary representation and bitwise XOR — Nim’s invariant is the XOR of pile sizes; understanding XOR’s properties is essential.
- →Asymptotic complexity — To analyze O(n) evaluation and O(1) updates of nim-sum in competitive settings.
- →Graphs and DFS (for Grundy) — General impartial games are modeled as DAGs; computing Grundy numbers uses DFS and mex.
- →Modular arithmetic and parity — Misère Nim’s all-ones case depends on even/odd counts of piles.
Detailed Explanation
Tap terms for definitions01Overview
Nim is a classic two-player strategy game that looks simple but hides deep structure. You start with several piles (also called heaps) of stones. Players alternate turns; on each turn, a player selects one pile and removes any positive number of stones from that single pile. The usual rule set is normal play: the player who makes the last move (i.e., takes the final stone) wins. The central discovery, due to Charles Bouton, is that there is a complete and efficient strategy for Nim based on bitwise XOR, also called the nim-sum. If you compute the XOR of all pile sizes, a zero result signals a losing position for the player to move, and a nonzero result signals a winning position. Even better, from any winning position, there is an explicit move that converts the position to a losing one for the opponent by carefully reducing one pile. This XOR characterization makes Nim ideal for algorithmic problem solving: you can determine the winner in linear time, maintain the outcome under updates in constant time, and find a specific optimal move easily. Nim is also a gateway to general impartial game theory via Sprague–Grundy numbers, which map more complex games into equivalent Nim piles.
02Intuition & Analogies
Imagine each bit position across all pile sizes as a row of light switches. Writing numbers in binary, a 1-bit means the switch is ON for that pile at that bit; a 0-bit means it’s OFF. The nim-sum is the XOR of these bits across piles, which is like saying a light is ON if an odd number of switches in that row are ON. A position with nim-sum 0 means every bit-row is balanced: each row has an even number of ONs. If you touch one pile (one column of switches), you will flip some bits and therefore break the perfect evenness in at least one row—unless you choose your flips carefully. The optimal Nim strategy is exactly about restoring balance. From a winning position, the nim-sum S is nonzero, so there is a highest bit where the rows are unbalanced (the topmost ON light). At least one pile has that bit set; by turning exactly the right combination of lower bits in that one pile (i.e., replacing a_i by a_i XOR S), you switch OFF that topmost light and adjust other rows so that all rows become even again—bringing the nim-sum back to 0. Your opponent is then forced to break the balance on their turn, because any legal move toggles some bits in one column and thus flips parities in at least one row. Think of it like a seesaw of parity: when all rows are even (nim-sum 0), any single change creates an odd row (nonzero nim-sum); when some row is odd (nim-sum nonzero), you can carefully nudge a single column to fix every row’s parity. The terminal position has every bit-row even (all zeros), so if it’s your turn and the nim-sum is 0, you are stuck—no move keeps parity even—so you eventually lose against perfect play.
03Formal Definition
04When to Use
Use the Nim XOR rule when each move consists of choosing exactly one pile and reducing it by any positive amount. This covers many competitive programming problems that disguise piles as counters, tokens, or stacks. If the problem is a disjoint sum of such subgames (e.g., several independent piles or boards), compute the nim-sum across all components and apply the same logic. If the game is impartial but moves are restricted (e.g., you may only remove 1, 3, or 4 stones), reduce it to Nim with Sprague–Grundy theory: compute a Grundy number g(state) using g(state) = mex({ g(next) }) and then take the XOR across independent components. Apply normal-play rules unless the statement explicitly uses misère play (last move loses); for misère Nim, use the standard exception: when all piles are of size 1, the first player wins iff the number of piles is even; otherwise use the normal XOR rule. In dynamic settings where piles are updated or queries are frequent, maintain the global XOR S incrementally: when pile i changes from old to new, set S \leftarrow S \oplus old \oplus new for O(1) updates. Finally, if a problem allows changing multiple piles in a single move or introduces dependencies between piles, the pure Nim XOR rule no longer applies; consider modeling with a game graph and computing Grundy numbers.
⚠️Common Mistakes
Common pitfalls include: (1) Summing instead of XORing pile sizes—Nim depends on bitwise XOR, not arithmetic sum. (2) Forgetting to pick a pile that contains the highest set bit of the nim-sum; if you choose a pile without that bit, a_i' = a_i \oplus S might be larger than a_i or invalid. (3) Computing the target a_i' incorrectly; the correct winning move is to set a_i' = a_i \oplus S, not a_i - S or a_i & S. (4) Mixing normal play and misère rules; in misère Nim, the special case when all piles are size 1 flips the parity condition. (5) Assuming that removing from multiple piles in one move still follows the XOR rule—it does not; the Nim solution assumes exactly one pile changes per move. (6) Using signed 32-bit integers for very large pile sizes; prefer 64-bit unsigned or long long for safety in XOR operations. (7) In Sprague–Grundy reductions, forgetting to ensure the game graph is acyclic or to memoize results; cycles need careful handling, and missing memoization can cause exponential time. (8) Off-by-one mistakes when outputting moves (e.g., 0-based vs. 1-based pile indices) and not verifying that the computed removal amount is positive. By checking the highest set bit of the nim-sum, ensuring a_i' < a_i, and using consistent indexing and types, you can avoid these errors.
Key Formulas
Nim-sum
Explanation: The nim-sum S is the bitwise XOR of all pile sizes. It is the key invariant used to classify positions as winning or losing.
Bouton Outcome Criterion
Explanation: A position is losing for the player to move if and only if the XOR of all pile sizes is zero; otherwise it is winning.
Winning Move Construction
Explanation: From an N-position with nim-sum S, pick a pile that has the highest set bit of S and set it to '. This move makes the nim-sum zero and is always a decrease.
Post-move Nim-sum
Explanation: After applying the winning move to pile i, the new nim-sum becomes zero. This puts the opponent in a losing position under perfect play.
Moves from P-positions
Explanation: From a P-position (S=0), any legal move changes one pile and yields a nonzero nim-sum, proving no winning move exists from S=0.
Highest Set Bit
Explanation: The highest set bit of S identifies a bit position that must be present in some pile chosen for the winning move.
Sprague–Grundy Recurrence
Explanation: The Grundy number g(v) of a position v is the minimum excluded value of the Grundy numbers of its followers. This reduces impartial games to Nim heaps.
Disjunctive Sum XOR
Explanation: The Grundy number of a sum of independent subgames is the XOR of their individual Grundy numbers, mirroring Nim’s nim-sum.
Misère Nim (All Ones Case)
Explanation: In misère Nim, when all piles are size 1, the first player wins if and only if the number of nonempty piles is even. Otherwise, apply the normal nim-sum rule.
Complexity Analysis
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Given pile sizes, print who wins under perfect play. 5 // If the first player is winning, also output an optimal move: (pile_index, stones_to_remove). 6 // Indices are 1-based in output. 7 8 int main() { 9 ios::sync_with_stdio(false); 10 cin.tie(nullptr); 11 12 int n; 13 if (!(cin >> n)) return 0; 14 vector<unsigned long long> a(n); 15 for (int i = 0; i < n; ++i) cin >> a[i]; 16 17 unsigned long long S = 0; 18 for (auto v : a) S ^= v; // nim-sum 19 20 if (S == 0) { 21 cout << "Second\n"; // P-position: losing for player to move 22 return 0; 23 } 24 25 // Find highest set bit of S 26 int msb = 63 - __builtin_clzll(S); // valid for S != 0 27 28 // Choose a pile with that bit set and compute target a[i]' = a[i] ^ S 29 for (int i = 0; i < n; ++i) { 30 if ( (a[i] >> msb) & 1ULL ) { 31 unsigned long long target = a[i] ^ S; // strictly less than a[i] 32 // Sanity: target must be < a[i] 33 if (target < a[i]) { 34 unsigned long long remove = a[i] - target; // positive 35 cout << "First\n"; 36 cout << (i + 1) << " " << remove << "\n"; // remove 'remove' stones from pile (i+1) 37 return 0; 38 } 39 } 40 } 41 42 // Control should never reach here if S != 0 and inputs are valid 43 cout << "First\n"; 44 cout << 1 << " " << (a[0] ? a[0] : 0) << "\n"; 45 return 0; 46 } 47
This program computes the nim-sum S. If S = 0, the second player wins. Otherwise, it locates the highest set bit of S and finds a pile that also has this bit set. Setting that pile to a[i] XOR S yields a strictly smaller value, making the new nim-sum zero. It prints the index and the number of stones to remove to achieve this.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Supports Q updates of the form: set pile i to value x, and after each update prints the winner. 5 // Maintains global XOR S in O(1) per update. 6 7 int main() { 8 ios::sync_with_stdio(false); 9 cin.tie(nullptr); 10 11 int n, q; 12 if (!(cin >> n >> q)) return 0; 13 vector<unsigned long long> a(n); 14 unsigned long long S = 0; 15 for (int i = 0; i < n; ++i) { 16 cin >> a[i]; 17 S ^= a[i]; 18 } 19 20 while (q--) { 21 int i; 22 unsigned long long x; 23 cin >> i >> x; // 1-based index 24 --i; 25 // Update S efficiently: remove old contribution, add new 26 S ^= a[i]; 27 a[i] = x; 28 S ^= a[i]; 29 cout << (S == 0 ? "Second" : "First") << '\n'; 30 } 31 32 return 0; 33 } 34
The program reads initial piles and processes updates that set a specific pile to a new size. It keeps the nim-sum S by XORing out the old value and XORing in the new one, enabling O(1) updates. After each update, it prints which player is winning under perfect play.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Misère Nim: last move loses. Rule: 5 // - If all piles are size 1, First wins iff number of piles is even. 6 // - Otherwise, use normal Nim rule (nim-sum != 0 => First wins). 7 8 int main() { 9 ios::sync_with_stdio(false); 10 cin.tie(nullptr); 11 12 int T; cin >> T; 13 while (T--) { 14 int n; cin >> n; 15 vector<unsigned long long> a(n); 16 bool allOnes = true; 17 unsigned long long S = 0; 18 int cntOnes = 0; 19 for (int i = 0; i < n; ++i) { 20 cin >> a[i]; 21 S ^= a[i]; 22 if (a[i] != 1ULL) allOnes = false; 23 if (a[i] == 1ULL) cntOnes++; 24 } 25 bool firstWins; 26 if (allOnes) { 27 // Special parity flip 28 firstWins = (cntOnes % 2 == 0); 29 } else { 30 firstWins = (S != 0); 31 } 32 cout << (firstWins ? "First" : "Second") << '\n'; 33 } 34 35 return 0; 36 } 37
This code implements the misère Nim rule: when all piles are size 1, the parity of the number of piles decides the winner; otherwise, fall back to the normal XOR rule. It handles multiple test cases and outputs the winner for each.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Given a directed acyclic game graph (positions = nodes, edges = legal moves), 5 // and k tokens placed on starting nodes (independent subgames), compute the XOR 6 // of Grundy numbers to decide the winner under normal play. 7 8 // mex helper: returns smallest nonnegative integer not in the set 9 unsigned int mex(const vector<unsigned int>& vals) { 10 // Using a boolean array sized to vals.size()+1 is enough 11 vector<char> seen(vals.size() + 2, 0); 12 for (auto v : vals) if (v < seen.size()) seen[v] = 1; 13 for (unsigned int g = 0; g < seen.size(); ++g) if (!seen[g]) return g; 14 return (unsigned int)seen.size(); 15 } 16 17 int main() { 18 ios::sync_with_stdio(false); 19 cin.tie(nullptr); 20 21 int n, m; // nodes, edges 22 if (!(cin >> n >> m)) return 0; 23 vector<vector<int>> adj(n); 24 for (int i = 0; i < m; ++i) { 25 int u, v; cin >> u >> v; // edge u -> v, 0-based 26 adj[u].push_back(v); 27 } 28 29 int k; cin >> k; 30 vector<int> start(k); 31 for (int i = 0; i < k; ++i) cin >> start[i]; 32 33 // Compute Grundy numbers with DFS + memoization (DAG assumed) 34 vector<int> state(n, 0); // 0=unvis,1=visiting,2=done 35 vector<unsigned int> g(n, 0); 36 37 function<unsigned int(int)> dfs = [&](int u) -> unsigned int { 38 if (state[u] == 2) return g[u]; 39 if (state[u] == 1) { 40 // In practice, handle cycles carefully; here we assume DAG. 41 // Fallback: return 0 if unexpected cycle. 42 return 0; 43 } 44 state[u] = 1; 45 vector<unsigned int> nxt; 46 nxt.reserve(adj[u].size()); 47 for (int v : adj[u]) nxt.push_back(dfs(v)); 48 unsigned int gu = mex(nxt); 49 g[u] = gu; 50 state[u] = 2; 51 return gu; 52 }; 53 54 unsigned int X = 0; 55 for (int s : start) X ^= dfs(s); 56 57 cout << (X ? "First" : "Second") << '\n'; 58 return 0; 59 } 60
This program demonstrates the Sprague–Grundy reduction: each node’s Grundy number is mex of its successors’ Grundy numbers. The XOR across independent starting positions decides the winner, generalizing Nim’s nim-sum. It assumes the move graph is acyclic; for cycles, more advanced handling is needed.