Monocurl Essays
Series Download

The cycle double cover conjecture, and its proof

Every bridgeless graph has cycles that use each edge exactly twice. The whole proof, from bridges and paint to three-bit labels and a parity count.

70 min·High school and early college·

AssumesBridges of Königsberg: Euler paths and circuitsClock arithmetic: modular arithmetic and why it works

The corners and edges of a cube, pressed flat. The boundaries of its six faces, counting the face around the outside, are six cycles, and every edge has exactly two of them running along it.
Monocurl source
let FCOL = [PLUM, AZURE, P_TEAL, EMBER, LEAF, GOLD]
let Faces = |u| block {
    for (i in range(0, 6)) {
        . Grown(CLOOPS[i], clamp(0, u - i, 1), FCOL[i], 6)
    }
}
mesh edges = Edges(CP, CE)
mesh loops = Faces(u: 0)
mesh nodes = Nodes(CP)

slide "faces"
    play Wait(0.5)
    loops.u = 6
    play Lerp(7.5, [&loops], linear)
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

Here is the graph formed by the corners and edges of a cube, pressed flat onto the page. I want to lay cycles on it, closed routes that never pass through a node twice, so that every edge is used by exactly two of them. For the cube there is an easy way, which is to take the boundary of each face. Pressed flat, the cube shows five faces and has a sixth around the outside, and every edge lies between two faces, so the six boundaries together use every edge exactly twice.

Whether this can be done on every graph was an open question for at least fifty years. One kind of edge plainly prevents it, and the cycle double cover conjecture said that nothing else does. In July 2026 OpenAI published a proof found by one of its models, so the conjecture is now a theorem. The argument is short enough to go through completely, using nothing beyond the arithmetic of odd and even.

The proof has four moves. The first reduces the problem to graphs with three edges at every node. The second trades cycles for paint: every cycle gets its own paint, and a cover becomes a rule that can be checked at one node at a time. The third writes each paint as a code of bits, so that every edge gets a label recording how its two coats differ, and a theorem of Jaeger and Kilpatrick from the 1970s says that labels of the right kind exist on every bridgeless graph. The fourth is the new one. It gets back from the labels to paint by letting each node choose its paints separately, and a count of odd and even shows that the choices can be made to fit together.

Graphs, cycles and double covers

The proof will count edges at nodes, so the definitions have to be exact.

Parallel edges are allowed because one of the simplifications below creates them. The clause to remember is that the nodes of a cycle are all different: a cycle that visits a node arrives by one edge and leaves by another, so it uses exactly two of the edges at that node.

For the cube the six face boundaries have four edges each, which makes $6 \times 4 = 24$ uses of an edge, and the cube has 12 edges, each used twice.

Pause and try

The graph $K_4$ has four nodes $a, b, c, d$ and an edge between every two of them, six edges in all. Find a cycle double cover of $K_4$. Then find a second one with a different number of cycles.

Show answerHide answer

The four triangles $abc$, $abd$, $acd$, $bcd$ work: the edge $ab$ lies on the two triangles that contain both $a$ and $b$, and the same holds for every edge. So do the three cycles of length four, $abcd$, $abdc$ and $acbd$: the cycle $abcd$ misses only the edges $ac$ and $bd$, the cycle $abdc$ misses $ad$ and $bc$, and $acbd$ misses $ab$ and $cd$, so each edge is missed by one of the three and used by the other two. Both lists use an edge $12 = 2 \times 6$ times.

Bridges are the obvious obstacle

  1. This graph has seven edges. Look at the one in the middle.
  2. Remove it and the graph falls into two pieces. An edge like that is called a bridge.
  3. Put it back and send a route across. On the far side it can go round the triangle, but the only way home is the edge it has already used.
A graph made of two triangles joined by one edge. Removing that edge disconnects the graph, and a route that crosses it has no other way back.
Monocurl source
let BP = [[-3.1, 1.3, 0], [-3.1, -1.3, 0], [-1.2, 0, 0], [1.2, 0, 0], [3.1, 1.3, 0], [3.1, -1.3, 0]]
let BE = [[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]]
let Pieces = |g| {
    let Q = map(range(0, 6), |i| BP[i] + [[0 - g, 0, 0], [g, 0, 0]][i >= 3])
    return [Edges(Q, BE), Nodes(Q)]
}
let ROUTE = dense(map([0, 2, 3, 4, 5, 3], |i| BP[i]))
let Walk = |u| [Grown(ROUTE, u, EMBER, 8), Pt(tip_of(ROUTE, u), EMBER, 0.14)]
mesh bridge = Seg(BP[2], BP[3], SLATE, 3)
mesh pieces = Pieces(g: 0)
mesh walk = []
mesh note = []

slide "This graph has seven edges. Look at the one in the middle."
    bridge = Seg(BP[2], BP[3], ROSE, 6)
    play Fade(0.6, [&bridge])

slide "Remove it and the graph falls into two pieces. An edge like that is called a bridge."
    bridge = []
    play Fade(0.5, [&bridge])
    pieces.g = 0.45
    play Lerp(1.2, [&pieces])

slide "Put it back and send a route across. On the far side it can go round the triangle, but the only way home is the edge it has already used."
    pieces.g = 0
    play Lerp(0.8, [&pieces])
    bridge = Seg(BP[2], BP[3], ROSE, 6)
    play Fade(0.4, [&bridge])
    walk = Walk(u: 0)
    play Fade(0.3, [&walk])
    walk.u = 1
    play Lerp(4, [&walk], linear)
    note = [
        center{BP[3]} fill{alpha{0} PAPER} stroke{ROSE, 4} Circle(0.26, 48),
        center{[1.2, -0.75, 0]} Cap("stuck", 0.85, ROSE)
    ]
    play Fade(0.5, [&note])

Uses the shared definitions for this lesson's figures.

The figure suggests that no cycle can use a bridge. That is true, and so is the converse, which we will need more often.

So a graph with a bridge has no cycle double cover: the bridge would have to lie on two cycles and lies on none. The conjecture is that this is the only thing that can go wrong.

This statement is the goal. Everything in the proof will be shown in full except the theorem of Jaeger and Kilpatrick, which is stated exactly and sketched when we reach it.

A graph with no faces to use

The face trick needs a drawing in which no two edges cross, so that there are faces. For a bridgeless graph drawn that way the method always works, after some care in the cases where a face boundary passes through a node twice. The graph below has no such drawing. (I will not prove that, and nothing later depends on it.) It is called the Petersen graph, and ideas about cycles are tested on it first because so many of them fail there.

  1. The Petersen graph has ten nodes and fifteen edges, three at every node. One cycle goes around the outer ring.
  2. A second cycle follows the inner star.
  3. The five spokes are on no cycle yet, and it is not clear what to do next.
A first attempt on the Petersen graph. The outer ring and the inner star are two cycles, and after them every ring and star edge still needs one more cycle and every spoke needs two.
Monocurl source
let PP = petersen([-1.5, -0.14, 0], 1.9, 0.95)
mesh edges = Edges(PP, PE)
mesh spokes = []
mesh loops = []
mesh nodes = Nodes(PP)
mesh tally = [center{[2.35, 1.3, 0]} Cap("cycles so far", 0.78, SLATE)]
let Row = |name, got, y, col| [
    center{[2.0, y, 0]} Cap(name, 0.78, col),
    center{[3.25, y, 0]} Math(str_join([to_string(got), "\text{ of } 2"]), 0.82, col)
]

slide "The Petersen graph has ten nodes and fifteen edges, three at every node. One cycle goes around the outer ring."
    loops = [LoopLine(PP, PE, [0, 1, 2, 3, 4], PLUM)]
    tally .= Row("ring edges", 1, 0.55, PLUM)
    play Fade(0.7, [&loops, &tally])

slide "A second cycle follows the inner star."
    loops .= LoopLine(PP, PE, [5, 7, 9, 6, 8], P_TEAL)
    tally .= Row("star edges", 1, -0.1, P_TEAL)
    play Fade(0.7, [&loops, &tally])

slide "The five spokes are on no cycle yet, and it is not clear what to do next."
    spokes = map(range(5, 10), |k| Seg(PP[PE[k][0]], PP[PE[k][1]], ROSE, 7))
    tally .= Row("spokes", 0, -0.75, ROSE)
    play Fade(0.7, [&spokes, &tally])

Uses the shared definitions for this lesson's figures.

Graphs come in every shape, which makes them hard to reason about. Before attacking the Petersen graph we tidy the problem twice.

Reducing to three edges at every node

The first simplification is a reduction: we replace the problem by a narrower one and show that solving the narrower one solves everything.

The cube, $K_4$ and the Petersen graph are all cubic. The claim is that these are the only graphs we need to handle.

The idea is to change a graph one node at a time until it is cubic, in such a way that a cover of the new graph can be turned back into a cover of the old one. A node of degree 4 or more gets pulled apart.

  1. A node with four edges.
  2. Pull it apart into two nodes, with two of the old edges on each, and join the two with a new edge. Both nodes now have three edges.
  3. Suppose the new graph has a cycle double cover. Near the new edge its cycles run in a pattern like this one, two along every edge.
  4. Shrink the new edge back to nothing. Every cycle is still a closed route, and every old edge still has two cycles along it.
Splitting a node of degree four into two nodes of degree three, and turning a cycle double cover of the new graph back into one of the old graph. Only the part of the graph near the node is drawn.
Monocurl source
let Q = [[-3.0, 1.75, 0], [-3.0, -1.75, 0], [3.0, 1.75, 0], [3.0, -1.75, 0]]
let SplitGraph = |g| block {
    let L = [0 - g, 0, 0]
    let R = [g, 0, 0]
    . [Seg(Q[0], L, SLATE, 3), Seg(Q[1], L, SLATE, 3), Seg(Q[2], R, SLATE, 3), Seg(Q[3], R, SLATE, 3)]
    if (g > 0.02) {
        . Seg(L, R, INK, 4.5)
    }
    . [Pt(L, INK, 0.1), Pt(R, INK, 0.1)]
}
let SplitPaint = |g| {
    let d = 0.11
    let L = [0 - g, 0, 0]
    let R = [g, 0, 0]
    let u0 = ang(Q[0] - L)
    let u1 = ang(Q[1] - L)
    let u2 = ang(Q[2] - R)
    let u3 = ang(Q[3] - R)
    # the end of a ribbon beside the edge from c to q: sgn +1 is the counterclockwise side seen from c
    let s = |q, c, sgn| q + perp(unit(q - c)) * (sgn * d)
    return [
        Path([s(Q[0], L, 1), corner_pt(L, u0, turn(u0, u1), d), s(Q[1], L, -1)], PLUM, 6),
        Path([s(Q[3], R, 1), corner_pt(R, u3, turn(u3, u2), d), s(Q[2], R, -1)], P_TEAL, 6),
        Path([s(Q[0], L, -1), corner_pt(L, 0, turn(0, u0), d), corner_pt(R, u2, turn(u2, PI), d), s(Q[2], R, 1)], EMBER, 6),
        Path([s(Q[1], L, 1), corner_pt(L, u1, turn(u1, 0), d), corner_pt(R, PI, turn(PI, u3), d), s(Q[3], R, -1)], AZURE, 6)
    ]
}
mesh paint = []
mesh graph = SplitGraph(g: 0)

slide "A node with four edges."
    play Wait(0.5)

slide "Pull it apart into two nodes, with two of the old edges on each, and join the two with a new edge. Both nodes now have three edges."
    graph.g = 1
    play Lerp(1.5, [&graph])

slide "Suppose the new graph has a cycle double cover. Near the new edge its cycles run in a pattern like this one, two along every edge."
    paint = SplitPaint(g: 1)
    play Fade(0.8, [&paint])

slide "Shrink the new edge back to nothing. Every cycle is still a closed route, and every old edge still has two cycles along it."
    graph.g = 0
    paint.g = 0
    play Lerp(2.2, [&graph, &paint])

Uses the shared definitions for this lesson's figures.

There is a catch. Pulling a node apart can create a bridge, and then the new graph has no cover to hand back.

Pause and try

The four edges at a node can be sorted into two pairs in three ways. In the graph below, two triangles share the middle node. Which of the three pairings turns the new edge into a bridge?

Show answerHide answer

Only one: the pairing that puts both edges of the left triangle on one new node and both edges of the right triangle on the other. Then the new edge is the only connection between the two triangles. The other two pairings put one edge of each triangle on each new node, and the new edge lies on a cycle that goes out through one triangle and back.

  1. The middle node has four edges. Two belong to the left triangle and two to the right one.
  2. Pair the edges by triangle and pull the node apart. The new edge is a bridge, so the new graph has no cycle double cover.
  3. Go back, and pair one edge from each triangle instead.
  4. Now a cycle runs through the new edge, so it is not a bridge.
Two triangles sharing a node. Pairing the four edges by the triangle they belong to creates a bridge, and pairing one edge from each triangle does not.
Monocurl source
let BW = [[-3.0, 1.3, 0], [-3.0, -1.3, 0], [3.0, 1.3, 0], [3.0, -1.3, 0]]
let A_OF = |h, v| [0 - 0.85 * h, 0.8 * v, 0]
let B_OF = |h, v| [0.85 * h, 0 - 0.8 * v, 0]
let Bow = |h, v| block {
    let A = A_OF(h, v)
    let B = B_OF(h, v)
    let vert = v > h
    # the new node that each old edge ends at, for the edges to the four outer nodes in order
    let ends = [[A, A, B, B], [A, B, A, B]][vert]
    . [Seg(BW[0], BW[1], SLATE, 3), Seg(BW[2], BW[3], SLATE, 3)]
    for (i in range(0, 4)) {
        . Seg(BW[i], ends[i], SLATE, 3)
    }
    if (h + v > 0.03) {
        . Seg(A, B, [ROSE, LEAF][vert], 6)
    }
    . map(BW, |p| Pt(p, INK, 0.09))
    . [Pt(A, INK, 0.1), Pt(B, INK, 0.1)]
}
let P6 = flatten([BW, [A_OF(0, 1), B_OF(0, 1)]])
let E6 = [[0, 1], [2, 3], [0, 4], [2, 4], [1, 5], [3, 5], [4, 5]]
mesh ring = []
mesh bow = Bow(h: 0, v: 0)
mesh word = []

slide "The middle node has four edges. Two belong to the left triangle and two to the right one."
    play Wait(0.5)

slide "Pair the edges by triangle and pull the node apart. The new edge is a bridge, so the new graph has no cycle double cover."
    bow.h = 1
    play Lerp(1.4, [&bow])
    word = [center{[0, 0.55, 0]} Cap("bridge", 0.8, ROSE)]
    play Fade(0.4, [&word])

slide "Go back, and pair one edge from each triangle instead."
    word = []
    play Fade(0.3, [&word])
    bow.h = 0
    play Lerp(1.0, [&bow])
    bow.v = 1
    play Lerp(1.4, [&bow])

slide "Now a cycle runs through the new edge, so it is not a bridge."
    ring = [LoopLine(P6, E6, [4, 0, 1, 5], EMBER)]
    play Fade(0.7, [&ring])

Uses the shared definitions for this lesson's figures.

The proof has to show that a good pairing always exists, and it has to deal with nodes of degree 2 as well.

From here on, every graph is cubic and bridgeless.

Trading cycles for paint

The second simplification changes what we search for. A cycle is awkward to hunt for, because whether a set of edges forms one cannot be checked by looking at one node. So give every cycle of a cover its own paint, and paint the edges it runs along. Two things are then true. Every edge has two coats, in two different paints. And at every node, each paint is on exactly two of the edges there, because its cycle arrives by one edge and leaves by another, or on none, because its cycle misses the node.

Both rules can be checked one edge and one node at a time. The question is whether they are enough.

Pause and try

