Skip to content
The Lumenary

Math laboratory

One-stroke figures: exhaustive search versus Euler's rule

Counts the degree of every dot in seven small figures, including the house puzzle and the seven bridges of Konigsberg, and searches every route that uses each line once. The search settles each figure for certain and agrees with Euler's odd-dot rule on all seven; it does not prove the rule for figures it has not tried.

Back to the labExact computationExhaustive finite check
One-stroke figures: exhaustive search versus Euler's rulePythonExact computationExhaustive finite checkComputationally tested
"""Which figures can be traced in one stroke? Search every route, then compare.

For each small figure (a network of dots and lines) the program counts the
line ends at every dot (the degree), and then searches *every* possible route
that uses each line exactly once, starting from every dot. The search is
exhaustive, so for these figures its answer is certain. We then compare the
search with Euler's rule: a connected figure can be traced in one stroke
exactly when it has 0 or 2 dots of odd degree.

Runs unchanged in the browser (Pyodide) using only the standard library.
"""

import textwrap

#: Each figure is a list of lines; a line joins two dots (letters).
FIGURES = {
    "square": ["AB", "BC", "CD", "DA"],
    "square + diagonal": ["AB", "BC", "CD", "DA", "AC"],
    "envelope (2 diagonals)": ["AB", "BC", "CD", "DA", "AC", "BD"],
    "house": ["AB", "BC", "CD", "DA", "AC", "BD", "DE", "EC"],
    "bow tie": ["AB", "BC", "CA", "CD", "DE", "EC"],
    "two triangles apart": ["AB", "BC", "CA", "DE", "EF", "FD"],
    "Konigsberg bridges": ["IN", "IN", "IS", "IS", "IE", "NE", "SE"],
}

def dots(lines):
    """Return the sorted list of dots that appear on some line."""
    return sorted({d for line in lines for d in line})

def degrees(lines):
    """Return {dot: number of line ends at that dot}."""
    deg = {d: 0 for d in dots(lines)}
    for a, b in lines:
        deg[a] += 1
        deg[b] += 1
    return deg

def odd_dots(lines):
    """Return the sorted list of dots of odd degree."""
    return [d for d, k in degrees(lines).items() if k % 2 == 1]

def is_connected(lines):
    """True if every dot can be reached from every other dot along lines."""
    all_dots = dots(lines)
    if not all_dots:
        return True
    seen, todo = {all_dots[0]}, [all_dots[0]]
    while todo:
        here = todo.pop()
        for a, b in lines:
            for x, y in ((a, b), (b, a)):
                if x == here and y not in seen:
                    seen.add(y)
                    todo.append(y)
    return len(seen) == len(all_dots)

def find_one_stroke(lines):
    """Search every route that uses each line once; return one or None.

    A route is returned as a string of dots, e.g. "ABCA". Every starting dot
    and every order of lines is tried (depth-first search with backtracking).
    """
    used = [False] * len(lines)

    def extend(route):
        if all(used):
            return route
        here = route[-1]
        for i, (a, b) in enumerate(lines):
            if used[i] or here not in (a, b):
                continue
            used[i] = True
            found = extend(route + (b if here == a else a))
            used[i] = False
            if found:
                return found
        return None

    for start in dots(lines):
        found = extend(start)
        if found:
            return found
    return None

def euler_rule_says_yes(lines):
    """Euler's rule: connected, with 0 or 2 dots of odd degree."""
    return is_connected(lines) and len(odd_dots(lines)) in (0, 2)

def main():
    print("Figure                  degrees          odd  rule  search finds")
    agree = 0
    for name, lines in FIGURES.items():
        deg = degrees(lines)
        shown = " ".join(str(deg[d]) for d in dots(lines))
        rule = euler_rule_says_yes(lines)
        route = find_one_stroke(lines)
        agree += rule == (route is not None)
        answer = route if route else "no route"
        print(f"{name:<23} {shown:<16} {len(odd_dots(lines)):>3}  "
              f"{'yes' if rule else 'no':<4}  {answer}")
    print("(Degrees are listed in alphabetical order of the dots.)")
    print()
    print(f"The rule and the search agree on {agree} of {len(FIGURES)} figures.")
    print("Each route starts and ends at the odd dots, when there are two.")
    print("'Two triangles apart' has no odd dots but is not connected,")
    print("so the rule needs the word 'connected'.")
    print()
    print(textwrap.fill(
        "Epistemic status: for each of these seven figures the search tried "
        "every route, so its yes or no is certain (exhaustive-finite). The "
        "agreement with Euler's rule is evidence for the rule, not a proof: "
        "the lesson explains why odd dots must be the ends of a route, and "
        "the Lean module proves that half of the rule for every figure.",
        width=78,
    ))

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in