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);
}

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.

SymbolDefinitionMeaning
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.

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).