← All Posts
CP Math Series · Game Theory · Part 1

Combinatorial Games

Game theory in competitive programming is one of the most elegant intersections of mathematics and algorithms. At its core lies the theory of combinatorial games — deterministic two-player games with perfect information where every position is either winning or losing. This post develops the rigorous foundations you'll need for Sprague-Grundy theory, Nim, and the entire game-theoretic toolkit used across Codeforces and ICPC.

What Are Combinatorial Games

Definition — Combinatorial Game

A combinatorial game is a two-player game satisfying all of the following axioms:

  1. Two players who alternate turns. We call them Left and Right (or Alice and Bob).
  2. Perfect information — both players see the complete game state at all times. There are no hidden cards, fog-of-war, or secrets.
  3. No chance — no dice, coin flips, or randomness of any kind. The outcome is determined entirely by the players' choices.
  4. Finiteness — the game must terminate in finitely many moves. Every play sequence reaches a terminal position.
  5. Normal play convention — the player who cannot move loses. (The misère convention reverses this.)

Formally, a combinatorial game is a pair $G = (X, F)$ where $X$ is the set of all positions and $F : X \to \mathcal{P}(X)$ is the move function mapping each position to its set of reachable successors. A position $x$ is terminal if $F(x) = \emptyset$.

The game graph $(X, E)$ has edges $E = \{(x, y) : y \in F(x)\}$. The finiteness axiom guarantees that this graph is a directed acyclic graph (DAG) — there are no cycles.

Example — The Subtraction Game

Consider a pile of $n$ stones. Two players alternate turns, and on each turn a player must remove exactly $1$, $2$, or $3$ stones. The player who takes the last stone wins (normal play).

Here $X = \{0, 1, 2, \ldots, n\}$, and $F(k) = \{k-1, k-2, k-3\} \cap X$. The terminal position is $0$ (no stones to remove), and the player who faces $0$ loses.

Other classic examples include Nim (multiple piles, remove any number from one pile), Chomp (breaking a chocolate bar), and Wythoff's game. Notice that chess, checkers, and Go are also combinatorial games, although their state spaces are far too large for complete analysis.

Impartial vs Partizan Games

The most critical classification in combinatorial game theory divides all games into two families.

Definition — Impartial Game

A combinatorial game is impartial if at every position, both players have the same set of legal moves. Formally, $F_L(x) = F_R(x)$ for all $x \in X$, where $F_L$ and $F_R$ are the move functions for Left and Right respectively.

Definition — Partizan Game

A combinatorial game is partizan if there exists at least one position where the available moves differ between the two players: $\exists\, x \in X$ such that $F_L(x) \neq F_R(x)$.

The distinction determines which mathematical tools we can apply:

Property Impartial Partizan
Move setsIdentical for bothMay differ
Core theorySprague-GrundySurreal numbers
ValuesNon-negative integersSurreal numbers
CP frequencyVery commonRare
ExamplesNim, KaylesChess, Go

Why the distinction matters in CP: Nearly every game theory problem on competitive programming platforms involves impartial games. This is because Sprague-Grundy theory gives us a complete, algorithmic solution: reduce the game to Grundy values and XOR them. Partizan games require far more sophisticated analysis and appear only in advanced research problems.

Example — Classifying Games

Nim is impartial: both players choose a pile and remove stones, with no restriction on who may act on which pile. Chess is partizan: White can only move white pieces, and Black can only move black pieces — the move sets are fundamentally different.

For the remainder of this series, unless stated otherwise, we focus on impartial games under normal play convention.

N-Positions and P-Positions

The central concept in combinatorial game analysis is the classification of every position as either a winning position for the Next player or a winning position for the Previous player. This binary classification completely determines optimal play.

Definition — P-position and N-position

Let $G = (X, F)$ be a combinatorial game. A position $x \in X$ is:

  • A P-position (Previous player wins) if the player who just moved — equivalently, the player not about to move — wins under optimal play from both sides.
  • An N-position (Next player wins) if the player about to move wins under optimal play.

The labeling is determined recursively from the terminal positions:

