∑MathAdvanced

Game Theory - Advanced Games

Key Points

  • •
    Sprague–Grundy (SG) theory solves impartial, normal-play, terminating games by assigning each position a nonnegative integer called its Grundy value.
  • •
    SG fails when the game can draw (infinite play/cycles) or when the game is partizan (players have different legal moves).
  • •
    Draws can be handled algorithmically with retrograde analysis on the state graph, labeling states as Win/Lose/Draw via a BFS-like propagation.
  • •
    Staircase Nim reduces to ordinary Nim: the XOR of stones on odd-indexed stairs (1-based) alone determines the outcome.
  • •
    Wythoff’s Game P-positions are given by Beatty sequences built from the golden ratio; moving to the closest P-position is optimal.
  • •
    Green Hackenbush (all-green edges) is impartial and reduces to Nim; Blue–Red Hackenbush is partizan and requires different theory.
  • •
    Use SG values and nim-sum when you have a disjoint sum of impartial subgames on a DAG; do not use SG for partizan or draw-possible games.
  • •
    In contests, model the game as a directed graph: if acyclic and impartial use SG; if cyclic use Win/Lose/Draw retrograde; if partizan use retrograde on (position, player).

Prerequisites

  • →Directed graphs and BFS/DFS — Game positions and moves are naturally modeled as nodes and directed edges; retrograde analysis relies on traversals.
  • →Bitwise XOR — Nim-sum combines Grundy numbers across independent subgames and determines outcomes.
  • →Topological sorting — Computing Sprague–Grundy numbers on a DAG is simplest in reverse topological order.
  • →Number theory: Beatty sequences and golden ratio — Wythoff’s Game P-positions are characterized by golden-ratio Beatty sequences.
  • →Recurrence and memoization — Grundy computation is a recursive mex over options; memoization avoids recomputation.
  • →Game outcome classes (P/N/Draw) — Understanding how wins, losses, and draws propagate is key to modeling and solving games.

Detailed Explanation

Tap terms for definitions

01Overview

Advanced combinatorial game theory extends classic Nim-style reasoning to richer settings: games can have three outcomes (win, lose, draw), and players may have different legal moves (partizan games). The Sprague–Grundy theorem gives a powerful way to solve impartial, normal-play, terminating games by mapping every position to a nonnegative integer (its Grundy value), letting us combine independent components via XOR (nim-sum). However, the theorem’s assumptions are strict: if play can continue forever (draws from cycles) or if the two players’ move sets differ (partizan), the SG framework no longer applies directly. In programming contests, many problems can be turned into graph problems: positions are nodes, legal moves are directed edges, and the current player is either implicit (impartial) or explicit (partizan). For impartial DAGs we compute Grundy numbers; for general graphs (possibly cyclic) we compute Win/Lose/Draw via retrograde propagation; for partizan games we do retrograde on doubled states (position, player). Classic examples include Staircase Nim (reduces to Nim via an odd-level invariant), Wythoff’s Game (golden-ratio Beatty P-positions), and Hackenbush (green version reduces to Nim; colored versions are partizan). Mastering when SG applies—and having alternative tools when it doesn’t—is the key to solving 2000–2400 CF-level game problems.

02Intuition & Analogies

Think of game positions as rooms in a maze. A directed corridor from room A to room B means “you can move from A to B”. If every walk eventually leaves the maze (no cycles), you can assign each room a simple label: how many nim-heaps does this room feel like? That label is the Grundy number. When you play several mazes at once (independent subgames), the effect is like taking multiple Nim heaps and XORing their sizes—the overall challenge of the maze you face is just the XOR of those labels. But what if the maze has loops? You could keep walking forever without leaving, so there may be no forced win or loss—this is a draw. In that case, Grundy numbers lose their meaning because they rely on always reaching a dead end (a terminal room). Instead, you label rooms as Win/Lose/Draw using push–pull logic: if you can step to a Lose for the opponent, you’re Win; if all ways out go to Win for the opponent, you’re Lose; otherwise you’re Draw. Now imagine the maze gives different doors to the two players—partizan games. One player might be allowed through certain doors the other can’t use. The SG trick relies on both players having the same doors from a room, so it breaks. You can still solve many finite partizan mazes by tracking whose turn it is and using the same Win/Lose/Draw propagation over a doubled state space (room, player). Concrete cases help: in Staircase Nim, only stones on odd steps really matter; in Wythoff’s, the ‘cold’ positions line up along two magical golden-ratio rays; in Green Hackenbush, complicated vine shapes secretly behave like plain Nim heaps.

