36.5. Sparse graphs
A graph given by a short edge list need not allocate an adjacency
matrix. SparseGraph n stores n + 1 offsets
and a flat array containing the neighbours of every vertex, using
O(n + |E|) storage. Each row is sorted, duplicate-free and loopless,
and each undirected edge occurs in both directions.
SparseGraph.ofEdges accepts pairs of
vertices in Fin n, removes duplicate edges and drops loops.
For external data with natural-number endpoints, use
SparseGraph.ofEdges?: it rejects
loops and out-of-range endpoints instead. Both constructors normalize
edge orientation and row order. General input requires sorting, so the
storage bound is not a claim that every construction takes linear time.
36.5.1. Canonicalization and isomorphism
Here is the Petersen graph built directly from its fifteen edges.
Relabelling swaps vertices 0 and 9; the pentagonal prism replaces
the inner star by a pentagon. All three values remain sparse throughout
construction and search.
namespace HexGraphIsoSparseExample
def petersen : SparseGraph 10 := SparseGraph.ofEdges
[(0, 1), (1, 2), (2, 3), (3, 4), (0, 4),
(5, 7), (7, 9), (6, 9), (6, 8), (5, 8),
(0, 5), (1, 6), (2, 7), (3, 8), (4, 9)]
def swapEnds : Perm 10 :=
⟨#v[9, 1, 2, 3, 4, 5, 6, 7, 8, 0], ⊢ #v[9, 1, 2, 3, 4, 5, 6, 7, 8, 0].toList.Nodup All goals completed! 🐙, ⊢ ∀ (i : Fin 10), i ∈ #v[9, 1, 2, 3, 4, 5, 6, 7, 8, 0].toList All goals completed! 🐙⟩
def renamed : SparseGraph 10 := petersen.relabel swapEnds
def prism : SparseGraph 10 := SparseGraph.ofEdges
[(0, 1), (1, 2), (2, 3), (3, 4), (0, 4),
(5, 6), (6, 7), (7, 8), (8, 9), (5, 9),
(0, 5), (1, 6), (2, 7), (3, 8), (4, 9)]
#guard SparseGraph.ofEdges? 3 [(0, 1), (1, 0)] =
some (SparseGraph.ofEdges [(0, 1)])
#guard (SparseGraph.ofEdges? 3 [(0, 3)]).isNone
#guard (SparseGraph.ofEdges? 3 [(1, 1)]).isNone
#guard petersen ≠ renamed
#guard SparseGraph.canon petersen =
SparseGraph.canon renamed
#guard (SparseGraph.findIso petersen renamed).isSome
#guard !(SparseGraph.isIso petersen prism)
example : SparseGraph.Isomorphic petersen renamed := ⊢ petersen.Isomorphic renamed
All goals completed! 🐙
example : ¬ SparseGraph.Isomorphic petersen prism := ⊢ ¬petersen.Isomorphic prism
All goals completed! 🐙
SparseGraph.canonicalize returns
the form and its label together. A label maps each new vertex to its
old vertex. In contrast, the permutation returned by
SparseGraph.findIso maps vertices of
the first input to vertices of the second. These operations are total,
including on the empty graph. A result of none from isomorphism search
means that the graphs are not isomorphic, by
SparseGraph.findIso_eq_none_iff.
example (G : SparseGraph 10) :
G.relabel (SparseGraph.label G).perm =
SparseGraph.canon G :=
SparseGraph.relabel_label G
example (G H : SparseGraph 10) :
SparseGraph.Isomorphic G H ↔
SparseGraph.canon G = SparseGraph.canon H :=
SparseGraph.iso_iff_canon_eq G H
example : SparseGraph.Isomorphic
(SparseGraph.empty 0) (SparseGraph.empty 0) := ⊢ (SparseGraph.empty 0).Isomorphic (SparseGraph.empty 0)
All goals completed! 🐙
36.5.2. Ordered colours and automorphisms
Sparse coloured graphs use the same
Coloring as dense graphs. Marking a
Petersen edge or a non-edge imposes exactly the constraints from the
ordered-colour example.
def edgeMarkA : Sparse.Colored 10 2 :=
⟨petersen, HexGraphIsoChapterExample.markPair 0 1⟩
def edgeMarkB : Sparse.Colored 10 2 :=
⟨petersen, HexGraphIsoChapterExample.markPair 2 3⟩
def nonedgeMark : Sparse.Colored 10 2 :=
⟨petersen, HexGraphIsoChapterExample.markPair 0 2⟩
example : Sparse.Isomorphic edgeMarkA edgeMarkB := ⊢ Sparse.Isomorphic edgeMarkA edgeMarkB
All goals completed! 🐙
example : ¬ Sparse.Isomorphic edgeMarkA nonedgeMark := ⊢ ¬Sparse.Isomorphic edgeMarkA nonedgeMark
All goals completed! 🐙
#guard (SparseGraph.autos petersen).order = 120
#guard (SparseGraph.autos petersen).numOrbits = 1
#guard (Sparse.autos edgeMarkA).order = 8
example (p : Perm 10)
(h : p ∈ (SparseGraph.autos petersen).gens) :
SparseGraph.IsIso petersen petersen p :=
SparseGraph.autos_isIso h
end HexGraphIsoSparseExample
Sparse.autos and
SparseGraph.autos return generators,
orbit representatives, the orbit count and an exact natural-number
group order from one traversal. The generators generate every
automorphism, by
Sparse.autos_complete.
Equality of two orbit entries is equivalent to the existence of an
automorphism carrying one vertex to the other, by
Sparse.autos_sameOrbit.
The order is the product of the stabilizer indices accumulated during
the search. Its cardinality theorem and the corresponding Mathlib
operations appear in the
sparse Mathlib section.
36.5.3. Choosing and converting representations
Native sparse input avoids storing or scanning an adjacency matrix.
For an existing dense graph,
Graph.toSparse scans its n² entries.
SparseGraph.toDense allocates a dense
matrix. Both conversions preserve adjacency and isomorphism. To compare
graphs stored differently, convert one so that both use the same engine.
The performance comparison shows
how the choice affects different graph families.
Canonicalization itself need not commute with conversion. For two disjoint edges, dense and sparse nauty choose different canonical adjacency matrices:
namespace HexGraphIsoRepresentationExample
def matching : SparseGraph 4 :=
SparseGraph.ofEdges [(0, 3), (1, 2)]
#guard (Graph.label matching.toDense).toArray =
#[0, 3, 1, 2]
#guard (SparseGraph.label matching).toArray = #[0, 1, 2, 3]
#guard (SparseGraph.canon matching).toDense ≠
Graph.canon matching.toDense
end HexGraphIsoRepresentationExample
Each result is a canonical form for its own algorithm. When storing canonical forms as persistent keys, record which engine and options produced them. Traces defines another algorithm and is included in the performance comparison, but has no Lean implementation in this library.
36.5.4. Certificates and tactic limits
graph_iso operates on closed graph expressions and selects the dense
or sparse checker from their types. A positive proof checks a literal
vertex permutation. For a negative proof, a differing root refinement
code suffices when available. Otherwise the tactic produces compact
certificates for both sparse canonical keys and replays their literal
checkers in the kernel. The sparse checker recomputes sparse refinement
and comparisons. The direct canonicalization API does not perform this
replay.
The tactic accepts two optional limits. maxSearchNodes bounds search
node visits, and maxCertRecords bounds certificate records. Both
default to 100000. Exceeding a limit makes the tactic fail; it does
not establish non-isomorphism. They can be raised independently:
example : ¬ SparseGraph.Isomorphic
HexGraphIsoSparseExample.petersen
HexGraphIsoSparseExample.prism := ⊢ ¬HexGraphIsoSparseExample.petersen.Isomorphic HexGraphIsoSparseExample.prism
All goals completed! 🐙
These limits do not apply to the total canonicalization and isomorphism functions. Kernel replay has no estimated-cost cutoff or operation meter. Lean's ordinary heartbeat and recursion-depth options still apply to elaboration.