Skip to content
The Lumenary

Math laboratory

All subgroups of S3, D4, A4, S4 and C12

Generates five permutation groups, finds every subgroup as an iterated join of cyclic subgroups, checks that each subgroup order divides the group order (Lagrange), counts normal subgroups, and exhibits the counterexample to the converse of Lagrange: A4 has order 12 but no subgroup of order 6. Exact and exhaustive for these groups only.

Back to the labExact computationExhaustive finite checkCounterexample
All subgroups of S3, D4, A4, S4 and C12PythonExact computationExhaustive finite checkCounterexampleComputationally tested
"""Every subgroup of five small groups, found by machine, with Lagrange checked.

Permutations are tuples of images, and a subgroup is generated by closure. Every
subgroup of a finite group is the join of its cyclic subgroups, so starting from
the cyclic subgroups and repeatedly forming the subgroup generated by two known
subgroups finds *all* subgroups. For each group the program lists the subgroup
orders, counts the normal subgroups, checks Lagrange's theorem and reports the
divisors of |G| that are not the order of any subgroup (for A4: 6). This is an
exact, exhaustive computation for these five groups and proves nothing beyond.
"""

from collections import Counter
from math import lcm

def compose(p, q):
    """Return the permutation 'first q, then p' (p after q), as a tuple."""
    return tuple(p[i] for i in q)

def inverse(p):
    """Return the inverse permutation of p."""
    inv = [0] * len(p)
    for i, image in enumerate(p):
        inv[image] = i
    return tuple(inv)

def closure(gens, n):
    """The subgroup of S_n generated by gens, as a frozenset of tuples.

    In a finite group every inverse is a positive power, so closing the identity
    under multiplication by the generators gives the whole generated subgroup.
    """
    identity = tuple(range(n))
    elements = {identity}
    frontier = [identity]
    while frontier:
        new = []
        for x in frontier:
            for g in gens:
                y = compose(g, x)
                if y not in elements:
                    elements.add(y)
                    new.append(y)
        frontier = new
    return frozenset(elements)

def order(p):
    """The order of a permutation: the lcm of its cycle lengths."""
    seen, result = set(), 1
    for start in range(len(p)):
        length, i = 0, start
        while i not in seen:
            seen.add(i)
            i = p[i]
            length += 1
        if length:
            result = lcm(result, length)
    return result

def all_subgroups(group, n):
    """Every subgroup of a finite permutation group, as frozensets.

    Begin with the cyclic subgroups; join each newly found subgroup with every
    known one until nothing new appears. A subgroup H is the join of the <h>,
    h in H, built one cyclic subgroup at a time, so H is eventually found.
    """
    gens = {}
    for g in sorted(group):
        gens.setdefault(closure([g], n), [g])
    frontier = list(gens)
    while frontier:
        found = []
        for h in frontier:
            for k in list(gens):
                if h <= k or k <= h:
                    continue
                join = closure(gens[h] + gens[k], n)
                if join not in gens:
                    gens[join] = gens[h] + gens[k]
                    found.append(join)
        frontier = found
    return list(gens)

def is_normal(sub, group):
    """True when g h g^-1 lies in sub for all g in group and h in sub."""
    return all(compose(compose(g, h), inverse(g)) in sub
               for g in group for h in sub)

def census(group, n):
    """Return (subgroup-order counts, number normal, missing divisors)."""
    subs = all_subgroups(group, n)
    counts = Counter(len(s) for s in subs)
    normal = sum(is_normal(s, group) for s in subs)
    size = len(group)
    divisors = [d for d in range(1, size + 1) if size % d == 0]
    missing = [d for d in divisors if d not in counts]
    return counts, normal, missing

def examples():
    """The five groups, as (name, degree n, generators)."""
    return [
        ("S3", 3, [(1, 0, 2), (1, 2, 0)]),
        ("D4", 4, [(1, 2, 3, 0), (0, 3, 2, 1)]),
        ("A4", 4, [(1, 2, 0, 3), (1, 0, 3, 2)]),
        ("S4", 4, [(1, 0, 2, 3), (1, 2, 3, 0)]),
        ("C12", 12, [tuple((i + 1) % 12 for i in range(12))]),
    ]

def main():
    print("group |G| subgroups normal  subgroup orders: order(how many)")
    total, lagrange_ok = 0, True
    for name, n, gen in examples():
        group = closure(gen, n)
        counts, normal, missing = census(group, n)
        total += sum(counts.values())
        lagrange_ok &= all(len(group) % d == 0 for d in counts)
        shown = " ".join(f"{d}({c})" for d, c in sorted(counts.items()))
        print(f"{name:<5} {len(group):>3} {sum(counts.values()):>9} {normal:>6}"
              f"  {shown}")
        if missing:
            print(f"      divisors of {len(group)} with no subgroup: {missing}")
    print(f"Lagrange (subgroup order divides |G|) holds for all {total} "
          f"subgroups: {lagrange_ok}")
    a4 = closure(examples()[2][2], 4)
    hist = sorted(Counter(order(p) for p in a4).items())
    print(f"A4 element orders (order: how many): {hist}")
    print("(Index 2 would force a subgroup of order 6 to contain all nine")
    print("squares in A4: the identity and the eight 3-cycles.)")
    print()
    print("Epistemic status: exact and exhaustive for these five groups. The")
    print("A4 row is a genuine counterexample to the converse of Lagrange's")
    print("theorem; the other rows are evidence, not proofs of general laws.")

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

Used in