{-# OPTIONS --cubical-compatible #-}
module Padova2025.Recreational.Hanoi where
The Tower of Hanoi
open import Padova2025.ProgrammingBasics.Naturals.Base open import Padova2025.ProgrammingBasics.Vectors open import Padova2025.ProvingBasics.Negation open import Padova2025.ProvingBasics.Equality.Base open import Padova2025.ProvingBasics.Equality.General open import Padova2025.ProvingBasics.Connectives.Conjunction open import Padova2025.ProvingBasics.Connectives.Existential open import Padova2025.ProvingBasics.Connectives.More using (Dec; yes; no)
A finite number of disks is stacked on a peg A. They
should be moved to a different peg C. Temporarily, disks
may also be parked on a third peg B. The catch: The disks
each have a different size, and at no point may a larger disk rest on a
smaller one.
This
celebrated puzzle has a well-known recursive solution. To move n disks from peg A to
C, do the following:
- Recursively move the topmost n − 1 disks from
Ato the auxiliary pegB. The disk which was originally the n-th disk onAis now at the top. - Move that disk from
Ato the target pegC. - Recursively move the n − 1
disks from peg
BtoC.
Let us formalize the puzzle and this solution in Agda. With just a tiny adaptation of this strategy, we will even be able to prove that any valid configuration of disks can be reached from any other valid configuration by a valid sequence of moves, and we can run this proof to determine such a valid sequence of moves.
We will follow a correct-by-construction approach, so the description of the strategy and the proof that the strategy is indeed working as it should are unified into a single artefact.
Preliminaries
For a vector xs and a predicate P, the type
All P xs expresses that every element of the vector
xs satisfies P. An inhabitant of
All P xs is a list of suitable witnesses. The following
definition is lifted straight from Padova2025.ProvingBasics.Connectives.More,
where we discussed the same notion but for lists instead of vectors.
data All {A : Set} (P : A → Set) : {n : ℕ} → Vector A n → Set where
[] : All P []
_∷_ : {n : ℕ} {x : A} {xs : Vector A n} → P x → All P xs → All P (x ∷ xs)
all-replicateV : {n : ℕ} {A : Set} {P : A → Set} {x : A} → P x → All P (replicateV n x)
all-replicateV {zero} px = {!!}
all-replicateV {succ n} px = {!!}
Formalization of the puzzle
The three pegs are encoded by the following type with three constructors.
data Peg : Set where A : Peg B : Peg C : Peg
A configuration of n disks is a vector of pegs of length n, with the i-th entry telling us on which peg the i-th disk is placed. We don’t need to record the disk’s position within its peg, as it is uniquely determined by the size requirement anyway. (But we will need to impose a size-related restriction when formalizing the notion of a valid move.)
Config : ℕ → Set Config n = Vector Peg n
A (valid) game move either relocates the largest disk from its peg to another, or leaves it untouched and is hence a (valid) move in the subgame with the largest disk imagined away. In the first case, for the move to be valid, all smaller disks need to sit on neither the source peg (else the largest disk is not exposed) nor the target peg (else the largest disk cannot be put there).
The type Move c c' is the type of (valid) moves from
configuration c to configuration c'.
data Move : {n : ℕ} → Config n → Config n → Set where
here
: {n : ℕ}
→ (source target : Peg) {c : Config n}
→ source ≢ target → All (λ p → p ≢ source × p ≢ target) c
→ Move (source ∷ c) (target ∷ c)
there
: {n : ℕ} {c c' : Config n} {p : Peg}
→ Move c c'
→ Move (p ∷ c) (p ∷ c')
A (valid) play is a sequence of (valid) moves. We track the starting and ending configuration directly in the type signature.
infixr 5 _◅_
data Star {X : Set} (P : X → X → Set) : X → X → Set where
ε : {x : X} → Star P x x
_◅_ : {a b c : X} → (x : P a b) → (xs : Star P b c) → Star P a c
Play : {n : ℕ} → Config n → Config n → Set
Play = Star Move
Auxiliary results
Equality of pegs is decidable, by the following boring proof where we just visit every case.
_≡?_ : (p p' : Peg) → Dec (p ≡ p') A ≡? A = yes refl A ≡? B = no λ () A ≡? C = no λ () B ≡? A = no λ () B ≡? B = yes refl B ≡? C = no λ () C ≡? A = no λ () C ≡? B = no λ () C ≡? C = yes refl
We can concatenate sequences of moves.
infixr 5 _◅◅_
_◅◅_ : {X : Set} {P : X → X → Set} {a b c : X} → Star P a b → Star P b c → Star P a c
_◅◅_ = {!!}
We can translate a play in the n-disk subgame without the largest
disk to a play in the full (n + 1)-disk game. We call this
operation frame in reference to the frame rule of separation
logic, which likewise lets one lift a statement about a small part
of the system to the whole, leaving everything else untouched.
frame : {n : ℕ} (p : Peg) {c c' : Config n} → Play c c' → Play (p ∷ c) (p ∷ c')
frame = {!!}
There is always a third peg.
third : (p p' : Peg) → p ≢ p' → Σ[ q ∈ Peg ] q ≢ p × q ≢ p'
third = {!!}
Formalization of the solution
Given distinct pegs source, target and
auxiliary, the following function produces a play which,
starting from the configuration that all disks are on
source, moves them to target. No separate
correctness proof is needed – correctness is already established by the
types. (The strategy outlined in the comments is optimal, though this is
neither stated nor proven.)
solve₀
: {n : ℕ} → (source target auxiliary : Peg)
→ source ≢ target
→ source ≢ auxiliary
→ target ≢ auxiliary
→ Play (replicateV n source) (replicateV n target)
solve₀ {zero} source target auxiliary s≢t s≢a t≢a = {!!}
solve₀ {succ n} source target auxiliary s≢t s≢a t≢a =
-- Step 1: Move the topmost n disks to the auxiliary peg.
{!!} ◅◅
-- Step 2: Move the now-exposed disk to the target peg.
{!!} ◅
-- Step 3: Move the n disks from their temporary parking position to the target peg.
{!!}
where
a≢t = ≢-sym t≢a
a≢s = ≢-sym s≢a
t≢s = ≢-sym s≢t
With just a bit more work, we can show that every (valid) configuration is reachable from any other:
solve : {n : ℕ} → (c c' : Config n) → Play c c'
solve [] [] = {!!}
solve (p ∷ c) (p' ∷ c') with p ≡? p'
... | yes refl = {!!}
... | no p≢p' =
let (q , q≢p,p') = third p p' p≢p'
mid = replicateV _ q
in
-- Step 1: Move the topmost n disks to the auxiliary peg.
{!!} ◅◅
-- Step 2: Move the now-exposed (n + 1)-th disk to where it belongs.
{!!} ◅
-- Step 3: Move the parked disks to where they belong.
{!!}
Again, correctness is ensured by the types. (But unlike
solve₀, the function solve does not always
compute optimal plays.)
Time to run our proofs! Put, for instance,
replicateV three A and replicateV three C in
the two holes in the type signature and solve _ _ into the
remaining hole below. Then use C-c C-v to run
example and observe the resulting sequence of moves. (The
output will be a bit littered by inequality witnesses.)
example : Play {!!} {!!}
example = {!!}