{-# OPTIONS --cubical-compatible #-}
module Padova2025-zh-TW.Recreational.Hanoi where
河內塔#
open import Padova2025-zh-TW.ProgrammingBasics.Naturals.Base
open import Padova2025-zh-TW.ProgrammingBasics.Vectors
open import Padova2025-zh-TW.ProvingBasics.Negation
open import Padova2025-zh-TW.ProvingBasics.Equality.Base
open import Padova2025-zh-TW.ProvingBasics.Equality.General
open import Padova2025-zh-TW.ProvingBasics.Connectives.Conjunction
open import Padova2025-zh-TW.ProvingBasics.Connectives.Existential
open import Padova2025-zh-TW.ProvingBasics.Connectives.More using (Dec; yes; no)
有限多個圓盤堆疊在柱子 A 上。它們應該被移動到另一根柱子 C 上。暫時地,圓盤也可以停放在第三根柱子 B 上。
難處在於:每個圓盤的大小都不同,而且在任何時刻,較大的圓盤都不能放在較小的圓盤上面。
這個著名的謎題有一個眾所周知的遞迴解法。
要把 n 個圓盤從柱子 A 移到 C,做法如下:
- 遞迴地把最上面的 n - 1 個圓盤從
A移到輔助柱子B。原本在A上的第 n 個圓盤現在位於最上面。 - 把那個圓盤從
A移到目標柱子C。 - 遞迴地把 n - 1 個圓盤從柱子
B移到C。
讓我們在 Agda 中形式化這個謎題與這個解法。只要對這個策略做一點點調整,我們甚至能夠證明:任何有效的圓盤組態, 都可以經由一個有效的移動序列,從任何其他有效的組態到達,而且我們可以執行這個證明,來決定這樣一個有效的移動序列。
我們會採用依構造而正確的做法,因此策略的描述,以及該策略確實如預期運作的證明,會被統一成單一個產物。
預備知識#
對向量 xs 與謂詞 P,型別 All P xs 表示向量 xs 的每個元素都滿足 P。All P xs 的居民就是適當見證所成的串列。
下面的定義直接取自
Padova2025-zh-TW.ProvingBasics.Connectives.More,
我們在那裡討論過同一個概念,只不過針對的是串列而不是向量。
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-replicateVall-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 = {!!}
謎題的形式化#
三根柱子由下面這個有三個建構子的型別編碼。
data Peg : Set where
A : Peg
B : Peg
C : Peg
n 個圓盤的組態是一個長度為 n 的柱子向量,第 i 個項目告訴我們第 i 個圓盤放在哪根柱子上。我們不需要記錄圓盤在柱子上的位置, 因為這已經被大小的要求唯一決定了。(不過,在形式化有效移動的概念時,我們會需要強加一個與大小有關的限制。)
Config : ℕ → Set
Config n = Vector Peg n
一個(有效的)遊戲移動,要嘛把最大的圓盤從它所在的柱子搬到另一根,要嘛不動它,因而是想像把最大圓盤拿掉後的子遊戲中的一個(有效的)移動。 在第一種情況下,為了讓移動有效,所有較小的圓盤都必須既不在來源柱子上(否則最大的圓盤不是露出來的), 也不在目標柱子上(否則最大的圓盤無法放到那裡)。
型別 Move c c' 是從組態 c 到組態 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')
一個(有效的)過程(play)是(有效)移動的序列。我們直接在型別簽名中追蹤起始與結束的組態。
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
輔助結果#
柱子的相等是可判定的,證明如下,一個無聊的證明,我們只是逐一走訪每種情況。
_≡?_ : (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
我們可以串接移動序列。
_◅◅_infixr 5 _◅◅_
_◅◅_ : {X : Set} {P : X → X → Set} {a b c : X} → Star P a b → Star P b c → Star P a c
_◅◅_ = {!!}
我們可以把不含最大圓盤的 n 圓盤子遊戲中的過程,轉換成完整的 (n + 1) 圓盤遊戲中的過程。我們把這個運算稱為 frame,
這是參照分離邏輯的框架規則(frame rule),
它同樣讓人把關於系統一小部分的陳述提升到整體,而讓其他一切保持不動。
frameframe : {n : ℕ} (p : Peg) {c c' : Config n} → Play c c' → Play (p ∷ c) (p ∷ c')
frame = {!!}
永遠存在第三根柱子。
thirdthird : (p p' : Peg) → p ≢ p' → Σ[ q ∈ Peg ] q ≢ p × q ≢ p'
third = {!!}
參考解答
third : (p p' : Peg) → p ≢ p' → Σ[ q ∈ Peg ] q ≢ p × q ≢ p'
third A A p≢p' = ⊥-elim (p≢p' refl)
third A B p≢p' = C , (λ ()) , (λ ())
third A C p≢p' = B , (λ ()) , (λ ())
third B A p≢p' = C , (λ ()) , (λ ())
third B B p≢p' = ⊥-elim (p≢p' refl)
third B C p≢p' = A , (λ ()) , (λ ())
third C A p≢p' = B , (λ ()) , (λ ())
third C B p≢p' = A , (λ ()) , (λ ())
third C C p≢p' = ⊥-elim (p≢p' refl)
解法的形式化#
給定相異的柱子 source、target 與 auxiliary,下面的函數會產生一個過程,從所有圓盤都在 source 上的組態出發,
把它們移到 target。不需要另外的正確性證明——正確性已經由型別確立。(註解中概述的策略是最優的,不過這一點既沒有被陳述也沒有被證明。)
solve₀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 =
-- 步驟 1:把最上面的 n 個圓盤移到輔助柱子。
{!!} ◅◅
-- 步驟 2:把現在露出來的圓盤移到目標柱子。
{!!} ◅
-- 步驟 3:把這 n 個圓盤從暫時停放的位置移到目標柱子。
{!!}
where
a≢t = ≢-sym t≢a
a≢s = ≢-sym s≢a
t≢s = ≢-sym s≢t
參考解答
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 =
-- 步驟 1:把最上面的 n 個圓盤移到輔助柱子。
frame source (solve₀ source auxiliary target s≢a s≢t a≢t) ◅◅
-- 步驟 2:把現在露出來的圓盤移到目標柱子。
here source target s≢t (all-replicateV (a≢s , a≢t)) ◅
-- 步驟 3:把這 n 個圓盤從暫時停放的位置移到目標柱子。
frame target (solve₀ auxiliary target source a≢t a≢s t≢s)
where
a≢t = ≢-sym t≢a
a≢s = ≢-sym s≢a
t≢s = ≢-sym s≢t
再多費一點功夫,我們就能證明:每個(有效的)組態都可以從任何其他組態到達:
solvesolve : {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
-- 步驟 1:把最上面的 n 個圓盤移到輔助柱子。
{!!} ◅◅
-- 步驟 2:把現在露出來的第 (n + 1) 個圓盤移到它該去的地方。
{!!} ◅
-- 步驟 3:把停放的圓盤移到它們該去的地方。
{!!}
參考解答
solve : {n : ℕ} → (c c' : Config n) → Play c c'
solve [] [] = ε
solve (p ∷ c) (p' ∷ c') with p ≡? p'
... | yes refl = frame p (solve c c')
... | no p≢p' =
let (q , q≢p,p') = third p p' p≢p'
mid = replicateV _ q
in
-- 步驟 1:把最上面的 n 個圓盤移到輔助柱子。
frame p (solve c mid) ◅◅
-- 步驟 2:把現在露出來的第 (n + 1) 個圓盤移到它該去的地方。
here p p' p≢p' (all-replicateV q≢p,p') ◅
-- 步驟 3:把停放的圓盤移到它們該去的地方。
frame p' (solve mid c')
同樣地,正確性由型別保證。(但與 solve₀ 不同,函數 solve 並不總是計算出最優的過程。)
是時候執行我們的證明了!例如,把 replicateV three A 與 replicateV three C 放進型別簽名的兩個 hole 裡,
並把 solve _ _ 放進下面剩下的 hole 裡。然後用 C-c C-v 執行 example,觀察得到的移動序列。
(輸出會被一些不等式見證弄得有點雜亂。)
exampleexample : Play {!!} {!!}
example = {!!}
參考解答
example : Play (replicateV three A) (replicateV three C)
example = solve _ _