Prüfer encoding and decoding of labeled trees
DefinitionGYGraphTheoryThis module supplies the Prüfer sequence encoding and decoding maps for labeled
trees on Fin n, following Gross and Yellen, Graph Theory and Its Applications,
3rd ed., Section 3.7, Algorithms 3.7.1 and 3.7.3 (pp. 157, 159).
pruferEncode T (Algorithm 3.7.1) repeatedly removes, from the "active" vertex set
(initially all of Fin n), the smallest-labeled vertex with exactly one neighbor still
active, recording that neighbor's label; iterating this n - 2 times produces the
Prüfer sequence of the tree T. pruferDecode s (Algorithm 3.7.3) reverses this: it
recurses on the sequence s, at each step joining the smallest label of the active
label-set absent from the remaining sequence to the head of the sequence, and finishing
by joining the two labels left over. Both maps are defined for every SimpleGraph (Fin n) and every function Fin (n - 2) → Fin n, not only for genuine trees / genuine
Prüfer sequences; outside the range where the source's algorithm is meaningful (fewer
than two active vertices remain when a leaf is sought, etc.) they default to the label
0. This matches the source's algorithms exactly on trees and does not presuppose the
propositions that establish they are mutually inverse — those are this mission's
milestones.
import Mathlib
/-!
Definitions for the GYGraphTheory mission proposal (Cayley's Tree Formula).
Source: J.L. Gross and J. Yellen, *Graph Theory and Its Applications*, 3rd ed.,
CRC Press, 2018, Section 3.7 "Counting Labeled Trees: Prüfer Encoding", pp. 157-162.
-/
noncomputable section
open Classical
namespace GYGraphTheory
variable {n : ℕ} [NeZero n]
/-- The neighbors of `v` lying in the active vertex set `S` (Gross–Yellen, p. 157: the
neighbor set used when peeling leaves during Prüfer encoding). -/
def activeNeighbors (T : SimpleGraph (Fin n)) (S : Finset (Fin n)) (v : Fin n) : Finset (Fin n) :=
S.filter (fun w => T.Adj v w)
/-- `v` is a leaf of `T` relative to the active set `S`: an element of `S` with exactly one
neighbor still in `S`. -/
def IsActiveLeaf (T : SimpleGraph (Fin n)) (S : Finset (Fin n)) (v : Fin n) : Prop :=
v ∈ S ∧ (activeNeighbors T S v).card = 1
/-- The smallest-labeled active leaf of `T` in `S` (Algorithm 3.7.1, p. 157: "Let `v` be the
1-valent vertex with the smallest label"). Junk-valued (`0`) when `S` has no active leaf,
i.e. outside the intended range `2 ≤ S.card`. -/
def leastActiveLeaf (T : SimpleGraph (Fin n)) (S : Finset (Fin n)) : Fin n :=
(S.filter (IsActiveLeaf T S)).min.getD 0
/-- The unique neighbor, inside `S`, of the smallest-labeled active leaf of `T` in `S`
(Algorithm 3.7.1, p. 157: "Let `s_i` be the label of the neighbor of `v`"). Junk-valued when
that leaf has no such neighbor. -/
def leastActiveLeafNeighbor (T : SimpleGraph (Fin n)) (S : Finset (Fin n)) : Fin n :=
(activeNeighbors T S (leastActiveLeaf T S)).min.getD 0
/-- One step of Prüfer encoding: peel the smallest-labeled active leaf out of `S`, recording
its neighbor. Iterating this `n - 2` times from `S = Finset.univ` produces the Prüfer
sequence (Algorithm 3.7.1, p. 157). -/
def pruferPeel (T : SimpleGraph (Fin n)) : ℕ → Finset (Fin n) × List (Fin n)
| 0 => (Finset.univ, [])
| k + 1 =>
let (S, acc) := pruferPeel T k
(S.erase (leastActiveLeaf T S), acc ++ [leastActiveLeafNeighbor T S])
/-- The Prüfer encoding of a labeled tree `T` on `Fin n` (Algorithm 3.7.1, p. 157): the
length-`(n - 2)` sequence of neighbor-labels recorded while repeatedly peeling off the
smallest-labeled leaf. -/
def pruferEncode (T : SimpleGraph (Fin n)) (i : Fin (n - 2)) : Fin n :=
(pruferPeel T (i.1 + 1)).2.getD i.1 0
/-- Prüfer decoding (Algorithm 3.7.3, p. 159), recursing on the remaining sequence `P` while
tracking the shrinking active label-set `L`. Builds the tree by adding, at each step, an edge
from the smallest label in `L` absent from `P` to the head of `P`; the base case (`P = []`,
so `L` has the two labels left over) is the single edge joining them. Junk-valued (no edge)
wherever `L` does not have the expected size, i.e. outside the intended recursive shape. -/
def pruferDecodeAux : List (Fin n) → Finset (Fin n) → SimpleGraph (Fin n)
| [], L =>
let a := L.min.getD 0
let b := (L.erase a).min.getD 0
SimpleGraph.fromEdgeSet {s(a, b)}
| p :: ps, L =>
let k := (L \ (p :: ps).toFinset).min.getD 0
pruferDecodeAux ps (L.erase k) ⊔ SimpleGraph.fromEdgeSet {s(k, p)}
/-- The Prüfer decoding of a sequence into a labeled tree on `Fin n` (Algorithm 3.7.3, p. 159). -/
def pruferDecode (s : Fin (n - 2) → Fin n) : SimpleGraph (Fin n) :=
pruferDecodeAux (List.ofFn s) Finset.univ
end GYGraphTheory
Read-back
What the Lean code literally says, in plain math · claude-sonnet-5
activeNeighbors. For a natural number with (fixed throughout via the [NeZero n] assumption), a simple graph on the vertex set (so 's adjacency relation is symmetric and irreflexive — no vertex is adjacent to itself), a finite subset , and a vertex , activeNeighbors is defined as the finite set — i.e. those elements of that are -adjacent to . Note itself need not lie in , and if or has no neighbors in this set is simply empty.
IsActiveLeaf. For the same data and a vertex , the proposition IsActiveLeaf holds exactly when and , i.e. belongs to and has exactly one -neighbor lying in (using activeNeighbors as defined above). This says nothing about 's neighbors outside , and it is a strict equality to : a vertex in with zero or with two-or-more active neighbors does not satisfy this predicate.
leastActiveLeaf. For and as above, leastActiveLeaf first forms the sub-finset of consisting of those that are active leaves in the above sense (i.e. , which is just the set of active leaves of ), then takes its minimum under the standard order on (), and if that set of active leaves is empty, returns the default/junk value instead (which is a genuine element since ). So the output is: the smallest-labeled vertex of that has exactly one -neighbor in , when such a vertex exists; and it is the numeral — indistinguishable, by this definition alone, from a genuine leaf labeled — whenever has no active leaf at all (this is expected to happen only outside the "intended" regime , per the doc comment, but the definition itself imposes no such hypothesis and is total on all ).
leastActiveLeafNeighbor. For and , this takes leastActiveLeaf (as just described, possibly the junk value ), computes its active-neighbor set , and returns the minimum element of that set under the order on , again defaulting to if that set is empty. Thus the result is: the smallest-labeled -neighbor of the least active leaf that also lies in — which, when genuinely is an active leaf, is its unique such neighbor (so "least" is redundant there, since there is only one) — and it falls back to the junk value whenever that neighbor set is empty, in particular whenever had no active leaf to begin with (so was already junk) or, degenerately, whenever the found happens to have no -neighbor inside .
pruferPeel. This is a function defined for every natural number (not just those in some intended range), taking values in pairs (finite subset of , list of elements of ), by recursion on , for a fixed graph . At it is : the full vertex set (all labels, nonempty since ) paired with the empty list. Given its value at some , its value at is , where leastActiveLeaf is removed from (erasing an element not present is a no-op, relevant if is the junk value and ), and leastActiveLeafNeighbor is appended to the accumulator list. So, informally, step removes the smallest-labeled active leaf of the current active set from that set and records (the smallest label of) its unique remaining neighbor — and it does this uniformly for every , including far beyond (where may already be very small, empty, or have no active leaf, in which case the junk-value fallbacks of leastActiveLeaf/leastActiveLeafNeighbor silently kick in and the "peel" degenerates to repeatedly appending ).
pruferEncode. For a graph and an index ranging over — where is truncated natural-number subtraction, so this type is empty whenever , making pruferEncode vacuously have no outputs to specify in that case — pruferEncode is defined as the entry at position (0-indexed) of the list-component of pruferPeel , using List.getD with default value if that position is out of range. Since the list produced after applications of the peeling step (as defined above) always has exactly entries, the requested index always exists and refers to the last entry appended, so no default value is ever actually triggered here; concretely, pruferEncode equals leastActiveLeafNeighbor , where is the active vertex set remaining after peeling steps from the full vertex set (i.e. the -th recorded neighbor label in the peeling process). Thus pruferEncode is a function giving, for each of the first peeling steps, the neighbor label recorded at that step — the usual "Prüfer sequence" of under this specific leaf-peeling rule, well-defined (non-junk) precisely to the extent that each successive active set genuinely has an active leaf; if it does not, the corresponding sequence entries silently become via the upstream junk-value defaults, with no signal in the type that this happened.
pruferDecodeAux. This is a function taking a list of elements of and a finite set of "available labels," returning a simple graph on , defined by recursion on . In the base case : let be the minimum element of (or if is empty) and the minimum element of with removed (or if that is empty, i.e. if has at most one element); the result is the graph whose only possible edge is — via SimpleGraph.fromEdgeSet, which automatically discards a "loop" with since simple graphs are irreflexive — so this yields the single edge joining 's two smallest elements when and , and yields the empty graph (no edges at all) whenever has fewer than two elements, or degenerately whenever the two computed values coincide (e.g. and its element happens to be , or ). In the recursive case (head , tail ): let be the minimum element of (or if no such element exists, i.e. if every element of already occurs in the remaining list ); the result is the graph pruferDecodeAux (the recursive decoding of the tail on with removed) unioned (edge-set union, ⊔) with the single-edge graph on (again silently containing no edge if ). Thus, informally and when no junk case is triggered, this implements the standard Prüfer-sequence decoding: repeatedly attach an edge from the smallest currently-available label not still needed later in the sequence to the current sequence entry, shrinking the available-label pool by one each step, with a final edge joining whichever two labels remain once the sequence is exhausted; but the definition is total and silently produces missing/degenerate edges (via the described 0-fallbacks and self-loop suppression) whenever does not have the size the recursion expects at that point.
pruferDecode. For a function (again vacuous data, i.e. the empty function, whenever ), pruferDecode is defined as pruferDecodeAux applied to the list (the values of listed in increasing order of index, via List.ofFn) together with , the full set of all vertex labels, as the initial available-label pool. It returns a simple graph on , built by the recursive procedure of pruferDecodeAux described above, starting from all labels available and consuming exactly the entries of in order before the base-case final edge.
Confirmed by the mission captain (proposal self-audit).