Suppose someone hands you a painting, and you want cycles. Stand on an edge that has some paint, walk to its end, and look for the same paint on another edge. Why is there exactly one choice? And why can the walk not run into a node it has already left, other than its starting node?

Show answerHide answer

At the node you arrive at, the paint is on exactly two edges, the one you came along and one other, so the way on is forced. If the walk arrived at a node it had already passed through, that node would have the paint on three edges: the two used on the first visit and the one used to arrive now. That is against the rule. Since the graph is finite, the walk must repeat a node eventually, and the only node it can repeat is the one where it started.

Three paints on the cube, each on eight edges. A walker follows the purple paint from edge to edge and returns to its start after four edges. The purple paint that is left over gives a second cycle in the same way.
Monocurl source
let PCOL = [PLUM, PLUM, P_TEAL, GOLD, P_TEAL, GOLD]
let Follow = |u| block {
    . Grown(CLOOPS[0], clamp(0, u, 1), PLUM, 8)
    . Grown(CLOOPS[1], clamp(0, u - 1.15, 1), PLUM, 8)
    if (u <= 1.15) {
        . Pt(tip_of(CLOOPS[0], u), INK, 0.12)
    }
    if (u > 1.15) {
        . Pt(tip_of(CLOOPS[1], u - 1.15), INK, 0.12)
    }
}
mesh edges = Edges(CP, CE)
mesh pale = map(range(0, 6), |i| Path(CLOOPS[i], tint(PCOL[i], 0.45), 6))
mesh nodes = Nodes(CP)
mesh walker = Follow(u: 0)

slide "follow"
    play Wait(0.6)
    walker.u = 2.15
    play Lerp(7.5, [&walker], linear)
    play Wait(1.6)

Uses the shared definitions for this lesson's figures.

So for the rest of the proof we look for a painting. In a cubic graph the rules pin down what a painting looks like at a single node.

One node of a cubic graph in a painting. The six coat ends at the node are shared out two to a paint, so exactly three paints meet there, one for each pair of edges.
One node of a cubic graph in a painting. The six coat ends at the node are shared out two to a paint, so exactly three paints meet there, one for each pair of edges.
Monocurl source
let c = [-1.7, -0.25, 0]
let A = [PI / 2, PI / 2 + TAU / 3, PI / 2 + 2 * TAU / 3]
let Len = 2.0
let d = 0.13
let arm = |i| [Len * cos(A[i]), Len * sin(A[i]), 0]
let COLS3 = [PLUM, P_TEAL, GOLD]
mesh edges = map([0, 1, 2], |i| Seg(c, c + arm(i), SLATE, 3.5))
mesh ribbons = map([0, 1, 2], |i| {
    let j = imod(i + 1, 3)
    return Path([
        c + arm(i) + perp(unit(arm(i))) * d,
        corner_pt(c, A[i], TAU / 3, d),
        c + arm(j) - perp(unit(arm(j))) * d
    ], COLS3[i], 8)
})
mesh node = Pt(c, INK, 0.11)
mesh count = [
    center{[2.05, 1.0, 0]} Cap("coat ends at the node", 0.76, SLATE),
    center{[2.05, 0.45, 0]} Math("3 \text{ edges} \times 2 = 6", 0.9),
    center{[2.05, -0.55, 0]} Cap("paints at the node", 0.76, SLATE),
    center{[2.05, -1.1, 0]} Math("6 \div 2 = 3", 0.9)
]

Uses the shared definitions for this lesson's figures.

Check yourself

In a painting of a cubic graph, two of the three edges at a node carry the coats $\{1, 2\}$ and $\{2, 3\}$. What are the coats on the third edge?

Three colors give three paints

For some cubic graphs a painting can be read off from a different and much older kind of coloring.

A coloring is not a painting. A coloring puts one color on an edge, and a painting puts two coats on it. Here is how one turns into the other on $K_4$.

  1. Color each edge orange, blue or green so that every node sees all three colors. The two diagonals are green.
  2. Keep the orange and blue edges. Every node has one of each, so they form a closed route. Call it the first paint.
  3. Orange and green together give the second paint.
  4. Blue and green give the third.
  5. Put all three on. An orange edge is in orange with blue and in orange with green, so it has exactly two coats, and so has every other edge.
A 3-edge-coloring of $K_4$ turned into a painting. Each pair of colors forms a cycle, which becomes one paint, and every edge ends up with two coats.
Monocurl source
# drawn as a square with its two diagonals, a little taller than the kit's KP
let KQ = [[-3.2, 1.65, 0], [-3.2, -1.65, 0], [0.1, -1.65, 0], [0.1, 1.65, 0]]
let Colored = |dim| map(range(0, 6), |k| {
    if (KC[k] == dim) {
        return Seg(KQ[KE[k][0]], KQ[KE[k][1]], tint(ECOL[KC[k]], 0.3), 4)
    }
    return Seg(KQ[KE[k][0]], KQ[KE[k][1]], ECOL[KC[k]], 6)
})
let Legend = |i, words, col| center{[2.45, 1.0 - 0.75 * i, 0]} Cap(words, 0.74, col)
mesh colored = Colored(0)
mesh loops = []
mesh nodes = Nodes(KQ)
mesh legend = []

slide "Color each edge orange, blue or green so that every node sees all three colors. The two diagonals are green."
    play Wait(0.5)

slide "Keep the orange and blue edges. Every node has one of each, so they form a closed route. Call it the first paint."
    colored = Colored(3)
    loops = [LoopLine(KQ, KE, [0, 1, 2, 3], PLUM, 0.11)]
    legend = [Legend(0, "orange + blue", PLUM)]
    play Fade(0.7, [&colored, &loops, &legend])

slide "Orange and green together give the second paint."
    colored = Colored(2)
    loops .= LoopLine(KQ, KE, [0, 1, 3, 2], P_TEAL, 0.11)
    legend .= Legend(1, "orange + green", P_TEAL)
    play Fade(0.7, [&colored, &loops, &legend])

slide "Blue and green give the third."
    colored = Colored(1)
    loops .= LoopLine(KQ, KE, [0, 3, 1, 2], GOLD, 0.11)
    legend .= Legend(2, "blue + green", GOLD)
    play Fade(0.7, [&colored, &loops, &legend])

slide "Put all three on. An orange edge is in orange with blue and in orange with green, so it has exactly two coats, and so has every other edge."
    colored = Colored(0)
    play Fade(0.7, [&colored])

Uses the shared definitions for this lesson's figures.

The Petersen graph has no 3-edge-coloring

If every bridgeless cubic graph had a 3-edge-coloring we would be finished. Try it on the Petersen graph, starting with the outer ring.

Pause and try

The outer ring has five edges. In a 3-edge-coloring, two ring edges that share a node have different colors. Show that some color is used on exactly one ring edge.

Show answerHide answer

Two colors are not enough: going round the ring they would have to alternate, and after five edges the last would match the first, which it touches. So all three colors appear. A single color cannot be on three ring edges, because among any three of the five edges two share a node. So each color is on at most two edges, and the only way three colors can fill five edges is two, two and one.

  1. Color the outer ring. One color is used once, here green, and the other two alternate.
  2. At each outer node two colors are taken, so the spoke must have the third.
  3. Three spokes are green. At the inner end of each, the two star edges must be orange and blue in some order. That accounts for all five star edges.
  4. The top inner node has a blue spoke, so its two star edges need orange and green. Neither can be green, and they cannot both be orange.
An attempt to 3-edge-color the Petersen graph. After the outer ring is colored the spokes are forced, and then the two star edges at the top inner node cannot both be colored.
Monocurl source
let PP = petersen([-1.35, -0.14, 0], 1.9, 0.95)
let RING = [1, 2, 1, 2, 3]
# the spoke at outer node i takes the color missing from its two ring edges
let SPOKE = map(range(0, 5), |i| 6 - RING[i] - RING[imod(i + 4, 5)])
let Thick = |k, c| Seg(PP[PE[k][0]], PP[PE[k][1]], ECOL[c], 6)
let Both = |a, b| {
    let n = perp(unit(b - a)) * 0.055
    return [Seg(a + n, b + n, EMBER, 3.5), Seg(a - n, b - n, AZURE, 3.5)]
}
let Sample = |y, c, name| [
    Seg([1.75, y, 0], [2.35, y, 0], ECOL[c], 6),
    center{[2.95, y, 0]} Cap(name, 0.76, ECOL[c])
]
mesh base = Edges(PP, PE, SLATE, 2.5)
mesh ringc = []
mesh spokec = []
mesh starc = []
mesh nodes = Nodes(PP)
mesh legend = [Sample(1.45, 1, "orange"), Sample(0.95, 2, "blue"), Sample(0.45, 3, "green")]
mesh bad = []

slide "Color the outer ring. One color is used once, here green, and the other two alternate."
    ringc = map(range(0, 5), |k| Thick(k, RING[k]))
    play Fade(0.7, [&ringc])

slide "At each outer node two colors are taken, so the spoke must have the third."
    spokec = map(range(0, 5), |i| Thick(5 + i, SPOKE[i]))
    play Fade(0.7, [&spokec])

slide "Three spokes are green. At the inner end of each, the two star edges must be orange and blue in some order. That accounts for all five star edges."
    starc = map(range(10, 15), |k| Both(PP[PE[k][0]], PP[PE[k][1]]))
    legend .= [Both([1.75, -0.35, 0], [2.35, -0.35, 0]), center{[2.6, -0.8, 0]} Cap("orange or blue", 0.76, SLATE)]
    play Fade(0.7, [&starc, &legend])

slide "The top inner node has a blue spoke, so its two star edges need orange and green. Neither can be green, and they cannot both be orange."
    bad = [
        center{PP[5]} fill{alpha{0} PAPER} stroke{ROSE, 4.5} Circle(0.3, 48),
        center{[2.6, -1.55, 0]} Cap("no way to finish", 0.78, ROSE)
    ]
    play Fade(0.6, [&bad])

Uses the shared definitions for this lesson's figures.

A computer search agrees: the ring can be colored in 30 ways, and none of the 30 can be completed. Cubic bridgeless graphs with no 3-edge-coloring are called snarks, and the Petersen graph is the smallest.

So three paints are not always enough, and the Petersen graph needs more. (A theorem recorded in François Jaeger’s 1985 survey says that a cubic graph with a painting in four paints also has one in three, so the Petersen graph needs at least five. One of the problems at the end proves the three-paint half of that.)

With more than three paints there is no coloring to start from, and searching for all the paint at once is hopeless. The remaining two moves split the job.

  1. For every edge, forget which two paints it has and record only how the two differ.
  2. Rebuild the paints from the differences.

That only makes sense if paints are things that can be subtracted, so the first task is to choose the right kind of number.

Arithmetic that only remembers odd and even

Look again at the rule for a painting: at a node, a paint is on two edges or none, never on one or three. The rule only cares whether a count is odd or even. So we use the arithmetic that remembers nothing else. It has two numbers, 0 for even and 1 for odd, and its addition table says that even plus odd is odd and odd plus odd is even:

$$ 0 + 0 = 0, \qquad 0 + 1 = 1 + 0 = 1, \qquad 1 + 1 = 0. $$

This is arithmetic modulo 2. One bit can tell only two paints apart, so we use several bits side by side.

  1. First column: 1 + 0 = 1.
  2. Second column: 0 + 1 = 1.
  3. Third column: 1 + 1 = 0. Nothing is carried, so the sum is 110.
  4. Now add 011 to the answer, a second time.
  5. The result is 101, the code we started with. Adding a code twice does nothing.
Adding the codes 101 and 011 one column at a time, and then adding 011 to the result a second time. The second addition undoes the first.
Monocurl source
let a = 5
let b = 3
let c = xor(a, b)
let e = xor(c, b)
let XL = -1.95
let XR = 1.95
let Row = |n, x, y, col, upto = 3| map(range(0, upto), |j| center{[x + 0.7 * (j - 1), y, 0]} Math(to_string(bit(n, 2 - j)), 1.3, col))
let Rule = |x| [center{[x - 1.5, 0.35, 0]} Math("+", 1.2, SLATE), Seg([x - 1.8, -0.2, 0], [x + 1.15, -0.2, 0], SLATE, 2.5)]
let Column = |x, j| Region([[x + 0.7 * (j - 1) - 0.32, -1.25, 0], [x + 0.7 * (j - 1) + 0.32, -1.25, 0], [x + 0.7 * (j - 1) + 0.32, 1.6, 0], [x + 0.7 * (j - 1) - 0.32, 1.6, 0]], tint(GOLD, 0.3))
mesh column = []
mesh left = [Row(a, XL, 1.1, INK), Row(b, XL, 0.35, INK), Rule(XL)]
mesh sum1 = []
mesh right = []
mesh sum2 = []

slide "First column: 1 + 0 = 1."
    column = Column(XL, 0)
    sum1 = Row(c, XL, -0.75, EMBER, 1)
    play Fade(0.5, [&column, &sum1])

slide "Second column: 0 + 1 = 1."
    column = Column(XL, 1)
    sum1 = Row(c, XL, -0.75, EMBER, 2)
    play Fade(0.5, [&column, &sum1])

slide "Third column: 1 + 1 = 0. Nothing is carried, so the sum is 110."
    column = Column(XL, 2)
    sum1 = Row(c, XL, -0.75, EMBER, 3)
    play Fade(0.5, [&column, &sum1])

slide "Now add 011 to the answer, a second time."
    column = []
    right = [Row(c, XR, 1.1, EMBER), Row(b, XR, 0.35, INK), Rule(XR)]
    play Fade(0.6, [&column, &right])

slide "The result is 101, the code we started with. Adding a code twice does nothing."
    sum2 = Row(e, XR, -0.75, LEAF, 3)
    play Fade(0.6, [&sum2])

Uses the shared definitions for this lesson's figures.

Three facts about this addition carry the rest of the argument, and each can be read off one column at a time.

  • A code added to itself gives zero: $x + x = 0$, because $0 + 0 = 0$ and $1 + 1 = 0$. So adding $x$ twice does nothing, and subtracting $x$ is the same as adding it.
  • The sum of two codes shows where they disagree: a column of $x + y$ is 1 exactly when $x$ and $y$ differ there. So $x + y$ is also the difference between $x$ and $y$, and $x + y = 0$ exactly when $x = y$.
  • Order and grouping do not matter, since that is true of addition in each column.

Pause and try

Compute $110 + 011$. Then find the code $c$ with $101 + c = 011$, without trial and error.

Show answerHide answer

Column by column, $110 + 011 = 101$. For the second, add $101$ to both sides: the left side becomes $101 + 101 + c = c$, so $c = 101 + 011 = 110$. The code that turns one code into another is their sum.

Difference labels

Now let the paints be codes, and carry out step 1 on a painting we already have. The 3-edge-coloring of $K_4$ gave three paints. Name them 00, 01 and 10.

  1. Name the three paints 00, 01 and 10.
  2. The left side has coats 00 and 01. They differ in the second column only, so its label is 01.
  3. The bottom side has 00 and 10, and its label is 10.
  4. A diagonal has 01 and 10, which disagree in both columns. Its label is 11.
  5. The other three edges go the same way. The label 01 is on the edges that were orange, 10 on the blue ones and 11 on the green ones.
The three paints of $K_4$ named by two-bit codes. The label of each edge is the sum of its two coats, and edges of the same color in the original 3-edge-coloring get the same label.
Monocurl source
let LOOPS = [[0, 1, 2, 3], [0, 1, 3, 2], [0, 3, 1, 2]]
let Swatch = |i| [Seg([1.75, 1.45 - 0.55 * i, 0], [2.4, 1.45 - 0.55 * i, 0], PAINT[i], 6), center{[2.95, 1.45 - 0.55 * i, 0]} Cap(code2(i), 0.82, PAINT[i])]
let Working = |k| center{[2.45, -0.75, 0]} Math(str_join([code2(COATS[KC[k]][0]), " + ", code2(COATS[KC[k]][1]), " = ", code2(KL[k])]), 0.95)
let Chips = |ks, tinted| map(ks, |k| Label2(KPos(k), KL[k], [INK, ECOL[KC[k]]][tinted]))
mesh colored = []
mesh edges = Edges(KP, KE, SLATE, 2.5)
mesh loops = map([0, 1, 2], |i| LoopLine(KP, KE, LOOPS[i], PAINT[i], 0.11))
mesh nodes = Nodes(KP)
mesh legend = []
mesh chips = []
mesh working = []

