Hackenbush is played on a graph attached to the ground. A move deletes one edge; everything no longer connected to the ground falls off. The player who cannot move loses.
- Green edges (either player may cut): impartial, solved by Grundy numbers.
- Blue / Red edges (only Left cuts blue, only Right cuts red): partizan, solved by game values (surreal numbers).
Green Hackenbush
Colon principle. A branch hanging at a vertex may be replaced by a single stalk whose length is the branch’s Grundy value. For a tree rooted at the ground:
Fusion principle (Winning Ways, ch. 7). All vertices of a cycle may be fused into one vertex, each cycle edge becoming a self-loop; a loop is worth a stalk of length 1, so an odd cycle reduces to a single edge and an even cycle vanishes. Together the principles reduce any green graph to a tree: contract every 2-edge-connected component (bridges stay as tree edges; the ground is one vertex), a component with internal edges contributes at its vertex.
int greenTree(int v, int p, const vector<vector<int>>& adj) { // rooted tree; use iteratively for deep trees
int res = 0;
for (int c : adj[v]) if (c != p) res ^= greenTree(c, v, adj) + 1;
return res;
}
// Any green graph: vertices 0..n-1, vertex 0 = the ground (merge all ground vertices into it first). O(V + E).
int greenGraph(int n, const vector<pair<int, int>>& edges) {
int m = sz(edges); vector<vector<pair<int, int>>> adj(n);
for (int i = 0; i < m; i++) { auto [u, v] = edges[i]; adj[u].push_back({v, i}); adj[v].push_back({u, i}); }
vector<int> tin(n, -1), low(n), comp(n, -1); vector<char> bridge(m, 0); int timer = 0;
auto dfs = [&](auto&& self, int v, int pe) -> void { // bridges (loops and multi-edges are never bridges)
tin[v] = low[v] = timer++;
for (auto [u, id] : adj[v]) {
if (id == pe) continue;
if (tin[u] == -1) { self(self, u, id); low[v] = min(low[v], low[u]); if (low[u] > tin[v]) bridge[id] = 1; }
else low[v] = min(low[v], tin[u]);
}
};
dfs(dfs, 0, -1); // parts not connected to the ground never matter
int C = 0;
for (int s = 0; s < n; s++) if (tin[s] != -1 && comp[s] == -1) { // 2-edge-connected components
vector<int> st{s}; comp[s] = C;
while (!st.empty()) { int v = st.back(); st.pop_back(); for (auto [u, id] : adj[v]) if (!bridge[id] && comp[u] == -1) comp[u] = C, st.push_back(u); }
C++;
}
vector<int> loops(C, 0); vector<vector<int>> tree(C);
for (int i = 0; i < m; i++) {
auto [u, v] = edges[i]; if (tin[u] == -1) continue;
if (bridge[i]) tree[comp[u]].push_back(comp[v]), tree[comp[v]].push_back(comp[u]);
else loops[comp[u]] ^= 1;
}
auto val = [&](auto&& self, int c, int p) -> int {
int r = loops[c];
for (int d : tree[c]) if (d != p) r ^= self(self, d, c) + 1;
return r;
};
return val(val, comp[0], -1);
}Proof: stalks, the colon principle and the tree formula
Stalks. Cutting edge of a stalk of edges leaves a stalk of edges, so the options have values by induction and the stalk has value : it is the Nim heap .
Colon principle. Let denote an edge from the attachment point with hanging on its top end. If (equal Grundy values), then : in the second player answers a cut of either by cutting the other (leaving ), and answers moves inside or with the second-player strategy of ; the first player eventually runs out of moves. Branches at a vertex are disjoint games (cutting in one never disconnects another), so the vertex’s value is the XOR of its branch values.
Trees. By induction the subtree hanging at child is equivalent to a stalk of length ; with the edge to underneath it becomes a stalk of length , worth ; XOR over the children. The fusion principle is quoted from Winning Ways (its proof is a longer strategy argument);
greenGraphwas checked against exhaustive search over all edge subsets on random graphs with up to 9 edges, including loops, parallel edges and pieces not attached to the ground.
Partizan games and game values
With coloured edges Grundy numbers no longer apply. Each position has a value, a measure of how many free moves one player has in reserve; values of sums add.
| Position (edges listed from the ground up) | Value |
|---|---|
| nothing | (second player wins) |
| one blue edge | (Left is one move ahead) |
| one red edge | |
| blue, then red on top | |
| red, then blue on top | |
| blue, red, red | |
| a single green edge | (a Nim heap of size 1, not a number) |
| stalk in general (Berlekamp’s rule) | for the first run of edges of one colour, then for the following edges (blue , red ) |
Outcome from the value: : Left wins whoever starts; : Right wins; : the second player wins; (fuzzy, like ): the first player wins. Blue-Red Hackenbush positions always have number values.
Conway’s construction. A game is a pair of sets of simpler games: , , , , . This single definition generates all integers and dyadic rationals (finite birthdays), all real numbers, infinite and infinitesimal numbers (, , ), and non-numbers like , , that are incomparable with 0.
Simplicity rule. If every left option is a number less than every right option, is the simplest number strictly between and : the integer of least absolute value if one fits, otherwise the dyadic rational with the smallest denominator (unique). This is the same question as the Stern–Brocot “simplest fraction in an interval”, restricted to dyadics.
| Symbol | Definition | Meaning |
|---|---|---|
| second player wins | ||
| first player wins (Nim heap of size 1) | ||
| Nim heap of size | impartial games always have values : Sprague–Grundy in this language | |
| positive but smaller than every positive number | ||
| its negative | ||
| a “switch”: whoever moves first takes it (hot game) |
Typical number-valued games: Blue-Red Hackenbush, Cutcake, and most Toads and Frogs positions (toads facing right, frogs facing left, on a strip of squares; each hops or slides one square in its facing direction, or jumps over exactly one opposing piece into an empty square) — though Toads and Frogs is also the standard example of a partizan game whose value is not always a number: once toads and frogs are adjacent and can jump each other, some positions evaluate to switches () or other non-numbers, which is exactly why it is the usual teaching example for “numbers aren’t everything.”
Beyond numbers: temperature and atomic weight (cited, not derived). For games whose value is a genuinely hot position (, i.e. not a number), Winning Ways’ thermography assigns a temperature — informally, the amount both players are willing to “pay” to move — via a cooling operator applied until the game becomes stable (a number plus at most an infinitesimal); the mast value at that point is the game’s mean value, and optimal play in a sum of hot games follows the thermostatic strategy: play in the component with the highest temperature first. Separately, for infinitesimally-close-to-zero games (like many all-small Hackenbush/Nim-like positions), the atomic weight (a generalization of the ordering used in the table above) orders infinitesimals well enough to decide long sums of them. Both theories are genuinely deep (full chapters of Winning Ways vol. 1 and On Numbers and Games) and almost never needed directly in a contest, where positions are normally arranged to be clean impartial sums or exact Blue-Red Hackenbush — but the vocabulary is worth recognising if a problem’s flavour text is quoting CGT literature.
struct Dyadic { // value = num / 2^k, normalized: k == 0 or num odd
ll num = 0; int k = 0;
Dyadic(ll n = 0, int kk = 0) : num(n), k(kk) { while (k > 0 && num % 2 == 0) num /= 2, k--; }
ll floorAt(int level) const { return level >= k ? num * (1LL << (level - k)) : num >> (k - level); } // floor(v * 2^level)
Dyadic operator-() const { return Dyadic(-num, k); }
friend Dyadic operator+(const Dyadic& a, const Dyadic& b) {
int k = max(a.k, b.k);
return Dyadic(a.num * (1LL << (k - a.k)) + b.num * (1LL << (k - b.k)), k);
}
friend Dyadic operator-(const Dyadic& a, const Dyadic& b) { return a + (-b); }
friend strong_ordering operator<=>(const Dyadic& a, const Dyadic& b) {
int k = max(a.k, b.k);
return (i128)a.num * ((i128)1 << (k - a.k)) <=> (i128)b.num * ((i128)1 << (k - b.k));
}
friend bool operator==(const Dyadic& a, const Dyadic& b) { return a.num == b.num && a.k == b.k; }
};
Dyadic simplestBetween(optional<Dyadic> lo, optional<Dyadic> hi) { // nullopt = unbounded; needs lo < hi
bool below = !lo || *lo < Dyadic(0), above = !hi || *hi > Dyadic(0);
if (below && above) return Dyadic(0);
if (!below) { Dyadic c(lo->floorAt(0) + 1); if (!hi || c < *hi) return c; } // lo >= 0: smallest integer > lo
else { Dyadic c(-((-*hi).floorAt(0) + 1)); if (!lo || c > *lo) return c; } // hi <= 0: largest integer < hi
for (int level = 1;; level++) { Dyadic c(lo->floorAt(level) + 1, level); if (c < *hi) return c; }
}
optional<Dyadic> numberGame(const vector<Dyadic>& L, const vector<Dyadic>& R) { // value of {L | R} if it is a number
optional<Dyadic> lo, hi;
for (auto& x : L) if (!lo || x > *lo) lo = x;
for (auto& x : R) if (!hi || x < *hi) hi = x;
if (lo && hi && !(*lo < *hi)) return nullopt; // max L >= min R: a hot game, not a number
return simplestBetween(lo, hi);
}
Dyadic stalkValue(const string& s) { // Blue-Red stalk from the ground up, 'B' / 'R' (Berlekamp's rule)
Dyadic v(0); int i = 0, n = sz(s);
while (i < n && s[i] == s[0]) v = v + Dyadic(s[0] == 'B' ? 1 : -1), i++;
for (int j = 1; i < n; i++, j++) v = v + Dyadic(s[i] == 'B' ? 1 : -1, j);
return v;
}Complexity: arithmetic (values must fit ), per simplestBetween.
Proof: simplestBetween, and Berlekamp's rule for stalks
Simplest number. If lies strictly inside, it is the simplest (birthday 0). If the interval is in the positives, integers are simpler than non-integers and smaller positive integers are simpler, so the answer is the least integer if it is . Otherwise no integer lies inside, and among the dyadics in the interval the one with the smallest denominator is unique: two such numbers would have a number of denominator between them. The loop tries and takes the least multiple of greater than ; the first for which it is below is the answer. Negative intervals are symmetric. Checked against an enumeration of all dyadics by birthday.
Stalks. Induction on the length; write for the claimed value of the bottom edges. A monochromatic stalk of blue edges is (red symmetric). Otherwise the options of the full stalk are the over cut edges (Left’s for blue edges, Right’s for red), which are numbers by induction. For a tail edge of weight all later weights sum to less than in absolute value, so cutting a blue edge gives a value below and cutting a red edge a value above ; run edges satisfy the same (the run’s partial sums are integers at distance inside, while the tail moves by less than 1). Say the top edge is blue with weight : Left’s best option is ; the topmost red edge is followed only by blue edges with weights , which sum to , so Right’s best option is ; all other options are farther away by the same tail inequality. Hence the stalk is , and , an odd multiple of , is the only number of denominator at most in that open interval: by the simplicity rule the value is . A red top edge is symmetric.
stalkValuewas checked against the recursive definition (options evaluated withnumberGame) for all stalks of up to 12 edges, and the table entries are such stalks.
Why this matters for contests
Directly, rarely. Indirectly, three transferable ideas: decomposition (a game splitting into independent parts is solved per part and combined: XOR for impartial games, addition for partizan values; recognising independence is the key skill), value beyond win/lose (when the question is “who wins and by how much”, compute a number: the advantage DP is the practical version), and fusion / reduction (collapsing cycles and branches into one equivalent value, the same instinct as condensation, bridge trees and virtual trees).
Related notes
- Sprague–Grundy
- Bridges and Biconnectivity
- Big Integers and Fractions (Stern–Brocot)