Labeling Rules

  1. Terminal positions are P-positions. The player to move has no moves and loses (normal play).
  2. A position is an N-position if it has at least one successor that is a P-position. The next player can move to a P-position, forcing the opponent into a losing state.
  3. A position is a P-position if all of its successors are N-positions. No matter what the next player does, the opponent ends up in a winning state.

Equivalently, using set notation:

$$\text{type}(x) = \begin{cases} P & \text{if } F(x) = \emptyset \text{ (terminal)}\\ N & \text{if } \exists\, y \in F(x) : \text{type}(y) = P \\ P & \text{if } \forall\, y \in F(x) : \text{type}(y) = N \end{cases}$$

The intuition is beautifully simple: from an N-position, the next player can "escape" to a P-position, passing the burden to the opponent. From a P-position, every move leads to an N-position — you're trapped.

Interactive: Game Tree Labeling

Below is the game tree for the subtraction game with pile sizes $0$ through $7$ and subtraction set $S = \{1, 2, 3\}$. Click Step to watch the labeling algorithm propagate from terminal positions upward. Notice how position $0$ is P (terminal loss), positions $1, 2, 3$ are N (can reach $0$), position $4$ is P (all moves go to N), and the pattern repeats with period $4$.

Unlabeled N-position P-position Positions Successors 0 P 1 N 2 N 3 N 4 P 5 N 6 N 7 N Subtraction set S = {1, 2, 3} · Edges show legal moves (remove s stones)
Click Step to begin labeling from terminal positions.

After the full labeling, the pattern emerges: $\text{type}(n) = P$ iff $n \equiv 0 \pmod{4}$. This periodicity is no coincidence — it follows from the structure of the subtraction set and is a special case of the Sprague-Grundy theorem (covered in the next post).

The Fundamental Theorem

We now prove that the labeling algorithm is well-defined and exhaustive: every position in a finite combinatorial game is exactly one of N or P.

Theorem (Fundamental Classification)

Let $G = (X, F)$ be a finite combinatorial game (DAG). Then every position $x \in X$ is either an N-position or a P-position (and not both). Moreover:

  • $x$ is a P-position $\iff$ every successor of $x$ is an N-position (including the case $F(x) = \emptyset$).
  • $x$ is an N-position $\iff$ some successor of $x$ is a P-position.

Proof (by strong induction on position depth)

Define the depth of a position $x$ as the length of the longest path from $x$ to any terminal position. Since the game graph is a finite DAG, every position has a well-defined finite depth.

Base case ($\text{depth}(x) = 0$): Position $x$ is terminal, so $F(x) = \emptyset$. The player to move loses. Hence $x$ is a P-position. The condition "every successor is an N-position" holds vacuously.

Inductive step: Assume the claim holds for all positions of depth $< d$. Consider position $x$ with $\text{depth}(x) = d$. Every successor $y \in F(x)$ has $\text{depth}(y) < d$, so by the induction hypothesis, each $y$ is classified as either P or N. Now exactly one of two cases holds:

  1. Case 1: $\exists\, y \in F(x)$ with $\text{type}(y) = P$. Then the player to move at $x$ can move to $y$, placing the opponent in a P-position (a losing position for the player to move). Hence $x$ is an N-position.
  2. Case 2: $\forall\, y \in F(x)$, $\text{type}(y) = N$. Then every move from $x$ leads to a position where the opponent (now the next player) has a winning strategy. The player to move at $x$ is stuck — no matter what they choose, the opponent wins. Hence $x$ is a P-position.

These two cases are mutually exclusive and exhaustive, so $x$ is classified as exactly one of N or P. $\blacksquare$

The proof gives us more than just existence — it provides a constructive algorithm. Process positions in topological order (terminal nodes first), applying the three rules. This runs in $O(|X| + |E|)$ time, where $|E|$ is the number of edges in the game graph.

Corollary — Optimal Strategy Existence

In any finite combinatorial game under normal play, exactly one of the two players has a winning strategy, determined entirely by the initial position's classification. If the start is an N-position, the first player wins by always moving to a P-position successor. If the start is a P-position, the second player wins regardless of the first player's choices.