slide "Name the three paints 00, 01 and 10."
    legend = map([0, 1, 2], Swatch)
    play Fade(0.6, [&legend])

slide "The left side has coats 00 and 01. They differ in the second column only, so its label is 01."
    chips = Chips([0], 0)
    working = Working(0)
    play Fade(0.6, [&chips, &working])

slide "The bottom side has 00 and 10, and its label is 10."
    working = []
    play Fade(0.25, [&working])
    chips = Chips([0, 1], 0)
    working = Working(1)
    play Fade(0.6, [&chips, &working])

slide "A diagonal has 01 and 10, which disagree in both columns. Its label is 11."
    working = []
    play Fade(0.25, [&working])
    chips = Chips([0, 1, 2], 0)
    working = Working(2)
    play Fade(0.6, [&chips, &working])

slide "The other three edges go the same way. The label 01 is on the edges that were orange, 10 on the blue ones and 11 on the green ones."
    working = []
    chips = []
    play Fade(0.25, [&working, &chips])
    colored = map(range(0, 6), |k| Seg(KP[KE[k][0]], KP[KE[k][1]], tint(ECOL[KC[k]], 0.55), 9))
    chips = Chips(range(0, 6), 1)
    play Fade(0.7, [&chips, &colored])

Uses the shared definitions for this lesson's figures.

So on $K_4$ the color of an edge was a record of how its two paints differ. Now look at what the labels do at a node.

Pause and try

At the top left corner of the square the three labels are 01, 11 and 10. Add them. Do the same at another corner. Then explain the result, using the fact that the three edges at a node carry three paints $a, b, c$, one shared by each pair of edges.

Show answerHide answer

$01 + 11 + 10 = 00$, and every other corner has the same three labels. The reason: the three edges have coats $\{a, b\}$, $\{b, c\}$ and $\{c, a\}$, so their labels are $a + b$, $b + c$ and $c + a$. In the sum of the three, each paint appears twice, and a code added twice is zero.

  1. At the top left corner the labels are 01, 11 and 10.
  2. Add them. Each column holds two 1s, and they cancel.
  3. The bottom right corner has the same three labels in another order, and the same sum.
The labels at two corners of $K_4$, added column by column. Each column contains two 1s, so the sum is 00.
Monocurl source
let Corner = |v| [
    map(range(0, 6), |k| Label2(KPos(k), KL[k], [SLATE, EMBER][KE[k][0] == v or KE[k][1] == v])),
    center{KP[v]} fill{alpha{0} PAPER} stroke{EMBER, 4} Circle(0.26, 48)
]
let Sum = |v, done| block {
    let L = map(inc(KE, v), |k| KL[k])
    for (r in [0, 1, 2]) {
        . center{[2.6, 1.15 - 0.62 * r, 0]} Math(code2(L[r]), 1.1, EMBER)
    }
    . center{[1.85, -0.1, 0]} Math("+", 1.05, SLATE)
    . Seg([1.75, -0.5, 0], [3.3, -0.5, 0], SLATE, 2.5)
    if (done == 1) {
        . center{[2.6, -0.98, 0]} Math(code2(node_sum(KE, KL, v)), 1.1, LEAF)
    }
}
mesh edges = Edges(KP, KE, SLATE, 3)
mesh nodes = Nodes(KP)
mesh corner = map(range(0, 6), |k| Label2(KPos(k), KL[k], SLATE))
mesh total = []

slide "At the top left corner the labels are 01, 11 and 10."
    corner = []
    play Fade(0.25, [&corner])
    corner = Corner(0)
    total = Sum(0, 0)
    play Fade(0.6, [&corner, &total])

slide "Add them. Each column holds two 1s, and they cancel."
    total = Sum(0, 1)
    play Fade(0.6, [&total])

slide "The bottom right corner has the same three labels in another order, and the same sum."
    total = []
    corner = []
    play Fade(0.25, [&total, &corner])
    corner = Corner(2)
    total = Sum(2, 1)
    play Fade(0.6, [&corner, &total])

Uses the shared definitions for this lesson's figures.

Nothing in that explanation used $K_4$ or two bits.

Both properties are checked at one edge or one node at a time, with no mention of paint. That is the point of step 1: labels can be hunted for directly, and they are far easier to find than paintings.

The name comes from a more general setting in which labels are numbers that flow along directed edges, with as much flowing into each node as out of it. With codes, $x = -x$, so no directions are needed. In the literature the set of $k$-bit codes with this addition is written $\mathbb{F}_2^k$ or $\mathbb{Z}_2^k$.

Two bits are the three colors again

How many bits do the labels need? Try two.

Pause and try

In a nowhere-zero two-bit flow on a cubic graph, can two of the three edges at a node have the same label?

Show answerHide answer

No. If two of them were $x$ and $x$, they would add up to 0, and the third label would have to be 0 for the three to add up to 0. That is not allowed.

So two-bit flows and 3-edge-colorings are the same thing, and the Petersen graph, which has no 3-edge-coloring, has no nowhere-zero two-bit flow.

Three bits are always enough

With a third bit there are eight codes for paints and seven possible labels. Here the 1970s supply a theorem.

P. A. Kilpatrick proved this in a 1975 thesis at the University of Cape Town, and François Jaeger found it independently, in work presented in 1975 and published in 1976 and 1979. It is known as the 8-flow theorem, because by a 1954 theorem of W. T. Tutte a graph has a nowhere-zero flow with values in a commutative group of eight elements, such as the three-bit codes, exactly when it has one whose values are nonzero whole numbers between $-7$ and $7$, flowing along directed edges. We will use the theorem without proving it, and a sketch of where it comes from follows below.

Here is such a flow on the Petersen graph. At the top node the labels are 111, 011 and 100: the first column holds $1, 0, 1$, the second $1, 1, 0$ and the third $1, 1, 0$, and each of those adds up to an even number.

A nowhere-zero three-bit flow on the Petersen graph. The sum at the top node is worked on the right, and the scene has checked the sum at all ten nodes.
A nowhere-zero three-bit flow on the Petersen graph. The sum at the top node is worked on the right, and the scene has checked the sum at all ten nodes.
Monocurl source
let PP = petersen([-1.45, -0.14, 0], 1.92, 1.1)
let L = map(inc(PE, 0), |k| PF[k])
let ZERO = count(range(0, 10), |v| node_sum(PE, PF, v) == 0)
mesh edges = Edges(PP, PE, SLATE, 3)
mesh nodes = Nodes(PP)
mesh ring = center{PP[0]} fill{alpha{0} PAPER} stroke{EMBER, 4} Circle(0.26, 48)
mesh labels = map(range(0, 15), |k| Label3(LabelPos(PP, k), PF[k], map([0, 1, 2], |j| [INK, EMBER][PE[k][0] == 0 or PE[k][1] == 0])))
mesh total = [
    map([0, 1, 2], |r| center{[2.6, 1.6 - 0.58 * r, 0]} Math(code3(L[r]), 1.05, EMBER)),
    center{[1.75, 0.45, 0]} Math("+", 1.05, SLATE),
    Seg([1.65, 0.05, 0], [3.4, 0.05, 0], SLATE, 2.5),
    center{[2.6, -0.4, 0]} Math(code3(node_sum(PE, PF, 0)), 1.05, LEAF),
    center{[2.55, -1.2, 0]} Cap("sum is 000 at", 0.76, SLATE),
    center{[2.55, -1.68, 0]} Math(str_join([to_string(ZERO), "\text{ of } 10 \text{ nodes}"]), 0.85)
]

Uses the shared definitions for this lesson's figures.

Check yourself

Two of the three edges at a node of a cubic graph have labels 101 and 110 in a nowhere-zero three-bit flow. What is the third label?

Why the theorem is believable: a sketch

This part is a sketch. It proves everything except one cited theorem about trees and one reduction, and I will say where those are.

Read a three-bit labeling one column at a time. The first column selects some edges, those whose first bit is 1. The labels at a node add up to 0 in that column exactly when the node has an even number of selected edges. And a label is different from 000 exactly when its edge is selected by at least one column. So a nowhere-zero three-bit flow is the same thing as three sets of edges, each meeting every node an even number of times, that together contain every edge. The next figure draws the three sets for the labeling above.

The three-bit labels of the Petersen graph, read one column at a time. Each column of the codes, taken alone, marks out a closed route: blue for the first column, orange for the second, green for the third.
Monocurl source
let PP = petersen([0, -0.14, 0], 1.92, 1.1)
let STRIPES = map([2, 1, 0], |b| cycles_of(PE, filter(range(0, 15), |k| bit(PF[k], b) == 1)))
let SPTS = map(STRIPES, |cs| map(cs, |V| dense(closed(map(V, |i| PP[i])))))
let SCOL = [tint(AZURE, 0.5), tint(EMBER, 0.8), LEAF]
let SW = [17, 10, 4.5]
let Stripes = |u| block {
    for (i in [0, 1, 2]) {
        for (pts in SPTS[i]) {
            . Grown(pts, clamp(0, u - 1.15 * i, 1), SCOL[i], SW[i])
        }
    }
}
mesh stripes = Stripes(u: 0)
mesh edges = Edges(PP, PE, SLATE, 2)
mesh nodes = Nodes(PP)
mesh labels = map(range(0, 15), |k| Label3(LabelPos(PP, k), PF[k], STRIPE))

slide "stripes"
    play Wait(0.8)
    stripes.u = 3.3
    play Lerp(8, [&stripes], linear)
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

Sets of edges that meet every node an even number of times are easy to make from a spanning tree, which is a set of edges that connects all the nodes and contains no cycle.

  1. A spanning tree of the Petersen graph: nine edges that connect all ten nodes and contain no cycle.
  2. Take one of the six edges outside the tree. Together with tree edges it closes exactly one cycle.
  3. Do this for all six outside edges, and write on every edge how many of the six cycles use it.
  4. Keep the edges with an odd count. Every node meets two of them or none, and all six outside edges are among them.
Building one column of the labeling from a spanning tree of the Petersen graph. Each edge outside the tree closes one cycle, and the edges used by an odd number of the six cycles are exactly the edges whose first bit is 1 in the labeling above.
Monocurl source
let PP = petersen([-1.45, -0.14, 0], 1.92, 1.1)
let TREE = [0, 1, 2, 4, 6, 7, 8, 10, 12]
let OUTSIDE = filter(range(0, 15), |k| count(TREE, |x| x == k) == 0)
# the cycle that each outside edge closes with the tree, as a list of edges
let FC = [[0, 1, 2, 3, 4], [0, 1, 5, 7, 10], [0, 1, 4, 7, 9, 12], [1, 2, 6, 8, 11], [2, 7, 8, 10, 13], [1, 6, 7, 12, 14]]
let USES = map(range(0, 15), |k| count(FC, |c| count(c, |x| x == k) > 0))
let ODD = filter(range(0, 15), |k| imod(USES[k], 2) == 1)
let ESeg = |k, col, w| Seg(PP[PE[k][0]], PP[PE[k][1]], col, w)
let Key = |i, col, words| [Seg([1.55, 1.3 - 0.6 * i, 0], [2.1, 1.3 - 0.6 * i, 0], col, 6), center{[2.95, 1.3 - 0.6 * i, 0]} Cap(words, 0.74, col)]
mesh edges = Edges(PP, PE, SLATE, 2.5)
mesh tree = []
mesh extra = []
mesh nodes = Nodes(PP)
mesh counts = []
mesh key = []

slide "A spanning tree of the Petersen graph: nine edges that connect all ten nodes and contain no cycle."
    tree = map(TREE, |k| ESeg(k, AZURE, 6))
    key = [Key(0, AZURE, "tree"), Key(1, SLATE, "outside")]
    play Fade(0.7, [&tree, &key])

slide "Take one of the six edges outside the tree. Together with tree edges it closes exactly one cycle."
    extra = [ESeg(3, EMBER, 6), LoopLine(PP, PE, [0, 1, 2, 3, 4], EMBER, 0.11, 4)]
    key .= Key(2, EMBER, "its cycle")
    play Fade(0.7, [&extra, &key])

slide "Do this for all six outside edges, and write on every edge how many of the six cycles use it."
    extra = []
    key = [Key(0, AZURE, "tree"), Key(1, SLATE, "outside")]
    counts = map(range(0, 15), |k| [
        center{LabelPos(PP, k)} fill{PAPER} stroke{MIST, 1.5} Circle(0.19, 32),
        center{LabelPos(PP, k)} Math(to_string(USES[k]), 0.78, INK)
    ])
    play Fade(0.7, [&extra, &counts, &key])

slide "Keep the edges with an odd count. Every node meets two of them or none, and all six outside edges are among them."
    tree = map(ODD, |k| ESeg(k, AZURE, 8))
    key = [Key(0, AZURE, "odd count")]
    play Fade(0.7, [&tree, &key])

Uses the shared definitions for this lesson's figures.

Here is why that works for any spanning tree $T$ of any connected graph. An edge $e$ outside $T$ closes exactly one cycle $C_e$ with it, namely $e$ together with the route through the tree between its ends. Let $S$ be the set of edges that lie on an odd number of the cycles $C_e$. Each cycle meets each node in two edges or none, so adding up over all the cycles, each node meets an even number of edge uses, and therefore an even number of edges that are used an odd number of times. And each outside edge lies on exactly one of the cycles, its own, so it belongs to $S$. Thus $S$ meets every node an even number of times and contains every edge outside $T$.

Now suppose we have three spanning trees with no edge in all three. Each tree gives a set $S$, and each edge is outside at least one of the trees, so it lies in at least one of the three sets. Those three sets are a nowhere-zero three-bit flow.

The existence of three such trees is the part I am citing. Call a graph 3-edge-connected if it stays connected when any two edges are deleted; the Petersen graph is an example. Replace each edge of such a graph by two parallel edges. The doubled graph stays connected when any five edges are deleted, and a theorem of Tutte and Nash-Williams from 1961 then provides three spanning trees that share no edges. Back in the original graph, no edge is in all three trees, because it had only two copies. For bridgeless graphs that are not 3-edge-connected a further reduction is needed, which I omit.

For the Petersen graph nothing has been left out: the labeling in the figures is the one that three particular spanning trees produce, and the figure above shows the first of them.

The stripes are not the cycles we want

The three columns give three sets of cycles, and every edge is on at least one of them. It is tempting to hope that they are the cycle double cover. Count how many of the three run along each edge.

The number of 1s in the label of each edge of the Petersen graph, which is the number of the three colored routes that use the edge. A cycle double cover needs exactly two on every edge.
The number of 1s in the label of each edge of the Petersen graph, which is the number of the three colored routes that use the edge. A cycle double cover needs exactly two on every edge.
Monocurl source
let PP = petersen([-1.45, -0.14, 0], 1.92, 1.1)
let ONES = map(PF, |f| bit(f, 0) + bit(f, 1) + bit(f, 2))
let NCOL = [SLATE, GOLD, AZURE, ROSE]
let Tally = |n| [
    Seg([1.45, 2.05 - 0.8 * n, 0], [2.0, 2.05 - 0.8 * n, 0], NCOL[n], 2 + 2.5 * n),
    center{[2.9, 2.05 - 0.8 * n, 0]} Math(str_join([to_string(n), "\text{ on } ", to_string(count(ONES, |x| x == n)), "\text{ edges}"]), 0.82, NCOL[n])
]
mesh edges = map(range(0, 15), |k| Seg(PP[PE[k][0]], PP[PE[k][1]], NCOL[ONES[k]], 2 + 2.5 * ONES[k]))
mesh nodes = Nodes(PP)
mesh labels = map(range(0, 15), |k| Label3(LabelPos(PP, k), PF[k], map([0, 1, 2], |j| NCOL[ONES[k]])))
mesh tally = map([1, 2, 3], Tally)