03Formal Definition

A combinatorial game under normal play is an ordered pair of sets (L, R) for each position, where L are moves available to Left and R are moves available to Right; play alternates moves and the player unable to move loses. An impartial game is one where at every position, i.e., both players share the same move options. A position is terminal if = . The Sprague–Grundy function g assigns to each position x of a finite impartial normal-play game a nonnegative integer defined recursively by g(x) = \{ g(y) : y (x) \}, where mex is the least nonnegative integer not in the set. The SG theorem states that any finite impartial normal-play game is equivalent to a Nim heap of size g(x), and that the Grundy value of the disjunctive sum of independent games is the bitwise XOR (nim-sum) of their Grundy values. A P-position (Previous player wins, i.e., next player loses under optimal play) in an impartial game is any position with Grundy value 0; N-positions have nonzero Grundy. If the game graph has directed cycles allowing infinite play, we admit a third outcome: draw. In such graphs, the Win/Lose/Draw classification can be defined using attractor sets on the product graph of positions and players. Partizan games violate impartiality; while they still admit outcome classes and deeper algebra (surreal/Conway values), SG values and nim-sums generally do not apply.

04When to Use

Use Sprague–Grundy when the game is impartial, normal-play, and guaranteed to terminate—most commonly when the state graph is a directed acyclic graph (DAG) or when repeated states are impossible by a decreasing measure. Decompose the game into a disjoint sum of independent components, compute each component’s Grundy value via mex of its options, and XOR them to decide the winner and find a winning move. Apply this to problems like Green Hackenbush on rooted trees, Staircase Nim, or token-removal games on DAGs. When the game can draw (cycles), switch to Win/Lose/Draw retrograde analysis on the directed state graph: mark terminals as Lose, propagate Wins to predecessors that can move into Lose, mark Lose where all moves go to Win, and leave the rest as Draw. When moves differ by player (partizan), explicitly include the player in the state (position, player) and run the same retrograde process; this works regardless of cycles and immediately gives WL/Draw outcomes. Use Wythoff’s Game formulas when you recognize the three-move pattern (remove from one pile or both equally): P-positions fall on golden-ratio Beatty pairs, letting you jump directly to optimal targets in O(1). Avoid SG in misère play (last move loses) except for the special Nim case, and avoid SG in general partizan settings like Blue–Red Hackenbush where surreal numbers, not XOR, govern sums.

⚠️Common Mistakes

• Applying SG to games with cycles or potential infinite play. SG requires termination; if cycles exist, Grundy values can be undefined or misleading. Use Win/Lose/Draw retrograde instead. • Ignoring impartiality. If the two players have different moves from some positions (partizan), SG and nim-sum are invalid. Model states as (position, player) and compute WL/Draw, or use partizan theory if needed. • Forgetting the disjoint-sum requirement. XOR only combines independent subgames. If a move in one component changes the legal moves in another, they are not independent and XOR will lie. • Misère confusion. Under misère rules, Grundy addition fails in general; only Nim has a simple fix (if all heaps are size 1, parity decides; otherwise play like normal Nim). • Off-by-one indexing in Staircase Nim. The invariant uses 1-based odd indices; switching to 0-based without adjusting breaks the XOR invariant. • Wythoff precision errors. Floating-point computation of φ can misclassify border cases; use integer/long double carefully or purely integer formulas to avoid rounding mistakes. • Treating Green Hackenbush as partizan. All-green edges yield an impartial game reducible to Nim; color asymmetry (blue/red) makes it partizan. • Assuming DAG when it isn’t. Many grid-walk games look acyclic but allow revisits; add a decreasing potential or detect cycles before committing to SG.

Key Formulas

Grundy Recurrence

Explanation: The Grundy value of a position is the smallest nonnegative integer not taken by any option’s Grundy values. This lets us reduce positions to equivalent Nim heaps.

Nim-sum of Subgames

Explanation: The Grundy of a disjoint sum of impartial games equals the bitwise XOR of their individual Grundy values. If the XOR is 0, the position is losing (P-position).

Wythoff P-positions

Explanation: In Wythoff’s Game, the cold (P) positions are exactly the Beatty pairs formed by the golden ratio and its square. Moving to the nearest such pair is optimal.

Wythoff Difference Property

Explanation: The difference between heaps in a P-position equals the index k. Given a state (a,b) with a b, target and move to ( k , k + k).