Computing Positions

We now translate the theory into practical C++ code. There are two primary approaches: backward induction (topological sort) and recursive memoization.

Approach 1: Backward Induction via Topological Sort

Process positions from terminal nodes upward using BFS. This is the most efficient approach and mirrors the proof of the fundamental theorem.

#include <bits/stdc++.h>
using namespace std;

vector<char> computePositions(int n, const vector<int>& S) {
    vector<vector<int>> radj(n + 1);
    vector<int> outdeg(n + 1, 0);
    for (int i = 0; i <= n; i++)
        for (int s : S)
            if (i - s >= 0) { radj[i - s].push_back(i); outdeg[i]++; }

    vector<char> pos(n + 1, '?');
    queue<int> q;
    for (int i = 0; i <= n; i++)
        if (outdeg[i] == 0) { pos[i] = 'P'; q.push(i); }

    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : radj[u]) {
            if (pos[v] != '?') continue;
            if (pos[u] == 'P') { pos[v] = 'N'; q.push(v); }
            else if (--outdeg[v] == 0) { pos[v] = 'P'; q.push(v); }
        }
    }
    return pos;
}

int main() {
    int n, k; cin >> n >> k;
    vector<int> S(k);
    for (int& s : S) cin >> s;
    auto pos = computePositions(n, S);
    for (int i = 0; i <= n; i++)
        cout << i << ": " << pos[i] << "\n";
}

Time: $O(n \cdot |S|)$  |  Space: $O(n \cdot |S|)$

Approach 2: Recursive Memoization (DFS)

For implicit game graphs (defined by rules rather than adjacency lists), DFS with memoization is cleaner:

#include <bits/stdc++.h>
using namespace std;
unordered_map<int, bool> memo;
vector<int> S;

bool isNPosition(int n) {
    if (n == 0) return false;
    auto it = memo.find(n);
    if (it != memo.end()) return it->second;
    bool result = false;
    for (int s : S) {
        if (n - s >= 0 && !isNPosition(n - s)) { result = true; break; }
    }
    return memo[n] = result;
}

int main() {
    int n, k; cin >> n >> k;
    S.resize(k);
    for (int& s : S) cin >> s;
    cout << (isNPosition(n) ? "First" : "Second") << "\n";
}

Time: $O(n \cdot |S|)$  |  Space: $O(n)$

Key insight for CP: The BFS approach is preferred when you need to classify all positions (answer multiple queries). The DFS approach is better for a single starting position in a large state space. For general DAGs, the same BFS logic applies with $O(V + E)$ time.

Classic Examples

Example 1: Subtraction Game Analysis

Consider the subtraction game with $S = \{1, 3, 4\}$. Let's compute the first few positions:

$n$ 01234567891011
Type PNPNNNNPNPNN

Verification for $n = 2$: $F(2) = \{1\}$. Since type(1) = N, all successors are N → type(2) = P. For $n = 7$: $F(7) = \{6, 4, 3\}$, all N-positions → type(7) = P. The pattern $P, N, P, N, N, N, N$ repeats with period 7.

Example 2: Simple Nim (Single Pile)

Nim with a single pile of $n$ stones (remove $\geq 1$ stones): $n = 0$ is a P-position (no moves); $n \geq 1$ is an N-position (take all). The real power emerges with multiple piles — the Sprague-Grundy theorem shows $(n_1, n_2, \ldots, n_k)$ is a P-position iff $n_1 \oplus n_2 \oplus \cdots \oplus n_k = 0$.

Practice Problems

Apply the concepts from this post to these problems, roughly ordered by difficulty:

  1. CF 1537D — Deleting Divisors — Direct N/P position analysis.
  2. CF 1194D — 1-2-3 — Subtraction game with multiple piles.
  3. CF 859C — Pie Rules — Game analysis with backward induction.
  4. CF 1451D — Circle Game — 2D position analysis with N/P labeling.

What's next: In Part 2, we cover the Sprague-Grundy theorem — Grundy values, XOR decomposition for game sums, and multi-component game analysis.