Uses the shared definitions for this lesson's figures.

Six edges have one, six have two and three have all three. For decades the labels were used in this direct way, by picking out cycles one combination of columns at a time, and some edges were always reached too often. The best result of that kind is from 1983: Jean-Claude Bermond, Bill Jackson and François Jaeger took all seven combinations of the three sets and showed that every edge lies on exactly four of them. (One of the problems at the end asks you to prove it.) Four times is not twice.

The stripes are still what makes the labels exist. The idea that was missing is to read a label as the theorem on labels of a painting suggests: as the difference between the two coats of its edge.

Back from labels to paint, at one node

Now for step 2. Suppose we are handed a nowhere-zero flow and want a painting whose labels are that flow. At a single node there is nothing to it.

  1. The labels at this node are 01, 11 and 10. Pick any paint for the first sector, say 00.
  2. Cross the edge labeled 01. The paint on its other side must differ from 00 by 01, so it is 01.
  3. Cross the edge labeled 11. The next paint is 01 + 11 = 10.
  4. Crossing the last edge gives 10 + 10 = 00, the paint we began with. It had to, because the three labels add up to 00.
  5. Begin with 11 instead, and all three paints change by 11. The first paint is the only choice there is.
Rebuilding the three paints at a node from its labels 01, 11 and 10. The paint in one sector is a free choice, each label gives the paint across its edge, and a different first choice moves all three paints together.
Monocurl source
let c = [-1.85, -0.35, 0]
let A = [PI / 2, PI / 2 + TAU / 3, PI / 2 + 2 * TAU / 3]
let LAB = [1, 3, 2]
let Len = 1.95
let d = 0.13
let arm = |i| [Len * cos(A[i]), Len * sin(A[i]), 0]
# sector i lies between arm i - 1 and arm i, going counterclockwise; crossing arm i adds its label
let paint_of = |t, i| [t, xor(t, LAB[0]), xor(xor(t, LAB[0]), LAB[1])][i]
let Sector = |t, i| {
    let a = imod(i + 2, 3)
    return Path([
        c + arm(a) + perp(unit(arm(a))) * d,
        corner_pt(c, A[a], TAU / 3, d),
        c + arm(i) - perp(unit(arm(i))) * d
    ], PAINT[paint_of(t, i)], 8)
}
let Sectors = |t, upto| map(range(0, upto), |i| Sector(t, i))
# the four two-bit codes at the corners of a square
let SQ = [[1.75, -1.25, 0], [3.25, -1.25, 0], [1.75, 0.25, 0], [3.25, 0.25, 0]]
let Used = |t, upto| [
    map(range(0, upto), |i| Pt(SQ[paint_of(t, i)], PAINT[paint_of(t, i)], 0.16)),
    map(range(0, 4), |n| center{SQ[n] + [[-0.42, 0.42][bit(n, 0)], [-0.3, 0.3][bit(n, 1)], 0]} Cap(code2(n), 0.76, PAINT[n]))
]
let PaintTri = |t| Shape(map([0, 1, 2], |i| SQ[paint_of(t, i)]), SLATE, 0.14, 2.5)
mesh tri = []
mesh grid = map([[0, 1], [0, 2], [1, 3], [2, 3]], |e| Seg(SQ[e[0]], SQ[e[1]], MIST, 2.5))
mesh edges = map([0, 1, 2], |i| Seg(c, c + arm(i), SLATE, 3.5))
mesh sectors = []
mesh node = Pt(c, INK, 0.11)
mesh labels = map([0, 1, 2], |i| Label2(c + arm(i) * 0.62, LAB[i]))
mesh used = Used(0, 0)
mesh title = center{[2.5, 1.2, 0]} Cap("the four paints", 0.76, SLATE)

slide "The labels at this node are 01, 11 and 10. Pick any paint for the first sector, say 00."
    sectors = Sectors(0, 1)
    used = Used(0, 1)
    play Fade(0.6, [&sectors, &used])

slide "Cross the edge labeled 01. The paint on its other side must differ from 00 by 01, so it is 01."
    sectors = Sectors(0, 2)
    used = Used(0, 2)
    play Fade(0.6, [&sectors, &used])

slide "Cross the edge labeled 11. The next paint is 01 + 11 = 10."
    sectors = Sectors(0, 3)
    used = Used(0, 3)
    play Fade(0.6, [&sectors, &used])

slide "Crossing the last edge gives 10 + 10 = 00, the paint we began with. It had to, because the three labels add up to 00."
    tri = PaintTri(0)
    play Fade(0.6, [&tri])

slide "Begin with 11 instead, and all three paints change by 11. The first paint is the only choice there is."
    sectors = Sectors(3, 3)
    used = Used(3, 3)
    tri = PaintTri(3)
    play Fade(0.8, [&sectors, &used, &tri])

Uses the shared definitions for this lesson's figures.

The three paints at a node form a triangle among the codes, and the only freedom is to move the whole triangle. Stated without reference to a starting sector:

I will call $s$ the slide of the node. In words, the paint between two edges is the slide plus the label of the third edge, and sliding by $t$ adds $t$ to all three paints. With three bits the codes sit at the corners of a cube, and the triangle has eight positions.

The top node of the Petersen graph, with labels 111, 011 and 100, and its triangle of three paints among the eight three-bit codes. As the slide runs through all eight codes the three paints move together, and each edge keeps a pair of paints that differ by its label.
Monocurl source
let c = [-2.0, -0.35, 0]
let A = [PI / 2, PI / 2 + TAU / 3, PI / 2 + 2 * TAU / 3]
let LAB = [7, 3, 4]
let Len = 1.9
let d = 0.13
let arm = |i| [Len * cos(A[i]), Len * sin(A[i]), 0]
# the cube of codes: one direction for each bit
let cube_pos = |n| [0.7 + 1.5 * bit(n, 0) + 0.95 * bit(n, 2), -1.4 + 1.5 * bit(n, 1) + 0.45 * bit(n, 2), 0]
let CUBE_E = filter(flatten(map(range(0, 8), |a| map(range(0, 8), |b| [a, b]))), |e| e[0] < e[1] and (xor(e[0], e[1]) == 1 or xor(e[0], e[1]) == 2 or xor(e[0], e[1]) == 4))
let Slid = |s| {
    let t = to_int(clamp(0, floor(s + 0.001), 7))
    return [
        Shape(map([0, 1, 2], |i| cube_pos(xor(t, LAB[i]))), SLATE, 0.16, 2.5),
        map([0, 1, 2], |i| Pt(cube_pos(xor(t, LAB[i])), PAINT[xor(t, LAB[i])], 0.17)),
        # the sector between arm i and the next arm gets the slide plus the label of the third arm
        map([0, 1, 2], |i| Path([
            c + arm(i) + perp(unit(arm(i))) * d,
            corner_pt(c, A[i], TAU / 3, d),
            c + arm(imod(i + 1, 3)) - perp(unit(arm(imod(i + 1, 3)))) * d
        ], PAINT[xor(t, LAB[imod(i + 2, 3)])], 8)),
        center{[-2.0, 1.9, 0]} Cap(str_join(["slide ", code3(t)]), 0.8, INK)
    ]
}
mesh grid = map(CUBE_E, |e| Seg(cube_pos(e[0]), cube_pos(e[1]), MIST, 2.5))
mesh edges = map([0, 1, 2], |i| Seg(c, c + arm(i), SLATE, 3.5))
mesh slid = Slid(s: 0)
mesh node = Pt(c, INK, 0.11)
mesh labels = map([0, 1, 2], |i| Label3(c + arm(i) * 0.62, LAB[i]))
mesh names = map(range(0, 8), |n| center{cube_pos(n) + [[-0.2, 0.2][bit(n, 0)], [-0.3, 0.3][bit(n, 1)], 0]} Cap(code3(n), 0.7, PAINT[n]))

slide "slide"
    slid.s = 7.999
    play Lerp(10, [&slid], linear)

Uses the shared definitions for this lesson's figures.

The pieces do not fit by themselves

Let every node choose a slide and paint its own half of each of its three edges. Each edge is then painted twice, once from each end, and we have a painting exactly when the two halves of every edge carry the same two paints.

With two-bit labels this takes care of itself. Give every node the slide 00. The half of an edge labeled $x$ gets the labels of the other two edges at its node, and by the theorem on two-bit flows those are always the two nonzero codes other than $x$. Both ends agree, on every edge. This is the painting that Szekeres’s theorem builds from a 3-edge-coloring, with its paints renamed: the paint called 01 is on exactly the edges labeled 10 and 11, which is the cycle formed by two of the three colors.

With three bits the two ends of an edge see different neighbors, and nothing forces them to agree.

  1. Every node has slide 000, so each half of an edge carries the labels of the other two edges at its node. On twelve of the fifteen edges the two halves do not match.
  2. Slide the ringed node by 001. Its three paints change, and nine broken edges are left.
  3. Slide two more nodes, by 011 and 101. Three broken edges are left.
  4. Now every slide of a single node leaves at least three broken edges. Mending one edge breaks another.
Every node of the Petersen graph paints its half of each of its edges. A red ring marks an edge whose two halves carry different pairs of paints. Sliding nodes one at a time mends some edges and then gets stuck.
Monocurl source
let PP = petersen([-1.4, -0.14, 0], 1.92, 1.1)
let S0 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let S1 = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
let S2 = [0, 0, 0, 0, 1, 0, 3, 5, 0, 0]
let set_at = |S, v, s| map(range(0, 10), |i| [S[i], s][i == v])
# the fewest broken edges reachable from S2 by changing the slide of one node
let BEST = min_of(flatten(map(range(0, 10), |v| map(range(0, 8), |s| len(broken(PE, PF, set_at(S2, v, s)))))))
let Marks = |S| map(broken(PE, PF, S), |k| center{(PP[PE[k][0]] + PP[PE[k][1]]) / 2} fill{PAPER} stroke{ROSE, 3.5} Circle(0.1, 32))
let Moved = |S| map(filter(range(0, 10), |v| S[v] != 0), |v| center{PP[v]} fill{alpha{0} PAPER} stroke{INK, 3} Circle(0.24, 48))
let Count = |S| [
    center{[2.55, 0.75, 0]} Cap("edges whose two", 0.76, SLATE),
    center{[2.55, 0.33, 0]} Cap("halves disagree", 0.76, SLATE),
    center{[2.55, -0.3, 0]} Math(str_join([to_string(len(broken(PE, PF, S))), "\text{ of } 15"]), 1.05, ROSE)
]
mesh edges = Edges(PP, PE, SLATE, 2)
mesh ribbons = Ribbons(PP, PE, PF, S0)
mesh marks = Marks(S0)
mesh nodes = Nodes(PP)
mesh moved = []
mesh readout = Count(S0)
mesh verdict = []

slide "Every node has slide 000, so each half of an edge carries the labels of the other two edges at its node. On twelve of the fifteen edges the two halves do not match."
    play Wait(0.6)

slide "Slide the ringed node by 001. Its three paints change, and nine broken edges are left."
    readout = []
    play Fade(0.25, [&readout])
    ribbons = Ribbons(PP, PE, PF, S1)
    marks = Marks(S1)
    moved = Moved(S1)
    readout = Count(S1)
    play Fade(0.8, [&ribbons, &marks, &moved, &readout])

slide "Slide two more nodes, by 011 and 101. Three broken edges are left."
    readout = []
    play Fade(0.25, [&readout])
    ribbons = Ribbons(PP, PE, PF, S2)
    marks = Marks(S2)
    moved = Moved(S2)
    readout = Count(S2)
    play Fade(0.8, [&ribbons, &marks, &moved, &readout])

slide "Now every slide of a single node leaves at least three broken edges. Mending one edge breaks another."
    verdict = [
        center{[2.55, -1.2, 0]} Cap("best after one", 0.76, SLATE),
        center{[2.55, -1.62, 0]} Cap(str_join(["more slide: ", to_string(BEST)]), 0.76, SLATE)
    ]
    play Fade(0.6, [&verdict])

Uses the shared definitions for this lesson's figures.

There are eight slides at each of ten nodes, $8^{10}$ settings in all, which is more than a billion, and fixing edges one at a time moves the trouble around. More to the point, we want an argument for every graph and every labeling.

So here is exactly what is established and what is missing. Every bridgeless cubic graph has a nowhere-zero three-bit flow (Jaeger and Kilpatrick, cited). At each node, the paintings of its three half-edges that fit the labels are known completely: there are eight, one for each slide. What is missing is a reason why the slides can always be chosen so that the two halves of every edge agree. That was the open half for fifty years, and the rest of the argument proves it.

A toy problem: a ring that must alternate

Before the real problem, here is one where the answer is visible at a glance, so that we can watch a method work. Color the edges of a ring of six nodes orange and blue so that the colors alternate. Let every node solve its own small problem: it colors the half-edge on one side orange and the half on the other side blue. Its only choice is which way round.

  1. Every node has colored its two half-edges. A red ring marks an edge whose halves have different colors.
  2. Flip the circled node, so that its orange and blue change places. One edge is mended and its neighbor breaks.
  3. Flip the next node as well. Every edge agrees, and the colors alternate around the ring.
A ring of six nodes. Each node colors one of its two half-edges orange and the other blue. Flipping two nodes makes the two halves of every edge agree, and the ring alternates.
Monocurl source
let B0 = [0, 1, 1, 0, 0, 1]
let B1 = [0, 1, 0, 0, 0, 1]
let B2 = [0, 1, 0, 1, 0, 1]
let C6 = [-1.3, 0, 0]
mesh ring = RingFig(6, B0, C6, 1.75, [])
mesh marks = RingMarks(6, B0, C6, 1.75)
mesh words = [
    center{[2.5, 0.5, 0]} Cap("edges whose two", 0.76, SLATE),
    center{[2.5, 0.08, 0]} Cap("halves disagree", 0.76, SLATE)
]
mesh number = center{[2.5, -0.6, 0]} Math(to_string(ring_broken(6, B0)), 1.1, ROSE)

slide "Every node has colored its two half-edges. A red ring marks an edge whose halves have different colors."
    play Wait(0.5)

slide "Flip the circled node, so that its orange and blue change places. One edge is mended and its neighbor breaks."
    number = []
    marks = []
    play Fade(0.25, [&number, &marks])
    ring = RingFig(6, B1, C6, 1.75, [2])
    marks = RingMarks(6, B1, C6, 1.75)
    number = center{[2.5, -0.6, 0]} Math(to_string(ring_broken(6, B1)), 1.1, ROSE)
    play Fade(0.7, [&ring, &marks, &number])

slide "Flip the next node as well. Every edge agrees, and the colors alternate around the ring."
    number = []
    marks = []
    play Fade(0.25, [&number, &marks])
    ring = RingFig(6, B2, C6, 1.75, [2, 3])
    number = center{[2.5, -0.6, 0]} Math(to_string(ring_broken(6, B2)), 1.1, LEAF)
    play Fade(0.7, [&ring, &number])

Uses the shared definitions for this lesson's figures.

Now try the same with five nodes.

  1. With five nodes, one edge is broken.
  2. Flip a node at one end of it. The broken edge moves along.
  3. Flip the next node. It moves again, and it never goes away.
  4. Count the orange halves. Every node colors exactly one, whichever way round it is, so there are five.