Graph Retrograde Complexity

Explanation: Processing a game graph with n states and m directed edges using BFS-style retrograde takes linear time in the size of the graph. Each edge and node is visited a constant number of times.

Staircase Nim Invariant

Explanation: With 1-based indexing, the XOR of stones on odd steps determines the outcome. A zero XOR is losing; a nonzero XOR is winning by adjusting one odd pile.

mex Definition

Explanation: The mex of a set S of nonnegative integers is the smallest nonnegative integer not in S. It powers the Grundy recurrence.

Typical Costs

Explanation: Computing mex over a sorted vector can be O(n)–O(n log n); overall SG on a DAG is O(n+m). Wythoff classification is O(1) using arithmetic.

Complexity Analysis

There are three core algorithmic patterns here. (1) Sprague–Grundy on impartial DAGs: Let n be the number of positions and m the number of moves (edges). A topological order or DFS with memoization computes each position’s Grundy number once. For each node you gather its children’s values and compute mex in time proportional to its outdegree; with a boolean seen array sized by the node’s degree or a small dynamic structure, the total is O(n + m) time and O(n + m) space (adjacency plus recursion/stack or order). For disjoint sums, combining k components costs O(k) to XOR their Grundy values. (2) Win/Lose/Draw retrograde on general graphs: We lift to a doubled state space (position, player), so at most 2n states and corresponding directed edges. We maintain the outdegree of each state and a queue. Each state/edge is processed O(1) times, yielding O(n + m) time and O(n + m) space. Unlabeled states after propagation are precisely those in draws (cycles not attracted to a win/lose basin). (3) Closed-form solvers: Staircase Nim outcome reduces to a single XOR over roughly n/2 piles in O(n) time and O(1) extra space. Wythoff’s classification and best-move computation are O(1): compute − a, a* = floor(k b* = a* + k and compare, using 64-bit integers and careful rounding. For Green Hackenbush on trees, an SG DFS is again O(n + m). Across all implementations, memory typically scales with the number of states plus edges; avoid recursion blowup on deep graphs by using iterative topological orders or tail-recursive patterns when stack limits are tight.

Code Examples

Win/Lose/Draw on a General Directed Game Graph (Partizan or Impartial)
1#include <bits/stdc++.h>
2using namespace std;
3
4// We solve Win/Lose/Draw on a game graph that may have cycles and may be partizan.
5// States are (position, player), player in {0,1}. Edges alternate turns.
6// Input format (example):
7// n m0 m1
8// then m0 edges for player 0: u v meaning from (u,0) you can move to (v,1)
9// then m1 edges for player 1: u v meaning from (u,1) you can move to (v,0)
10// Positions are 0..n-1. A state with no outgoing edges is Losing for the player to move.
11// Output: for each position, outcome for player 0 to move and player 1 to move.
12
13int main(){
14 ios::sync_with_stdio(false);
15 cin.tie(nullptr);
16
17 int n; int m0, m1;
18 if(!(cin >> n >> m0 >> m1)){
19 return 0;
20 }
21 // Build edges on doubled graph: id = pos*2 + player
22 int N = 2*n;
23 vector<vector<int>> out(N), in(N);
24 vector<int> outdeg(N, 0);
25
26 auto id = [&](int pos, int pl){ return pos*2 + pl; };
27
28 for(int i=0;i<m0;i++){
29 int u,v; cin>>u>>v;
30 int a = id(u,0), b = id(v,1);
31 out[a].push_back(b);
32 in[b].push_back(a);
33 }
34 for(int i=0;i<m1;i++){
35 int u,v; cin>>u>>v;
36 int a = id(u,1), b = id(v,0);
37 out[a].push_back(b);
38 in[b].push_back(a);
39 }
40 for(int s=0;s<N;s++) outdeg[s] = (int)out[s].size();
41
42 // -1 unknown, 0 = Lose, 1 = Win, 2 = Draw
43 vector<int> res(N, -1);
44 deque<int> dq;
45
46 // Initialize: states with no outgoing moves are Losing for the player to move
47 for(int s=0;s<N;s++) if(outdeg[s]==0){ res[s]=0; dq.push_back(s); }
48
49 // Retrograde propagation
50 while(!dq.empty()){
51 int s = dq.front(); dq.pop_front();
52 for(int p: in[s]){
53 if(res[p] != -1) continue; // already decided
54 if(res[s] == 0){
55 // If you can move to a Losing state for opponent, you are Winning
56 res[p] = 1;
57 dq.push_back(p);
58 } else if(res[s] == 1){
59 // All your moves lead to Winning states for opponent -> you may become Losing
60 if(--outdeg[p] == 0){
61 res[p] = 0;
62 dq.push_back(p);
63 }
64 }
65 // Draws are assigned at the end to undecided states
66 }
67 }
68
69 for(int s=0;s<N;s++) if(res[s]==-1) res[s]=2; // Draw
70
71 // Print results per position for both players
72 auto nameOf = [&](int v){ return v==0?"Lose":(v==1?"Win":"Draw"); };
73
74 for(int pos=0; pos<n; ++pos){
75 cout << "pos " << pos << ": P0-" << nameOf(res[id(pos,0)])
76 << ", P1-" << nameOf(res[id(pos,1)]) << "\n";
77 }
78 return 0;
79}
80

We create a doubled graph over states (position, player). Terminal states (no outgoing edges) are Losing for the player to move. Using a queue, we propagate: any predecessor of a Losing state is Winning; if all successors of a state are Winning, it becomes Losing. Unlabeled states after propagation are Draws (they lie in or are attracted to cycles that avoid decided outcomes). This works for both impartial and partizan rules and for cyclic graphs.

Time: O(n + m0 + m1)Space: O(n + m0 + m1)
Sprague–Grundy on an Impartial DAG and Nim-sum of Components
1#include <bits/stdc++.h>
2using namespace std;
3
4// Compute Grundy numbers on an impartial DAG and the XOR over components.
5// Input: n m, then m edges u->v (same for both players), and a partition of nodes into c components.
6// For demonstration, we read a list of component roots and output their SG XOR.
7
8int main(){
9 ios::sync_with_stdio(false);
10 cin.tie(nullptr);
11
12 int n,m;
13 if(!(cin>>n>>m)) return 0;
14 vector<vector<int>> g(n);
15 vector<int> indeg(n,0);
16 for(int i=0;i<m;i++){
17 int u,v; cin>>u>>v;
18 g[u].push_back(v);
19 indeg[v]++;
20 }
21 // Topological order (Kahn)
22 queue<int> q;
23 for(int i=0;i<n;i++) if(indeg[i]==0) q.push(i);
24 vector<int> topo; topo.reserve(n);
25 while(!q.empty()){
26 int u=q.front(); q.pop(); topo.push_back(u);
27 for(int v: g[u]) if(--indeg[v]==0) q.push(v);
28 }
29 if((int)topo.size()!=n){
30 cerr << "Graph has cycles; SG requires a DAG.\n";
31 return 0;
32 }
33
34 vector<int> grundy(n,0);
35 // Compute SG in reverse topological order so children are ready
36 vector<int> seen; seen.reserve(64);
37 for(int it=n-1; it>=0; --it){
38 int u = topo[it];
39 // gather children's grundy values
40 int mx = 0;
41 for(int v: g[u]) mx = max(mx, grundy[v]);
42 vector<char> used(mx+2, false); // enough to hold mex up to mx+1
43 for(int v: g[u]) used[grundy[v]] = true;
44 int gval = 0; while(gval < (int)used.size() && used[gval]) ++gval;
45 grundy[u] = gval;
46 }
47
48 // Read a set of k starting positions that form independent subgames
49 int k; cin>>k;
50 int xorsum = 0;
51 for(int i=0;i<k;i++){ int s; cin>>s; xorsum ^= grundy[s]; }
52 cout << "xor = " << xorsum << "\n";
53 cout << (xorsum? "First": "Second") << " player wins\n";
54
55 return 0;
56}
57

We ensure the graph is acyclic and compute Grundy values by processing nodes in reverse topological order. The mex of children’s Grundy numbers gives each node’s Grundy. Disjoint starting positions combine via XOR: zero means a losing (P) position; nonzero means winning (N).

Time: O(n + m)Space: O(n + m)
Staircase Nim Solver and Winning Move
1#include <bits/stdc++.h>
2using namespace std;
3
4// Staircase Nim (1-based stairs): you may move any positive number of stones
5// from stair i (i>=2) to stair i-1. Last move wins (normal play).
6// The invariant: XOR over odd-indexed stairs alone determines outcome.
7// We also output a concrete winning move if one exists.
8
9int main(){
10 ios::sync_with_stdio(false);
11 cin.tie(nullptr);
12
13 int n; if(!(cin>>n)) return 0;
14 vector<long long> a(n+1); // 1..n
15 for(int i=1;i<=n;i++) cin>>a[i];
16
17 long long xorsum = 0;
18 for(int i=1;i<=n;i+=2) xorsum ^= a[i];
19
20 if(xorsum==0){
21 cout << "Second\n";
22 return 0;
23 }
24 cout << "First\n";
25
26 // Find a winning move: pick an odd i where decreasing a[i] makes XOR zero.
27 // Let target = a[i] ^ xorsum, with target < a[i]. Move k = a[i]-target stones from i to i-1.
28 for(int i=1;i<=n;i+=2){
29 long long target = a[i] ^ xorsum;
30 if(target < a[i]){
31 long long k = a[i] - target; // move k stones from i to i-1
32 cout << "move " << k << " from stair " << i << " to " << (i-1) << "\n";
33 return 0;
34 }
35 }
36 // Should never reach here if xorsum != 0
37 return 0;
38}
39

For 1-based indexing, only odd stairs contribute to the nim-sum. Moving stones from an odd stair i to i−1 reduces a[i] and changes the XOR; moving from an even stair changes only a[i−1] (odd), which is equivalent to moving on that odd pile in Nim. Therefore the XOR over odd stairs decides the game. The code prints a concrete winning move by reducing some odd a[i] to a[i] XOR xorsum and moving the difference to stair i−1.

Time: O(n)Space: O(1)
Wythoff's Game: P-position Test and Best Move
1#include <bits/stdc++.h>
2using namespace std;
3
4// Wythoff's Game: two heaps (a,b), a<=b; move: remove any positive from one heap,
5// or remove the same positive from both. P-positions are (floor(k*phi), floor(k*phi^2)).
6// We test if (a,b) is P and, if not, output one optimal target P-position.
7
8static inline long long floordiv_long_double(long double x){
9 long double fx = floor(x + 1e-12L); // protect against tiny negative eps
10 return (long long)fx;
11}
12
13int main(){
14 ios::sync_with_stdio(false);
15 cin.tie(nullptr);
16
17 long long a,b; if(!(cin>>a>>b)) return 0;
18 if(a>b) swap(a,b);
19
20 const long double phi = (1.0L + sqrtl(5.0L)) / 2.0L; // golden ratio
21 long long k = b - a;
22 long long ak = floordiv_long_double(k * phi);
23 long long bk = ak + k; // floor(k*phi^2) but equal to ak + k
24
25 bool isP = (a == ak && b == bk);
26 cout << (isP? "Second\n" : "First\n");
27
28 if(!isP){
29 // Move to (ak, bk). We can always achieve this in one legal move.
30 // If a > ak: remove (a - ak) from heap a. If b > bk: remove (b - bk) from heap b.
31 // Otherwise, remove t from both heaps where t = a - ak = b - bk.
32 long long ta = (a > ak ? a - ak : 0);
33 long long tb = (b > bk ? b - bk : 0);
34 if(ta && tb){
35 // Should be equal; remove from both
36 long long t = min(ta, tb);
37 cout << "remove " << t << " from both\n";
38 } else if(ta){
39 cout << "remove " << ta << " from heap A\n";
40 } else if(tb){
41 cout << "remove " << tb << " from heap B\n";
42 } else {
43 // Rare due to rounding — adjust safely by searching nearby k
44 // Fallback: try neighbor kb11 to counter rounding.
45 for(long long dk: {-1LL,1LL}){
46 long long kk = k + dk;
47 if(kk<0) continue;
48 long long a2 = floordiv_long_double(kk * phi);
49 long long b2 = a2 + kk;
50 if(a==a2 && b>=b2){ cout << "remove " << (b-b2) << " from heap B\n"; break; }
51 if(b==b2 && a>=a2){ cout << "remove " << (a-a2) << " from heap A\n"; break; }
52 long long t = a - a2; if(t>0 && b - b2 == t){ cout << "remove " << t << " from both\n"; break; }
53 }
54 }
55 }
56 return 0;
57}
58

We use the golden-ratio characterization: let k = b − a; then (a,b) is a P-position iff a = ⌊kφ⌋ and b = a + k. If not P, moving to (⌊kφ⌋, ⌊kφ⌋ + k) is always legal: it is either a one-heap reduction or a diagonal reduction. The code computes this target and outputs a concrete winning move, with minor guarding against floating-point edge cases.

Time: O(1)Space: O(1)