Skip to content
The Lumenary

Math laboratory

Counting trees and sorting graphs by isomorphism

Checks every set of n - 1 edges on n <= 7 labelled vertices: connected exactly when acyclic, and there are n^(n-2) labelled trees (Cayley's formula). Sorts the trees into isomorphism classes with a canonical code (1, 1, 1, 2, 3, 6, 11 classes), finds an isomorphism between a pentagon and a pentagram by trying all 120 bijections, and separates two graphs with equal degrees by connectedness. Small cases only.

Back to the labExhaustive finite checkExact computation
Counting trees and sorting graphs by isomorphismPythonExhaustive finite checkExact computationComputationally tested
"""Trees and isomorphism, by exhaustive search on small vertex sets.

Every set of n - 1 edges on the vertices 0..n-1 (n <= 7) is examined: it is
connected exactly when it has no cycle, exactly when it is a tree - the
"two out of three" property proved in the lesson - and the labelled trees
number n^(n-2), as Cayley's formula predicts. The trees are then sorted into
isomorphism classes with a canonical code (Aho-Hopcroft-Ullman: root the tree
at its centre and encode it recursively). Finally an isomorphism between a
pentagon and a pentagram is found by trying all 120 bijections, and two
graphs with the same degrees are told apart by connectedness.
"""

import itertools

def components(n, edges):
    """Number of connected pieces, by merging endpoints (union-find)."""
    parent = list(range(n))

    def root(v):
        while parent[v] != v:
            v = parent[v]
        return v

    for a, b in edges:
        parent[root(a)] = root(b)
    return len({root(v) for v in range(n)})

def has_cycle(n, edges):
    """True if some edge joins two vertices already joined by a path."""
    parent = list(range(n))

    def root(v):
        while parent[v] != v:
            v = parent[v]
        return v

    for a, b in edges:
        ra, rb = root(a), root(b)
        if ra == rb:
            return True
        parent[ra] = rb
    return False

def tree_census(n):
    """(edge sets of size n-1, connected ones, acyclic ones), all trees."""
    pairs = list(itertools.combinations(range(n), 2))
    total = conn = acyc = 0
    trees = []
    for edges in itertools.combinations(pairs, n - 1):
        total += 1
        c, a = components(n, edges) == 1, not has_cycle(n, edges)
        conn, acyc = conn + c, acyc + a
        if c:
            trees.append(edges)
    return total, conn, acyc, trees

def canonical_tree(n, edges):
    """A code equal for two trees exactly when they are isomorphic (AHU)."""
    nbrs = {v: set() for v in range(n)}
    for a, b in edges:
        nbrs[a].add(b)
        nbrs[b].add(a)
    layer, left = [v for v in nbrs if len(nbrs[v]) <= 1], set(nbrs)
    while len(left) > 2:  # strip leaves until the centre (1 or 2 vertices)
        left -= set(layer)
        layer = [v for v in left if len(nbrs[v] & left) <= 1]

    def code(v, parent):
        return "(" + "".join(sorted(code(w, v) for w in nbrs[v]
                                    if w != parent)) + ")"

    return min(code(c, None) for c in left)

def find_isomorphism(n, edges1, edges2):
    """A bijection of 0..n-1 carrying edges1 onto edges2, or None."""
    target = {frozenset(e) for e in edges2}
    for perm in itertools.permutations(range(n)):
        if {frozenset((perm[a], perm[b])) for a, b in edges1} == target:
            return perm
    return None

def main():
    print(" n  edge sets  connected  acyclic  trees  n^(n-2)  classes")
    for n in range(2, 8):
        total, conn, acyc, trees = tree_census(n)
        classes = len({canonical_tree(n, t) for t in trees})
        print(f"{n:2d}  {total:9d}  {conn:9d}  {acyc:7d}  {len(trees):5d}"
              f"  {n ** (n - 2):7d}  {classes:7d}")
    print("(n = 1: one tree, one class.)")
    print()
    pentagon = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]
    pentagram = [(0, 2), (2, 4), (4, 1), (1, 3), (3, 0)]
    print(f"Pentagon -> pentagram isomorphism: "
          f"{find_isomorphism(5, pentagon, pentagram)}")
    hexagon = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)]
    triangles = [(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)]
    print("Hexagon vs two triangles: all six degrees 2 in both;")
    print(f"  pieces {components(6, hexagon)} vs "
          f"{components(6, triangles)}; isomorphism: "
          f"{find_isomorphism(6, hexagon, triangles)}")
    print()
    print("Epistemic status: exhaustive search over all edge sets for n <= 7")
    print("(evidence for the tree theorems and Cayley's formula, which hold")
    print("for every n) and over all bijections for the two examples (a")
    print("complete check, so those isomorphism verdicts are certain).")

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

Used in