A ring of five nodes. Flipping a node moves the broken edge but never removes it. Each node colors exactly one half-edge orange, so there are five orange halves, and an odd number of them cannot be paired up along edges.
Monocurl source
let B0 = [0, 1, 0, 1, 0]
let B1 = [1, 1, 0, 1, 0]
let B2 = [1, 0, 0, 1, 0]
let C5 = [-1.3, -0.2, 0]
let Panel5 = |bits| [
    center{[2.5, 0.5, 0]} Cap("edges whose two", 0.76, SLATE),
    center{[2.5, 0.08, 0]} Cap("halves disagree", 0.76, SLATE),
    center{[2.5, -0.6, 0]} Math(to_string(ring_broken(5, bits)), 1.1, ROSE)
]
mesh ring = RingFig(5, B0, C5, 1.55, [])
mesh marks = RingMarks(5, B0, C5, 1.55)
mesh tally = Panel5(B0)

slide "With five nodes, one edge is broken."
    play Wait(0.5)

slide "Flip a node at one end of it. The broken edge moves along."
    marks = []
    play Fade(0.25, [&marks])
    ring = RingFig(5, B1, C5, 1.55, [0])
    marks = RingMarks(5, B1, C5, 1.55)
    play Fade(0.7, [&ring, &marks])

slide "Flip the next node. It moves again, and it never goes away."
    marks = []
    play Fade(0.25, [&marks])
    ring = RingFig(5, B2, C5, 1.55, [0, 1])
    marks = RingMarks(5, B2, C5, 1.55)
    play Fade(0.7, [&ring, &marks])

slide "Count the orange halves. Every node colors exactly one, whichever way round it is, so there are five."
    tally = []
    play Fade(0.25, [&tally])
    tally = [
        map(range(0, 5), |i| center{ring_pts(5, 2.0, C5, PI / 2)[i]} Math("1", 0.95, EMBER)),
        center{[2.5, 0.5, 0]} Cap("orange halves", 0.78, EMBER),
        center{[2.5, -0.1, 0]} Math("1 + 1 + 1 + 1 + 1 = 5", 0.9, EMBER)
    ]
    play Fade(0.7, [&tally])

Uses the shared definitions for this lesson's figures.

Pause and try

Before reading on: an edge whose halves agree has how many orange halves? What does that say about the total number of orange halves if every edge agrees? Compare with what the nodes supply.

Show answerHide answer

An edge that agrees is orange on both halves or blue on both, so it has two orange halves or none. If every edge agreed, the orange halves would come in twos and their total would be even. But each node colors exactly one half orange, whatever its choice, so a ring of $n$ nodes has exactly $n$ orange halves. With $n = 5$ that is odd, so not every edge can agree. With $n = 6$ the count is even and nothing stands in the way.

The same argument can be written with equations, which is the form we will need. Record each node’s choice as a bit, saying on which side its orange half lies. Call the bits $a, b, c, d, e$ around the ring of five. An edge agrees when the choices at its two ends are different, which for bits means that they add up to 1.

The ring of five as five equations between bits, one for each edge. In the sum of all five every unknown appears twice and cancels, while the right-hand sides add up to 1.
The ring of five as five equations between bits, one for each edge. In the sum of all five every unknown appears twice and cancels, while the right-hand sides add up to 1.
Monocurl source
let C5 = [-1.9, -0.1, 0]
let P5 = ring_pts(5, 1.45, C5, PI / 2)
let NAMES = ["a", "b", "c", "d", "e"]
mesh ring = [
    map(range(0, 5), |i| Seg(P5[i], P5[imod(i + 1, 5)], SLATE, 3.5)),
    map(P5, |p| Pt(p, INK, 0.1)),
    map(range(0, 5), |i| center{ring_pts(5, 1.85, C5, PI / 2)[i]} Math(NAMES[i], 0.95, AZURE))
]
mesh eqs = [
    map(range(0, 5), |i| center{[2.0, 1.65 - 0.52 * i, 0]} Math(str_join([NAMES[i], " + ", NAMES[imod(i + 1, 5)], " = 1"]), 0.92)),
    Seg([0.95, -0.78, 0], [3.05, -0.78, 0], SLATE, 2.5),
    center{[2.0, -1.25, 0]} Math("0 = 1", 1.05, ROSE)
]

Uses the shared definitions for this lesson's figures.

Add the five equations. On the left every unknown appears twice, and anything added twice is 0. On the right there are five 1s, which add up to 1. The sum reads $0 = 1$. The choices have dropped out, and what remains is the odd number five. No choice of bits can satisfy all five equations, since it would satisfy their sum.

Look at how the method went. Each node solved its own problem, with one free choice. The constraints were between neighbors only. Whether a solution for the whole ring exists came down to whether those constraints are compatible, and incompatibility showed up as a sum of equations reading $0 = 1$.

Equations between bits fail only by adding up to 0 = 1

An equation between bits is linear if it says that the sum of some of the unknowns equals 0 or equals 1. For systems of such equations, the ring of five shows the only thing that can go wrong.

This is elimination, as taught for ordinary systems of equations, carried out with bits. It turns the question “is there a solution?” into the question “can the equations be added up to $0 = 1$?”, and the second question can be attacked by counting.

Agreement written as equations

Back to the graph, where each node paints its half of each of its edges with its slide plus the labels of the other two edges. Fix a cubic graph and a nowhere-zero three-bit flow, and write $f(e)$ for the label of the edge $e$ and $s_v$ for the slide of the node $v$. For a node $v$ and an edge $e$ at $v$, let $c(v, e)$ be the label of one of the other two edges at $v$; pick either, once and for all. The other one is then $c(v, e) + f(e)$, because the three labels at $v$ add up to 000. So the half of $e$ at $v$ carries the two paints

$$ s_v + c(v, e) \qquad \text{and} \qquad s_v + c(v, e) + f(e). $$

Pause and try

Let $p \neq 000$. Two pairs $\{A, A + p\}$ and $\{B, B + p\}$ are the same pair exactly when $A + B$ takes one of two values. Which two?

Show answerHide answer

The pairs are equal exactly when $A$ is one of $B$ and $B + p$ (the other member, $A + p$, is then the other one). That means $A + B = 000$ or $A + B = p$.

Apply this to an edge $e$ joining $u$ and $v$, with $A = s_u + c(u, e)$ and $B = s_v + c(v, e)$ and $p = f(e)$. Writing $d_e = c(u, e) + c(v, e)$, which is a known code, the two halves of $e$ agree exactly when $s_u + s_v + d_e$ is $000$ or $f(e)$. Introduce one more unknown bit $\varepsilon_e$ to say which.

Each of these is an equation between codes, so it is three equations between bits, one for each column. The unknowns are the three bits of each slide and the bit $\varepsilon_e$ of each edge, and every equation is linear. For the Petersen graph that makes 45 equations in 45 unknowns. By the theorem on the only obstacle, a solution exists unless some of these equations add up to $0 = 1$.

Adding up some of the equations

To describe a selection of equations we say, for each edge, which of its three columns are taken. Record that as a code $r_e$ with a 1 in each chosen column, and $000$ if none of that edge’s equations are taken.

Two rules follow column by column: $r \cdot (w + w') = r \cdot w + r \cdot w'$ and $(r + r') \cdot w = r \cdot w + r' \cdot w$. Also, $r \cdot w = 1$ for some $w$ forces $r \neq 000$.

Adding up the chosen columns of the equation of $e$ gives $r_e \cdot s_u + r_e \cdot s_v + \varepsilon_e \,(r_e \cdot f(e)) = r_e \cdot d_e$. Now add over all edges and ask when every unknown cancels. The unknown $\varepsilon_e$ occurs only in the equation of $e$, multiplied by $r_e \cdot f(e)$. A bit of the slide $s_v$ occurs in the equations of the three edges at $v$, and it cancels when the codes $r_e$ of those three edges have an even number of 1s in its column.

These are exactly the ways of adding up equations so that all unknowns cancel. What is left is $0 = \sum_e r_e \cdot d_e$, the sum over all edges. So the whole problem is now this: show that for every selection, the bits $r_e \cdot d_e$ add up to 0 and never to 1.

Since $d_e = c(u, e) + c(v, e)$, each edge contributes two bits, one for each of its halves. Call $r_e \cdot c(v, e)$ the bit of the half-edge of $e$ at $v$. (It does not depend on which of the two other labels was picked as $c(v, e)$, because they differ by $f(e)$ and $r_e \cdot f(e) = 0$.) Then

$$ \sum_{e} r_e \cdot d_e \;=\; \text{the sum of the bits of all half-edges}, $$

and we can add those up one node at a time, as we counted orange halves on the ring.

What one node supplies

What the bit of a half-edge measures

It helps to see these bits before computing with them. Take a label $p$ and a code $r \neq 000$ with $r \cdot p = 0$. The eight paints fall into four pairs that differ by $p$, and the two paints of a pair have the same dot product with $r$. So $r$ splits the eight paints into two groups of four without splitting any pair, and the bit of a half-edge says in which group its pair of paints lies. The edge’s equation, tested with $r$, says that both halves lie in the same group.

  1. An edge labeled 011 can carry one of four pairs of paints, shown joined by gray lines.
  2. The code r = 100 looks only at the first column. It puts two whole pairs in each group.
  3. So does r = 011, which adds the last two columns.
  4. And so does r = 111. These three are the only codes other than 000 with r · 011 = 0.
The eight three-bit paints at the corners of a cube, joined in the four pairs that differ by 011. Each code r with r · 011 = 0 separates the paints into two groups of four, filled and hollow, without separating any pair.
Monocurl source
let p = 3
let cpos = |n| [-2.9 + 2.0 * bit(n, 0) + 1.15 * bit(n, 2), -1.5 + 2.0 * bit(n, 1) + 0.6 * bit(n, 2), 0]
let CUBE_E = filter(flatten(map(range(0, 8), |a| map(range(0, 8), |b| [a, b]))), |e| e[0] < e[1] and (xor(e[0], e[1]) == 1 or xor(e[0], e[1]) == 2 or xor(e[0], e[1]) == 4))
let PAIRS = filter(range(0, 8), |a| a < xor(a, p))
let Group = |r| map(range(0, 8), |n| [
    center{cpos(n)} fill{[PAPER, PAINT[n]][dot3(r, n)]} stroke{PAINT[n], 4} Circle(0.17, 48)
])
let Note3 = |r| [
    center{[2.45, 0.9, 0]} Math(str_join(["r = ", code3(r)]), 1.0),
    center{[2.45, 0.2, 0]} Math(str_join(["r \cdot ", code3(p), " = ", to_string(dot3(r, p))]), 0.9, SLATE),
    center{[2.45, -0.7, 0]} Cap("filled: r · paint = 1", 0.74, SLATE),
    center{[2.45, -1.12, 0]} Cap("hollow: r · paint = 0", 0.74, SLATE)
]
mesh grid = map(CUBE_E, |e| Seg(cpos(e[0]), cpos(e[1]), MIST, 2.5))
mesh pairs = map(PAIRS, |a| Seg(cpos(a), cpos(xor(a, p)), SLATE, 5))
mesh dots = map(range(0, 8), |n| center{cpos(n)} fill{PAINT[n]} stroke{PAINT[n], 4} Circle(0.17, 48))
mesh names = map(range(0, 8), |n| center{cpos(n) + [[-0.42, 0.42][bit(n, 0)], [-0.28, 0.28][bit(n, 1)], 0]} Cap(code3(n), 0.72, PAINT[n]))
mesh side = [center{[2.45, 0.5, 0]} Cap("pairs that differ", 0.76, SLATE), center{[2.45, 0.08, 0]} Cap("by 011", 0.76, SLATE)]

slide "An edge labeled 011 can carry one of four pairs of paints, shown joined by gray lines."
    play Wait(0.5)

slide "The code r = 100 looks only at the first column. It puts two whole pairs in each group."
    side = []
    play Fade(0.25, [&side])
    dots = Group(4)
    side = Note3(4)
    play Fade(0.6, [&dots, &side])

slide "So does r = 011, which adds the last two columns."
    side = []
    play Fade(0.25, [&side])
    dots = Group(3)
    side = Note3(3)
    play Fade(0.6, [&dots, &side])

slide "And so does r = 111. These three are the only codes other than 000 with r · 011 = 0."
    side = []
    play Fade(0.25, [&side])
    dots = Group(7)
    side = Note3(7)
    play Fade(0.6, [&dots, &side])

Uses the shared definitions for this lesson's figures.

With two-bit labels the picture is smaller and can be drawn on the graph. There are four paints, an edge labeled $p$ can carry one of two pairs, and exactly one of those pairs contains the paint 00. So for the one nonzero $r$ with $r \cdot p = 0$, the bit of a half-edge says whether its pair is the one without 00. Mark those halves with a tick across the edge.

  1. All slides are 00. Each node’s paints are 01, 10 and 11, so none of its halves has the purple paint 00: three ticks at every node. Every edge agrees.
  2. Slide the top left node by 01. Now it uses 00, on two of its edges, and only its third half has a tick. Two edges break, each with a tick on one half only.
  3. Slide the bottom right node by 11. It has one tick too. A node has one tick or three, so four nodes always have an even number in all.
$K_4$ with two-bit labels. Every node paints its half of each edge, and a tick marks a half-edge whose two paints do not include the purple paint 00. Each node has one tick or three, whatever its slide, so the total is always even.
Monocurl source
let Dots = |S| block {
    for (v in range(0, 4)) {
        for (k in inc(KE, v)) {
            let h = half(KE, KL, S, v, k)
            if (h[0] != 0 and h[1] != 0) {
                let q = lerp(KP[v], KP[far(KE, k, v)], 0.22)
                let n = perp(unit(KP[far(KE, k, v)] - KP[v])) * 0.2
                . Seg(q - n, q + n, INK, 4.5)
            }
        }
    }
}
let dots_at = |S, v| count(inc(KE, v), |k| half(KE, KL, S, v, k)[0] != 0 and half(KE, KL, S, v, k)[1] != 0)
let Picture = |S| [
    Ribbons(KP, KE, KL, S, 0.1, 6),
    map(broken(KE, KL, S), |k| center{lerp(KP[KE[k][0]], KP[KE[k][1]], [0.5, 0.4][k == 2 or k == 4])} fill{PAPER} stroke{ROSE, 3.5} Circle(0.11, 32)),
    Dots(S)
]
let Counts = |S| [
    center{[2.5, 1.45, 0]} Cap("ticks at each node", 0.76, SLATE),
    center{[2.5, 0.85, 0]} Math(str_join(map(range(0, 4), |v| to_string(dots_at(S, v))), " + "), 1.0),
    center{[2.5, 0.25, 0]} Math(str_join(["= ", to_string(sum(map(range(0, 4), |v| dots_at(S, v))))]), 1.0)
]
let Z4 = [0, 0, 0, 0]
let T1 = [1, 0, 0, 0]
let T2 = [1, 0, 3, 0]
mesh edges = Edges(KP, KE, SLATE, 2)
mesh picture = Picture(Z4)
mesh nodes = Nodes(KP)
mesh counts = Counts(Z4)
mesh moved = []

slide "All slides are 00. Each node's paints are 01, 10 and 11, so none of its halves has the purple paint 00: three ticks at every node. Every edge agrees."
    play Wait(0.5)

slide "Slide the top left node by 01. Now it uses 00, on two of its edges, and only its third half has a tick. Two edges break, each with a tick on one half only."
    counts = []
    play Fade(0.25, [&counts])
    picture = Picture(T1)
    moved = [center{KP[0]} fill{alpha{0} PAPER} stroke{INK, 3} Circle(0.27, 48)]
    counts = Counts(T1)
    play Fade(0.7, [&picture, &moved, &counts])

slide "Slide the bottom right node by 11. It has one tick too. A node has one tick or three, so four nodes always have an even number in all."
    counts = []
    play Fade(0.25, [&counts])
    picture = Picture(T2)
    moved .= center{KP[2]} fill{alpha{0} PAPER} stroke{INK, 3} Circle(0.27, 48)
    counts = Counts(T2)
    play Fade(0.7, [&picture, &moved, &counts])

Uses the shared definitions for this lesson's figures.

This is the ring again, with one difference. On an edge that agrees, the ticks come in twos, so a solution needs an even number of ticks. The nodes supply an odd number each, as the nodes of the ring supplied one orange half each. But a cubic graph always has an even number of nodes, so here the supply is even and nothing stands in the way. With three bits there are more selections than this one, and we need to know what a node supplies in each.

The count at a node

The proof needs one fact about codes.

Notice what happened to the slides. A node may slide its paints in eight ways, and the bit of each half-edge is computed without them: they cancelled when we added up the equations. On the ring every node supplied one orange half however it was flipped. Here a node supplies, as odd or even, as many as it has selected edges.

Every edge has two ends

The lesson on Euler paths uses the same count for all the edges of a graph.

Pause and try

A selection has ten selected edges. Knowing nothing else about it, what do the numbers of selected edges at the ten nodes add up to?

Show answerHide answer

Twenty. Each selected edge ends at two nodes and is counted once at each.

  1. A selection. The orange edges are the selected ones, each with its code. The scene has checked both rules against the labels of the flow.
  2. Beside each node, write the number of selected edges that end there.
  3. The ten numbers add up to twenty, twice the number of selected edges.
  4. At every node the bits of its three half-edges add up to an odd number exactly when the number beside it is odd. So the bits of all the half-edges add up to an even number.
A selection on the Petersen graph. Ten edges are selected, and the number of selected edges ending at each node is written beside it. The ten numbers add up to twenty, because each selected edge is counted at both of its ends.
Monocurl source
let PP = petersen([-1.4, -0.24, 0], 1.92, 1.1)
let R = [3, 1, 5, 5, 3, 0, 2, 4, 0, 6, 0, 0, 4, 0, 2]
let SEL = filter(range(0, 15), |k| R[k] != 0)
let ends_at = |v| count(inc(PE, v), |k| R[k] != 0)
# the two rules of a selection, checked in the scene
let RULE1 = count(range(0, 15), |k| dot3(R[k], PF[k]) == 0)
let RULE2 = count(range(0, 10), |v| node_sum(PE, R, v) == 0)
# the bits of the three half-edges at v, added up
let supplied = |v| imod(sum(map(inc(PE, v), |k| dot3(R[k], PF[filter(inc(PE, v), |j| j != k)[0]]))), 2)
let MATCH = count(range(0, 10), |v| supplied(v) == imod(ends_at(v), 2))
let num_pos = |v| {
    let c = [-1.4, -0.24, 0]
    let u = unit(PP[v] - c)
    if (v < 5) {
        return PP[v] + u * 0.28
    }
    return PP[v] + perp(u) * 0.3
}
let Row = |y, words, tex, col| [center{[2.55, y, 0]} Cap(words, 0.74, SLATE), center{[2.55, y - 0.45, 0]} Math(tex, 0.9, col)]
mesh edges = Edges(PP, PE, SLATE, 2)
mesh selected = map(SEL, |k| Seg(PP[PE[k][0]], PP[PE[k][1]], EMBER, 6.5))
mesh nodes = Nodes(PP)
mesh codes = map(SEL, |k| Label3(LabelPos(PP, k), R[k], [EMBER, EMBER, EMBER]))
mesh numbers = []
mesh panel = [
    Row(1.6, "rule 1 holds on", str_join([to_string(RULE1), "\text{ of } 15 \text{ edges}"]), INK),
    Row(0.55, "rule 2 holds at", str_join([to_string(RULE2), "\text{ of } 10 \text{ nodes}"]), INK)
]

slide "A selection. The orange edges are the selected ones, each with its code. The scene has checked both rules against the labels of the flow."
    play Wait(0.5)

slide "Beside each node, write the number of selected edges that end there."
    codes = []
    play Fade(0.3, [&codes])
    numbers = map(range(0, 10), |v| center{num_pos(v)} Math(to_string(ends_at(v)), 0.95, EMBER))
    play Fade(0.6, [&numbers])

slide "The ten numbers add up to twenty, twice the number of selected edges."
    panel = []
    play Fade(0.25, [&panel])
    panel = [
        Row(1.6, "sum of the numbers", to_string(sum(map(range(0, 10), ends_at))), EMBER),
        Row(0.55, "selected edges", to_string(len(SEL)), EMBER)
    ]
    play Fade(0.6, [&panel])

slide "At every node the bits of its three half-edges add up to an odd number exactly when the number beside it is odd. So the bits of all the half-edges add up to an even number."
    panel .= Row(-0.5, "odd and even agree at", str_join([to_string(MATCH), "\text{ of } 10 \text{ nodes}"]), LEAF)
    play Fade(0.6, [&panel])

Uses the shared definitions for this lesson's figures.

Everything is now in place.

The Petersen graph, solved

Solving the 45 equations for the Petersen graph, by elimination, tells each node how far to slide. In the next figure the nodes take up their slides one at a time.

The Petersen graph, with every node painting its half of each of its edges. A red ring marks an edge whose two halves disagree. One node at a time slides its three paints to the position the equations dictate, and at the end no edge is broken.
Monocurl source
let PP = petersen([0, -0.14, 0], 1.92, 1.1)
let State = |t| {
    let n = to_int(clamp(0, floor(t + 0.001), 10))
    let S = map(range(0, 10), |v| [0, SOLVED[v]][v < n])
    return [
        Ribbons(PP, PE, PF, S),
        map(broken(PE, PF, S), |k| center{(PP[PE[k][0]] + PP[PE[k][1]]) / 2} fill{PAPER} stroke{ROSE, 3.5} Circle(0.1, 32))
    ]
}
mesh edges = Edges(PP, PE, SLATE, 2)
mesh state = State(t: 0)
mesh nodes = Nodes(PP)

slide "solve"
    play Wait(1.0)
    state.t = 10
    play Lerp(8, [&state], linear)
    play Wait(2.5)

Uses the shared definitions for this lesson's figures.

With all ten slides in place, each paint can be followed along the graph.

  1. With these slides, the two halves of every edge carry the same two paints.
  2. Follow one paint. It runs along five edges and closes into a cycle.
  3. So does the next.
  4. Six paints are in use, each a single cycle of five edges, and every edge has two of them: 6 times 5 is 30, twice 15.
The Petersen graph with slides that solve the equations. The two halves of every edge agree, and following any one paint gives a cycle of five edges. Six paints are used.
Monocurl source
let PP = petersen([-1.4, -0.14, 0], 1.92, 1.1)
let PAIR = map(range(0, 15), |k| half(PE, PF, SOLVED, PE[k][0], k))
let USED = filter(range(0, 8), |p| count(PAIR, |h| h[0] == p or h[1] == p) > 0)
let CYC = map(USED, |p| cycles_of(PE, filter(range(0, 15), |k| PAIR[k][0] == p or PAIR[k][1] == p)))
let Bold = |i| map(CYC[i], |V| LoopLineWide(PP, PE, V, PAINT[USED[i]], 0.075, 11))
let Line = |i| [
    Seg([1.55, 1.75 - 0.5 * i, 0], [2.05, 1.75 - 0.5 * i, 0], PAINT[USED[i]], 6),
    center{[2.5, 1.75 - 0.5 * i, 0]} Cap(code3(USED[i]), 0.74, PAINT[USED[i]]),
    center{[3.2, 1.75 - 0.5 * i, 0]} Math(str_join(map(CYC[i], |V| to_string(len(V))), " + "), 0.82, PAINT[USED[i]])
]
mesh edges = Edges(PP, PE, SLATE, 2)
mesh bold = []
mesh ribbons = Ribbons(PP, PE, PF, SOLVED)
mesh nodes = Nodes(PP)
mesh panel = [
    center{[2.55, 0.3, 0]} Cap("edges whose two", 0.76, SLATE),
    center{[2.55, -0.12, 0]} Cap("halves disagree", 0.76, SLATE),
    center{[2.55, -0.75, 0]} Math(str_join([to_string(len(broken(PE, PF, SOLVED))), "\text{ of } 15"]), 1.05, LEAF)
]

slide "With these slides, the two halves of every edge carry the same two paints."
    play Wait(0.5)

slide "Follow one paint. It runs along five edges and closes into a cycle."
    panel = []
    play Fade(0.25, [&panel])
    bold = Bold(0)
    panel = [Line(0)]
    play Fade(0.7, [&bold, &panel])

slide "So does the next."
    bold = Bold(1)
    panel .= Line(1)
    play Fade(0.7, [&bold, &panel])

slide "Six paints are in use, each a single cycle of five edges, and every edge has two of them: 6 times 5 is 30, twice 15."
    bold = []
    panel = map(range(0, len(USED)), Line)
    panel .= center{[2.55, -1.55, 0]} Cap("paint, and edges in its cycles", 0.7, SLATE)
    play Fade(0.7, [&bold, &panel])

Uses the shared definitions for this lesson's figures.

A computer search over all slides finds 16 solutions for this labeling. Adding the same code to every slide turns a solution into another, which accounts for a factor of 8, so there are two kinds. One is shown. The other uses only five paints, whose cycles have 5, 5, 5, 6 and 9 edges.

The whole proof

Who proved what, and what is open

George Szekeres stated the conjecture in 1973 for graphs with three edges at every node, and Paul Seymour stated it in 1979 for all bridgeless graphs. Alon Itai and Michael Rodeh gave an equivalent form in 1978. In a 1987 letter W. T. Tutte wrote that the question had been passed around in conversation long before anyone printed it, and OpenAI’s note also credits him. The reduction to cubic graphs and the painting from a 3-edge-coloring are classical. The flow theorem is due to P. A. Kilpatrick (1975) and François Jaeger (1976, 1979). In 1983 Bermond, Jackson and Jaeger built from the flow seven sets of cycles covering every edge four times.

The new step is the theorem from a flow to a painting: read each label as the difference between two paints, let every node slide its triangle of paints, and notice that agreement between neighbors is a matter of adding bits. OpenAI published it in July 2026 in a note of under three pages, which states that the proof is entirely due to its model GPT 5.6 Sol Ultra. The prompt, also published, told the model to assume that a complete proof exists and to keep working for at least eight hours. OpenAI has also released a formalization of the theorem, for graphs as defined at the start of this essay, that the Lean proof assistant checks. Jim Geelen and Sang-il Oum each wrote the argument out in their own way within weeks; the symmetric choice of paints at a node and the form of the count at a node used here follow Oum’s exposition. A paper by Shiva Kintali, posted the same week as OpenAI’s note and written with OpenAI’s Codex, gives a proof along the same lines starting from Seymour’s 6-flow theorem.

The subject is not finished. This construction can use up to eight paints. Uldis Celmins and Myriam Preissmann conjectured in the 1980s that five are always enough, and that five cycle double cover conjecture is still open.

The idea worth keeping is the one the ring showed. Solve the problem at every node separately. Write what neighbors must agree on as differences. Differences add up, and what adds up can be counted.

Check yourself

In a selection, how many of the three edges at a node can be selected?

Problems

The problems follow the order of the argument. Problems 1 to 7 are about covers, bridges and paint, problems 8 to 15 about codes, labels and flows, and problems 16 to 22 about the equations and the count at a node. In each group the early ones repeat moves from the text and the later ones ask for short proofs.

Problem 1

A graph has nodes $a, b, c, d, e$ and edges $ab$, $bc$, $ca$, $cd$, $de$, $ec$. Does it have a bridge? Find a cycle double cover or explain why there is none.

Show solutionHide solution

The graph is two triangles, $abc$ and $cde$, sharing the node $c$. Every edge lies on one of the triangles, so by the theorem on bridges and cycles there is no bridge. Listing each triangle twice gives a cycle double cover: every edge is on its own triangle, which appears twice. No other cover exists, because the only cycles in this graph are the two triangles (a cycle cannot pass through $c$ twice).

Problem 2

The prism has nodes $a_1, a_2, a_3, b_1, b_2, b_3$ and edges $a_1a_2$, $a_2a_3$, $a_3a_1$, $b_1b_2$, $b_2b_3$, $b_3b_1$, $a_1b_1$, $a_2b_2$, $a_3b_3$. Find a 3-edge-coloring, and write down the cycles of the three paints it gives.

Show solutionHide solution

Color $a_1a_2$ with 1, $a_2a_3$ with 2 and $a_3a_1$ with 3. Then $a_1$ has colors 1 and 3, so $a_1b_1$ gets 2; similarly $a_2b_2$ gets 3 and $a_3b_3$ gets 1. At $b_1$ color 2 is taken and at $b_2$ color 3, so $b_1b_2$ gets 1; in the same way $b_2b_3$ gets 2 and $b_3b_1$ gets 3. Every node now has all three colors. Colors 1 and 2 form the cycle $a_1 a_2 a_3 b_3 b_2 b_1$. Colors 1 and 3 form $a_1 a_2 b_2 b_1 b_3 a_3$. Colors 2 and 3 form $a_1 a_3 a_2 b_2 b_3 b_1$. Each is a single cycle of six edges, and $3 \times 6 = 18 = 2 \times 9$.

Problem 3

In a cycle double cover of a graph with $m$ edges, what do the lengths of the cycles add up to? The Petersen graph has 15 edges and no cycle with fewer than 5 edges. What is the largest number of cycles a cover of it could have, and what would such a cover consist of?

Show solutionHide solution

Each edge is counted once by each of the two cycles through it, so the lengths add up to $2m$. For the Petersen graph that is 30. Every cycle has at least 5 edges, so there are at most $30 / 5 = 6$ cycles, and a cover with six cycles would consist of six cycles of exactly five edges. (The painting of the Petersen graph found by solving the equations is one.)

Problem 4

Show that a cubic graph has an even number of nodes, and that in a 3-edge-coloring each color is on exactly half as many edges as there are nodes.

Show solutionHide solution

Count the ends of edges. Each of the $n$ nodes has three, so there are $3n$, and each edge has two, so $3n$ is twice the number of edges. Hence $3n$ is even, and so $n$ is even. In a 3-edge-coloring each node is an end of exactly one edge of a given color, and each such edge has two ends, so there are $n/2$ edges of that color.

Problem 5

A cubic graph has a bridge. Without using the theorem of Szekeres, show directly that it has no 3-edge-coloring. (Hint: look at one side of the bridge and count the ends of the edges of one color.)

Show solutionHide solution

Delete the bridge, colored orange say, and let $S$ be the set of nodes on one side. Consider the blue edges. None of them is the bridge, so every blue edge has both ends in $S$ or neither. Every node of $S$ is an end of exactly one blue edge, so the number of nodes in $S$ is twice the number of blue edges inside $S$, an even number. Now count orange edges the same way. Every node of $S$ is an end of exactly one orange edge, and all of these lie inside $S$ except the bridge, which has one end in $S$. So the number of nodes in $S$ is twice the number of orange edges inside $S$, plus one, an odd number. It cannot be both.

Problem 6

In the reduction, a cycle of the new graph $G'$ may pass through both $v'$ and $v$ without using the new edge. Take $G$ to be two triangles sharing a node $v$, split $v$ with one edge of each triangle on each new node, and find a cycle double cover of $G'$ that contains such a cycle. What does it become when the new edge is shrunk?

Show solutionHide solution

Call the triangles $v a b$ and $v c d$, and let $v'$ take the edges to $a$ and $c$, while $v$ keeps the edges to $b$ and $d$. In $G'$ the route $v', a, b, v, d, c$ and back to $v'$ is a cycle of six edges that uses every edge except the new one. The routes $v', a, b, v$ and $v', c, d, v$, each closed by the new edge, are two cycles of four edges. These three cycles are a cover: the new edge is on both four-edge cycles, and every other edge is on the six-edge cycle and on one four-edge cycle. Shrinking the new edge turns the four-edge cycles into the triangles $vab$ and $vcd$, and turns the six-edge cycle into a closed route through $v$ twice, which is cut at $v$ into the same two triangles. The result is each triangle listed twice.

Problem 7

Suppose a cubic graph has a painting that uses only three paints in total. Show that the graph has a 3-edge-coloring. Conclude that any painting of the Petersen graph uses at least four paints.

Show solutionHide solution

By the theorem on three paints at a node, exactly three paints meet at each node, so with only three paints available all three appear at every node, each on two of its three edges. Every edge has two of the three paints, so it is missing exactly one; color the edge by the paint it is missing. At a node, each paint is on two edges and missing from one, and the three paints are on three different pairs of edges, so they are missing from three different edges. Thus the three edges at each node get three different colors, which is a 3-edge-coloring. The Petersen graph has none, so no painting of it uses only three paints. A painting cannot use fewer than three either, since three paints meet at every node.

Problem 8

Compute $1011 + 0110$ and $1011 + 0110 + 1101$. Find the code $c$ with $1011 + c = 0110$.

Show solutionHide solution

Column by column, $1011 + 0110 = 1101$. Adding $1101$ to that gives $0000$, since a code added to itself is zero. For the last part, add $1011$ to both sides: $c = 1011 + 0110 = 1101$.

Problem 9

In a nowhere-zero three-bit flow, two of the labels at a node are 110 and 011. Find the third label. Then list the coats of the three edges when the node’s slide is 000, and again when it is 101.

Show solutionHide solution

The third label is $110 + 011 = 101$. With slide 000 the paints are the labels: the edge labeled 110 gets $\{011, 101\}$, the edge labeled 011 gets $\{110, 101\}$, and the edge labeled 101 gets $\{110, 011\}$. With slide 101, add 101 to every paint: the paints become $110 + 101 = 011$, $011 + 101 = 110$ and $101 + 101 = 000$, so the edge labeled 110 gets $\{110, 000\}$, the edge labeled 011 gets $\{011, 000\}$, and the edge labeled 101 gets $\{011, 110\}$. In each case the two coats differ by the label.

Problem 10

The cube has a painting with three paints, found from its three kinds of edges: horizontal, vertical and slanted. Name the paints 00, 01, 10 so that horizontal edges carry $\{00, 01\}$, vertical edges $\{00, 10\}$ and slanted edges $\{01, 10\}$. Find all the labels and check the sum at a node.

Show solutionHide solution

Horizontal edges get $00 + 01 = 01$, vertical edges $00 + 10 = 10$ and slanted edges $01 + 10 = 11$. Every node of the cube has one edge of each kind, so its labels are 01, 10 and 11, which add up to 00.

Problem 11

Show that no cubic graph has a nowhere-zero one-bit flow.

Show solutionHide solution

The only one-bit code other than 0 is 1, so every label would be 1. At a node the three labels would add up to $1 + 1 + 1 = 1$, not 0.

Problem 12

Take a nowhere-zero three-bit flow on a cubic graph and let $S$ be the set of edges whose first bit is 1. Show that the edges of $S$ form cycles, no two of which share a node.

Show solutionHide solution

The labels at a node add up to 000, so in the first column the three bits add up to 0. Hence an even number of the three edges at the node are in $S$: two or none. That is the condition a single paint satisfies in a painting, and the walking argument from the theorem on paint and cycles applies word for word: start on an edge of $S$, leave each node by its other edge of $S$, and the walk closes into a cycle; repeat on what is left. Two of these cycles cannot share a node, because that node would meet four edges of $S$.

Problem 13

$K_4$ has nodes $a, b, c, d$. Take the spanning tree made of the three edges at $d$. Find the cycle that each of the other three edges closes with the tree, and the set of edges that lie on an odd number of those cycles.

Show solutionHide solution

The edge $ab$ closes the cycle $a, b, d$; the edge $bc$ closes $b, c, d$; and $ca$ closes $c, a, d$. The tree edge $da$ lies on the first and third cycles, $db$ on the first and second, and $dc$ on the second and third, so each tree edge is used twice, an even number. Each of $ab$, $bc$, $ca$ is used once. The edges with an odd count are $ab$, $bc$, $ca$, the triangle that avoids $d$. It meets $a$, $b$, $c$ twice and $d$ not at all, and it contains every edge outside the tree, as the sketch predicts.

Problem 14

(The 1983 result.) A nowhere-zero three-bit flow gives three sets of edges $S_1, S_2, S_3$, one for each column. For each nonempty collection of columns, form the set of edges that have an odd number of 1s in those columns. That makes seven sets. Show that every edge lies in exactly four of them.

Show solutionHide solution

Fix an edge and let its label have $j$ ones, where $j$ is 1, 2 or 3. A collection of columns counts the edge exactly when it contains an odd number of the $j$ columns where the label has a 1. The number of ways to pick an odd number of things out of $j$ is $2^{j-1}$ (for $j = 1, 2, 3$ that is 1, 2, 4: one way from one; either single one from two; any single one or all three from three). The remaining $3 - j$ columns may be included or not, in $2^{3-j}$ ways. So the edge is counted by $2^{j-1} \times 2^{3-j} = 4$ collections, and none of them is empty since each contains at least one column.

Problem 15

Show that a graph with a bridge has no nowhere-zero flow, with any number of bits. (Hint: add up the equations at all the nodes on one side of the bridge.)

Show solutionHide solution

Suppose the labels add up to 0 at every node, and let $e$ be a bridge. Deleting $e$ leaves no route between its ends, so the nodes reachable from one end form a set $A$ that does not contain the other end, and $e$ is the only edge with exactly one end in $A$. Add up, over all nodes of $A$, the sum of the labels at that node. Each of these sums is 0, so the total is 0. In the total, an edge with both ends in $A$ appears twice and cancels, and $e$ appears once. So the total equals the label of $e$, which is therefore 0. The flow is not nowhere-zero. Together with Jaeger’s theorem this says that a graph has a nowhere-zero three-bit flow exactly when it is bridgeless.

Problem 16

Three unknown bits satisfy $a + b = 1$, $b + c = 1$ and $c + a = 0$. Which selections of these equations make all the unknowns cancel? Does the system have a solution? What changes if the third equation is $c + a = 1$?

Show solutionHide solution

Each unknown is in exactly two equations, so it cancels only if both or neither of those are taken; that forces all three equations or none. The sum of all three is $0 = 1 + 1 + 0 = 0$, so by the theorem on the only obstacle there is a solution, for instance $a = 0$, $b = 1$, $c = 0$. With $c + a = 1$ the sum is $0 = 1$ and there is none: this is a ring of three.

Problem 17

Compute $101 \cdot 110$ and $111 \cdot 011$. List every code $r$ with $r \cdot 011 = 0$.

Show solutionHide solution

$101 \cdot 110$ adds the first and third digits of 110: $1 + 0 = 1$. $111 \cdot 011$ adds all the digits of 011: $0 + 1 + 1 = 0$. For $r \cdot 011 = 0$, the last two digits of $r$ must be equal, and the first is free: 000, 100, 011, 111.

Problem 18

An edge $e$ from $u$ to $v$ has label 011. The other two labels at $u$ are 110 and 101, and the other two at $v$ are 001 and 010. If $s_u = 000$, which slides $s_v$ make the two halves of $e$ agree?

Show solutionHide solution

Take $c(u, e) = 110$ and $c(v, e) = 001$, so $d_e = 110 + 001 = 111$. The equation $s_u + s_v + \varepsilon_e f(e) = d_e$ with $s_u = 000$ gives $s_v = 111$ when $\varepsilon_e = 0$ and $s_v = 111 + 011 = 100$ when $\varepsilon_e = 1$. Check: the half at $u$ carries $\{110, 101\}$. With $s_v = 111$ the half at $v$ carries $\{111 + 001, 111 + 010\} = \{110, 101\}$, and with $s_v = 100$ it carries $\{101, 110\}$. The other six slides fail.

Problem 19

In a selection, show that no node has exactly one selected edge. Deduce that the selected edges form a graph in which every node has degree 0, 2 or 3, and that the number of nodes of degree 3 is even.

Show solutionHide solution

If exactly one of the three codes at a node were different from 000, the three would add up to that code and not to 000, against the second rule. So the number of selected edges at a node is 0, 2 or 3. By the handshake lemma these numbers add up to an even number. The nodes with 0 or 2 contribute an even amount, so the nodes with 3 contribute an even amount as well, which requires an even number of them.

Problem 20

At a node with labels $x = 110$, $y = 011$, $z = 101$, find the code $m \neq 000$ with $m \cdot x = m \cdot y = 0$. Then list all triples $r_a, r_b, r_c$ that obey the two rules of a selection at this node and have $\lambda = 0$.

Show solutionHide solution

$m \cdot 110 = 0$ says the first two digits of $m$ are equal, and $m \cdot 011 = 0$ says the last two are equal, so $m = 111$. (Then $m \cdot z = 1 + 1 = 0$ too.) With $\lambda = 0$ each code is 000 or 111 and the three add up to 000, so the triples are $(000, 000, 000)$, $(111, 111, 000)$, $(111, 000, 111)$ and $(000, 111, 111)$. They have 0 or 2 nonzero members, as the count at a node says.

Problem 21

On the ring of five, each node supplied one orange half and the argument ended in $0 = 1$. In the theorem from a flow to a painting, each node supplies an odd or even number as well, and the argument ends in $0 = 0$. State precisely what is counted in each case and why one count can be odd and the other cannot.

Show solutionHide solution

On the ring, the total is the number of orange half-edges. Each node colors exactly one half orange, so the total is the number of nodes, which may be odd. In the theorem, the total is the sum of the bits of all half-edges for a given selection. By the count at a node, each node contributes the number of selected edges ending at it, as odd or even, so the total has the parity of the number of ends of selected edges. Each selected edge has two ends, so that number is twice the number of selected edges and is always even. One total counts nodes and the other counts ends of edges.

Problem 22

Where did the proof use that the labels have three bits? Examine the theorem on blind codes for four-bit codes $x \neq y$, and show by an example that the last step of the count at a node fails as written.

Show solutionHide solution

The count at a node used that only 000 and one other code $m$ have dot product 0 with both $x$ and $y$. With four bits, the same class-matching argument gives four classes of size $16 / 4 = 4$, so four codes are blind to $x$ and $y$: 0000, $m_1$, $m_2$ and $m_1 + m_2$. For example with $x = 1000$, $y = 0100$ and $z = 1100$, the blind codes are 0000, 0010, 0001, 0011. Take $r_a = 0010$, $r_b = 0001$, $r_c = 0011$. They add up to 0000 and each has dot product 0 with all of $x, y, z$, so both rules hold and $\lambda = 0$. But all three are nonzero, an odd number, while the half-edge bits add up to $\lambda = 0$. So the argument as given does not go through for four-bit labels, and three bits are what Jaeger’s theorem provides.

Shared definitions used by this lesson's figures
# house kit: prepended to every essay figure after the std imports

# palette. AZURE and EMBER are the two actors, LEAF marks an answer, ROSE a problem
let PAPER = [1, 1, 1, 1]
let INK = [0.08, 0.09, 0.11, 1]
let SLATE = [0.40, 0.44, 0.50, 1]
let MIST = [0.85, 0.86, 0.88, 1]
let AZURE = [0.13, 0.42, 0.68, 1]
let EMBER = [0.89, 0.42, 0.12, 1]
let LEAF = [0.13, 0.55, 0.36, 1]
let ROSE = [0.78, 0.22, 0.24, 1]
let PLUM = [0.47, 0.30, 0.66, 1]
let GOLD = [0.85, 0.62, 0.08, 1]

background = PAPER

# an opaque pastel of col; use for fills so overlaps do not darken
let tint = |col, amount = 0.18| [
    1 - (1 - col[0]) * amount,
    1 - (1 - col[1]) * amount,
    1 - (1 - col[2]) * amount,
    1
]

# text: Cap for labels, Note for quiet labels, Math for typeset maths
let Cap = |txt, size = 0.75, col = INK| color{col} Text(txt, size, "Charter")
let Note = |txt, size = 0.65| color{SLATE} Text(txt, size, "Charter")
let Math = |tex, size = 0.9, col = INK| color{col} Tex(tex, size)

# a paper rectangle to sit behind text that crosses lines
let Plate = |pos, size| center{pos} fill{PAPER} stroke{alpha{0} PAPER, 0} Rect(size)

# shapes with fill and stroke both set, so no stray black appears
let Pt = |pos, col = INK, r = 0.075| center{pos} fill{col} stroke{col, 1} Circle(r, 48)
let Ring = |pos, col = INK, r = 0.1, w = 3| center{pos} fill{PAPER} stroke{col, w} Circle(r, 48)
let Seg = |a, b, col = INK, w = 3| stroke{col, w} Line(a, b)
let Path = |pts, col = INK, w = 3| stroke{col, w} Polyline(pts)
let Region = |pts, col| fill{col} stroke{alpha{0} col, 0} Polygon(pts)
let Outline = |pts, col = INK, w = 3| fill{alpha{0} PAPER} stroke{col, w} Polygon(pts)
let Shape = |pts, col, amount = 0.18, w = 3| fill{tint(col, amount)} stroke{col, w} Polygon(pts)
let Disc = |pos, r, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Circle(r, 96)
let Box = |pos, size, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Rect(size)
# a line from a to b ending in a small solid arrowhead
let Tip = |a, b, col = INK, w = 3, head = 0.17| {
    let d = normalize([b[0] - a[0], b[1] - a[1], 0])
    let n = [-d[1], d[0], 0]
    let base = [b[0] - d[0] * head, b[1] - d[1] * head, 0]
    return [
        stroke{col, w} Line(a, base),
        fill{col} stroke{col, 1} Polygon([
            b,
            [base[0] + n[0] * head * 0.42, base[1] + n[1] * head * 0.42, 0],
            [base[0] - n[0] * head * 0.42, base[1] - n[1] * head * 0.42, 0]
        ])
    ]
}

# plots. a frame is [x0, x1, y0, y1, centre, width, height]: a data window and the scene box it fills
let Frame = |x0, x1, y0, y1, c = [0, 0, 0], w = 6.4, h = 3.6| [x0, x1, y0, y1, c, w, h]
let fx = |fr, x| fr[4][0] + (x - (fr[0] + fr[1]) / 2) * fr[5] / (fr[1] - fr[0])
let fy = |fr, y| fr[4][1] + (y - (fr[2] + fr[3]) / 2) * fr[6] / (fr[3] - fr[2])
let at = |fr, x, y| [fx(fr, x), fy(fr, y), 0]

# axes through the data origin (or the nearest edge of the window), with optional names
let Axes = |fr, x_name = "", y_name = "", col = SLATE| {
    let ox = clamp(fr[0], 0, fr[1])
    let oy = clamp(fr[2], 0, fr[3])
    var parts = [
        Tip(at(fr, fr[0], oy), [fx(fr, fr[1]) + 0.25, fy(fr, oy), 0], col, 2.5, 0.15),
        Tip(at(fr, ox, fr[2]), [fx(fr, ox), fy(fr, fr[3]) + 0.25, 0], col, 2.5, 0.15)
    ]
    if (x_name != "") {
        parts .= center{[fx(fr, fr[1]) + 0.5, fy(fr, oy), 0]} Math(x_name, 0.8, col)
    }
    if (y_name != "") {
        parts .= center{[fx(fr, ox), fy(fr, fr[3]) + 0.5, 0]} Math(y_name, 0.8, col)
    }
    return parts
}

# tick marks with numbers at the listed data values
let XTicks = |fr, xs, col = SLATE, places = 0| block {
    let oy = fy(fr, clamp(fr[2], 0, fr[3]))
    for (x in xs) {
        . Seg([fx(fr, x), oy - 0.07, 0], [fx(fr, x), oy + 0.07, 0], col, 2)
        . center{[fx(fr, x), oy - 0.33, 0]} Math(to_string(x, places), 0.72, col)
    }
}
let YTicks = |fr, ys, col = SLATE, places = 0| block {
    let ox = fx(fr, clamp(fr[0], 0, fr[1]))
    for (y in ys) {
        . Seg([ox - 0.07, fy(fr, y), 0], [ox + 0.07, fy(fr, y), 0], col, 2)
        . center{[ox - 0.36, fy(fr, y), 0]} Math(to_string(y, places), 0.72, col)
    }
}

# faint grid lines at the listed data values
let GridLines = |fr, xs, ys, col = MIST| block {
    for (x in xs) {
        . Seg(at(fr, x, fr[2]), at(fr, x, fr[3]), col, 1.5)
    }
    for (y in ys) {
        . Seg(at(fr, fr[0], y), at(fr, fr[1], y), col, 1.5)
    }
}

# the graph of f over [a, b], drawn in the frame. values are clamped to the window
let Graph = |fr, f, a, b, col = AZURE, w = 4, n = 160|
    stroke{col, w} Polyline(map(sample(a, b, n), |x| at(fr, x, clamp(fr[2], f(x), fr[3]))))

# the region between the graph of f and the x axis over [a, b]
let AreaUnder = |fr, f, a, b, col = AZURE, amount = 0.2, n = 80| {
    var pts = [at(fr, a, 0)]
    for (x in sample(a, b, n)) {
        pts .= at(fr, x, clamp(fr[2], f(x), fr[3]))
    }
    pts .= at(fr, b, 0)
    return fill{tint(col, amount)} stroke{alpha{0} col, 0} Polygon(pts)
}

# three more paint colours beside the kit palette
let P_TEAL = [0.05, 0.58, 0.55, 1]
let P_PINK = [0.86, 0.36, 0.62, 1]
let P_BROWN = [0.50, 0.33, 0.16, 1]
# one paint colour for each code 000 .. 111
let PAINT = [PLUM, P_TEAL, GOLD, P_PINK, AZURE, P_BROWN, EMBER, LEAF]
# one colour for each column of a three-bit code
let STRIPE = [AZURE, EMBER, LEAF]
# colours of a 3-edge-colouring: 1 orange, 2 blue, 3 green
let ECOL = [SLATE, EMBER, AZURE, LEAF]

let vlen = |v| sqrt(v[0] * v[0] + v[1] * v[1])
let unit = |v| v / vlen(v)
let perp = |v| [0 - v[1], v[0], 0]
let ang = |v| arctan2(v[1], v[0])
let turn = |a, b| mod(b - a + 2 * TAU, TAU)
let imod = |a, n| a - n * (a // n)

# codes are the integers 0 .. 7, added column by column with no carry
let XT = [[0, 1, 2, 3, 4, 5, 6, 7], [1, 0, 3, 2, 5, 4, 7, 6], [2, 3, 0, 1, 6, 7, 4, 5], [3, 2, 1, 0, 7, 6, 5, 4], [4, 5, 6, 7, 0, 1, 2, 3], [5, 4, 7, 6, 1, 0, 3, 2], [6, 7, 4, 5, 2, 3, 0, 1], [7, 6, 5, 4, 3, 2, 1, 0]]
let xor = |a, b| XT[a][b]
let bit = |n, i| imod(n // [1, 2, 4][i], 2)
let code3 = |n| str_join([to_string(bit(n, 2)), to_string(bit(n, 1)), to_string(bit(n, 0))])
let code2 = |n| str_join([to_string(bit(n, 1)), to_string(bit(n, 0))])

let Edges = |P, E, col = SLATE, w = 3| map(E, |e| Seg(P[e[0]], P[e[1]], col, w))
let Nodes = |P, col = INK, r = 0.085| map(P, |p| Pt(p, col, r))
let other = |e, v| [e[0], e[1]][e[0] == v]
let nbrs = |E, v| map(filter(E, |e| e[0] == v or e[1] == v), |e| other(e, v))
let inc = |E, v| filter(range(0, len(E)), |k| E[k][0] == v or E[k][1] == v)
let far = |E, k, v| other(E[k], v)

let sector_sign = |P, E, b, a, c| {
    let ua = ang(P[a] - P[b])
    let phi = turn(ua, ang(P[c] - P[b]))
    let rest = filter(nbrs(E, b), |x| x != a and x != c)
    let inside = count(rest, |x| turn(ua, ang(P[x] - P[b])) < phi)
    if (len(rest) > 0 and inside == 0) { return 1 }
    if (len(rest) > 0 and inside == len(rest)) { return -1 }
    return [-1, 1][phi < PI]
}
let corner_pt = |c, a0, phi, d| {
    let r = d / max(0.3, sin(phi / 2))
    return [c[0] + r * cos(a0 + phi / 2), c[1] + r * sin(a0 + phi / 2), 0]
}
# corners of a ribbon that follows the cycle through the nodes V at distance d from its edges.
# where the ribbon has to change sides, it crosses the edge at its middle, over a stretch of 2 * xgap * d
let loop_pts_k = |P, E, V, d, xgap| block {
    let n = len(V)
    let S = map(range(0, n), |i| sector_sign(P, E, V[i], V[imod(i + n - 1, n)], V[imod(i + 1, n)]))
    for (i in range(0, n)) {
        let j = imod(i + 1, n)
        let b = P[V[i]]
        let c = P[V[j]]
        let ua = ang(P[V[imod(i + n - 1, n)]] - b)
        let uc = ang(c - b)
        if (S[i] == 1) {
            . corner_pt(b, ua, turn(ua, uc), d)
        }
        if (S[i] == -1) {
            . corner_pt(b, uc, turn(uc, ua), d)
        }
        if (S[j] != S[i]) {
            let t = unit(c - b)
            let m = (b + c) / 2
            . m - t * (xgap * d) - perp(t) * (d * S[i])
            . m + t * (xgap * d) - perp(t) * (d * S[j])
        }
    }
}
let loop_pts = |P, E, V, d| loop_pts_k(P, E, V, d, 2.2)
# a closed polyline that starts and ends in the middle of a side, so the join does not show
let closed = |pts| flatten([[(pts[len(pts) - 1] + pts[0]) / 2], pts, [(pts[len(pts) - 1] + pts[0]) / 2]])
let LoopLine = |P, E, V, col, d = 0.1, w = 6| Path(closed(loop_pts(P, E, V, d)), col, w)
# the same ribbon with a longer crossing, for thick lines
let LoopLineWide = |P, E, V, col, d, w| Path(closed(loop_pts_k(P, E, V, d, 3.5)), col, w)
let dense = |pts, step = 0.05| block {
    for (i in range(0, len(pts) - 1)) {
        let n = to_int(max(1, ceil(vlen(pts[i + 1] - pts[i]) / step)))
        for (j in range(0, n)) {
            . lerp(pts[i], pts[i + 1], j / n)
        }
    }
    . pts[len(pts) - 1]
}
let part = |pts, u| {
    let m = len(pts) - 1
    let x = clamp(0, u, 1) * m
    let k = to_int(min(floor(x), m - 1))
    var out = take(pts, k + 1)
    if (x - k > 0.02) {
        out .= lerp(pts[k], pts[k + 1], x - k)
    }
    return out
}
let tip_of = |pts, u| {
    let m = len(pts) - 1
    let x = clamp(0, u, 1) * m
    let k = to_int(min(floor(x), m - 1))
    return lerp(pts[k], pts[k + 1], x - k)
}
let Grown = |pts, u, col, w| block {
    if (u * (len(pts) - 1) > 1) {
        . Path(part(pts, u), col, w)
    }
}
# the cycles formed by a set S of edges that meets every node twice or not at all, as lists of nodes
let cycles_of = |E, S| {
    var left = S
    var out = []
    while (len(left) > 0) {
        let k0 = left[0]
        var seq = [E[k0][0]]
        var cur = E[k0][1]
        var prev = k0
        var used = [k0]
        while (cur != seq[0]) {
            seq .= cur
            let c = cur
            let pk = prev
            let pool = left
            let nk = filter(pool, |k| k != pk and (E[k][0] == c or E[k][1] == c))[0]
            cur = other(E[nk], c)
            prev = nk
            used .= nk
        }
        let u = used
        left = filter(left, |k| count(u, |x| x == k) == 0)
        out .= seq
    }
    return out
}

# the edges at v in counterclockwise order
let around = |P, E, v| {
    let I = inc(E, v)
    let a0 = ang(P[far(E, I[0], v)] - P[v])
    return sort(I, |k| turn(a0, ang(P[far(E, k, v)] - P[v])))
}
# with labels F and slides S, the half of edge k at node v carries S[v] plus the labels of the other two edges at v
let half = |E, F, S, v, k| map(filter(inc(E, v), |j| j != k), |j| xor(S[v], F[j]))
let same = |a, b| (a[0] == b[0] and a[1] == b[1]) or (a[0] == b[1] and a[1] == b[0])
let agree = |E, F, S, k| same(half(E, F, S, E[k][0], k), half(E, F, S, E[k][1], k))
let broken = |E, F, S| filter(range(0, len(E)), |k| not agree(E, F, S, k))
# the stretch of ribbon for paint p beside edge k, seen from node v, listed from the far end inwards.
# sgn +1 is the counterclockwise side. where the two halves of the edge agree the ribbon runs to the
# middle, crossing over there if the other node has this paint on the other side; otherwise it stops short
let tail = |P, E, F, S, v, k, sgn, p, d| {
    let w = far(E, k, v)
    let c = P[v]
    let u = unit(P[w] - c)
    let L = vlen(P[w] - c)
    let n = perp(u) * (sgn * d)
    if (not agree(E, F, S, k)) {
        return [c + u * (L * 0.4) + n]
    }
    let O = around(P, E, w)
    let m = filter([0, 1, 2], |x| O[x] == k)[0]
    let wsgn = [-1, 1][xor(S[w], F[O[[2, 0, 1][m]]]) == p]
    if (wsgn != sgn) {
        return [c + u * (L * 0.5) + n]
    }
    return [c + u * (L * 0.5), c + u * (L * 0.5 - 3.5 * d) + n]
}
# every node paints its three sectors: the sector between two edges gets S[v] plus the label of the third
let Ribbons = |P, E, F, S, d = 0.075, w = 5| block {
    for (v in range(0, len(P))) {
        let O = around(P, E, v)
        for (m in [0, 1, 2]) {
            let i = O[m]
            let j = O[[1, 2, 0][m]]
            let p = xor(S[v], F[O[[2, 0, 1][m]]])
            let ui = ang(P[far(E, i, v)] - P[v])
            let uj = ang(P[far(E, j, v)] - P[v])
            . Path(flatten([
                tail(P, E, F, S, v, i, 1, p, d),
                [corner_pt(P[v], ui, turn(ui, uj), d)],
                reverse(tail(P, E, F, S, v, j, -1, p, d))
            ]), PAINT[p], w)
        }
    }
}

# the cube pressed flat: outer face, inner face, and the four faces between them
let CP = [[-2.5, 1.72, 0], [-2.5, -1.72, 0], [2.5, -1.72, 0], [2.5, 1.72, 0], [-1.0, 0.68, 0], [-1.0, -0.68, 0], [1.0, -0.68, 0], [1.0, 0.68, 0]]
let CE = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]]
let CFACES = [[0, 1, 2, 3], [4, 5, 6, 7], [0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]
let CLOOPS = map(CFACES, |f| dense(closed(loop_pts(CP, CE, f, 0.1))))

# the Petersen graph: outer ring 0..4, inner nodes 5..9, spokes i to i + 5, inner star
let ring_pts = |n, R, c, ph| map(range(0, n), |i| [c[0] + R * cos(ph + TAU * i / n), c[1] + R * sin(ph + TAU * i / n), 0])
let petersen = |c, R, r| flatten([ring_pts(5, R, c, PI / 2), ring_pts(5, r, c, PI / 2)])
let PE = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 0], [0, 5], [1, 6], [2, 7], [3, 8], [4, 9], [5, 7], [6, 8], [7, 9], [8, 5], [9, 6]]
# a nowhere-zero three-bit flow on the Petersen graph
let PF = [7, 6, 7, 5, 3, 4, 1, 1, 2, 6, 3, 5, 2, 7, 4]
let node_sum = |E, F, v| xor(xor(F[inc(E, v)[0]], F[inc(E, v)[1]]), F[inc(E, v)[2]])
# labels sit at the middle of ring edges and spokes, and near one end of star edges
let LabelPos = |P, k| lerp(P[PE[k][0]], P[PE[k][1]], [0.5, 0.26][k >= 10])
let Label3 = |pos, n, cols = [INK, INK, INK], size = 0.72| [
    center{pos} fill{PAPER} stroke{MIST, 1.5} Rect([0.46, 0.36]),
    map([0, 1, 2], |j| center{pos + [0.12 * (j - 1), 0, 0]} Cap(to_string(bit(n, 2 - j)), size, cols[j]))
]
let Label2 = |pos, n, col = INK| [
    center{pos} fill{PAPER} stroke{MIST, 1.5} Rect([0.44, 0.36]),
    center{pos} Cap(code2(n), 0.74, col)
]

# K4 drawn as a square with its diagonals, with a 3-edge-colouring
let KP = [[-3.2, 1.6, 0], [-3.2, -1.6, 0], [0.1, -1.6, 0], [0.1, 1.6, 0]]
let KE = [[0, 1], [1, 2], [2, 0], [0, 3], [1, 3], [2, 3]]
let KC = [1, 2, 3, 2, 3, 1]
# the two coats of an edge of each colour, as two-bit codes, and the label they give
let COATS = [[0, 0], [0, 1], [0, 2], [1, 2]]
let KL = map(KC, |c| xor(COATS[c][0], COATS[c][1]))
let KPos = |k| lerp(KP[KE[k][0]], KP[KE[k][1]], [0.5, 0.28][k == 2 or k == 4])
let dot3 = |a, b| imod(bit(a, 0) * bit(b, 0) + bit(a, 1) * bit(b, 1) + bit(a, 2) * bit(b, 2), 2)
# slides that solve the agreement equations for the labelling PF
let SOLVED = [4, 6, 0, 4, 5, 0, 5, 5, 6, 2]

# the ring toy: node i colours its half-edge towards node i + 1 orange when bits[i] is 0, blue when it is 1,
# and its other half-edge the other colour
let ring_broken = |n, bits| count(range(0, n), |i| bits[i] + bits[imod(i + 1, n)] != 1)
let RingFig = |n, bits, c, R, circled| block {
    let P = ring_pts(n, R, c, PI / 2)
    for (i in range(0, n)) {
        let j = imod(i + 1, n)
        let m = (P[i] + P[j]) / 2
        . Seg(P[i], m, [EMBER, AZURE][bits[i]], 10)
        . Seg(m, P[j], [AZURE, EMBER][bits[j]], 10)
    }
    . map(P, |p| Pt(p, INK, 0.13))
    . map(circled, |i| center{P[i]} fill{alpha{0} PAPER} stroke{INK, 3} Circle(0.3, 48))
}
let RingMarks = |n, bits, c, R| block {
    let P = ring_pts(n, R, c, PI / 2)
    for (i in range(0, n)) {
        let j = imod(i + 1, n)
        if (bits[i] + bits[j] != 1) {
            . center{(P[i] + P[j]) / 2} fill{PAPER} stroke{ROSE, 4} Circle(0.14, 32)
        }
    }
}