CLRS Section 19.2 - Linked-list representation
The CLRS linked-list representation stores, for every element, the head of its set and stores a set size at each head. The executable model below makes that table-level behavior explicit. A weighted union redirects every head pointer in the smaller class and records the number of rewritten pointers as its cost.
Main results:
-
Theorem
LinkedList.weightedUnion_sameSet_iff: weighted union has the exact abstract merge semantics. -
Theorem
LinkedList.weightedUnion_preserves_headInvariant: every head remains a representative. -
Theorem
LinkedList.weightedUnion_changed_doubles: whenever an element's representative pointer is rewritten, its recorded set size at least doubles. -
Theorems
LinkedList.move_count_le_log2andLinkedList.total_rewrites_le_n_mul_log2: the standard CLRS aggregate bound extracted from the doubling argument.
namespace CLRSnamespace Chapter21namespace LinkedListopen FinsetTable-level model of the linked-list disjoint-set representation.
structure State (n : Nat) where
head : Fin n → Fin n
size : Fin n → Natnamespace Statevariable {n : Nat}Two elements are in the same represented list exactly when their heads agree.
The size recorded at the representative of x.
Every stored head is itself a representative.
The represented abstract partition.
def partition (s : State n) : Partition (Fin n) where
sameSet := s.sameSet
refl := fun _ => rfl
symm := fun h => h.symm
trans := fun hab hbc => hab.trans hbc
The initial state containing n singleton lists.
@[simp]
theorem singleton_head (x : Fin n) :
(singleton n).head x = x :=
rfl@[simp]
theorem singleton_size (x : Fin n) :
(singleton n).setSize x = 1 :=
rfltheorem singleton_headInvariant (n : Nat) :
(singleton n).HeadInvariant := by
intro x
rfltheorem singleton_sameSet_iff (x y : Fin n) :
(singleton n).sameSet x y ↔ x = y :=
Iff.rfl
Redirect the class of src to the head of dst.
def mergeToward (s : State n) (src dst : Fin n) : State n where
head z := if s.head z = s.head src then s.head dst else s.head z
size z :=
if z = s.head dst then s.setSize src + s.setSize dst
else if z = s.head src then 0
else s.size zWeighted union redirects the smaller class. The second component is exactly the number of representative pointers charged by the table-level model.
def weightedUnion (s : State n) (x y : Fin n) : State n × Nat :=
if s.head x = s.head y then
(s, 0)
else if s.setSize x ≤ s.setSize y then
(s.mergeToward x y, s.setSize x)
else
(s.mergeToward y x, s.setSize y)Redirecting one distinct class to another has the exact merge relation.
theorem mergeToward_sameSet_iff (s : State n) {x y : Fin n}
(_hxy : ¬s.sameSet x y) (a b : Fin n) :
(s.mergeToward x y).sameSet a b ↔
s.sameSet a b ∨
(s.sameSet a x ∧ s.sameSet y b) ∨
(s.sameSet a y ∧ s.sameSet x b) := by
unfold sameSet at ⊢
by_cases ha : s.head a = s.head x
· by_cases hb : s.head b = s.head x
· simp [mergeToward, ha, hb]
· constructor
· intro h
simp [mergeToward, ha, hb] at h
exact Or.inr (Or.inl ⟨ha, h⟩)
· intro h
rcases h with hab | ⟨⟨_, hyb⟩ | ⟨_, hxb⟩⟩
· exact (hb (hab.symm.trans ha)).elim
· simpa [mergeToward, ha, hb] using hyb
· exact (hb hxb.symm).elim
· by_cases hb : s.head b = s.head x
· constructor
· intro h
simp [mergeToward, ha, hb] at h
exact Or.inr (Or.inr ⟨h, hb.symm⟩)
· intro h
rcases h with hab | ⟨⟨hax, _⟩ | ⟨hay, _⟩⟩
· exact (ha (hab.trans hb)).elim
· exact (ha hax).elim
· simpa [mergeToward, ha, hb] using hay
· constructor
· intro h
exact Or.inl (by simpa [mergeToward, ha, hb] using h)
· intro h
rcases h with hab | ⟨⟨hax, _⟩ | ⟨_, hxb⟩⟩
· simpa [mergeToward, ha, hb] using hab
· exact (ha hax).elim
· exact (hb hxb.symm).elimWeighted union implements the Section 19.1 abstract union operation.
theorem weightedUnion_sameSet_iff (s : State n) (x y a b : Fin n) :
(s.weightedUnion x y).1.sameSet a b ↔
s.sameSet a b ∨
(s.sameSet a x ∧ s.sameSet y b) ∨
(s.sameSet a y ∧ s.sameSet x b) := by
by_cases hxy : s.sameSet x y
· change s.head x = s.head y at hxy
rw [show s.weightedUnion x y = (s, 0) by simp [weightedUnion, hxy]]
change s.sameSet a b ↔ _
constructor
· exact Or.inl
· intro h
rcases h with hab | ⟨⟨hax, hyb⟩ | ⟨hay, hxb⟩⟩
· exact hab
· exact (s.partition.trans (s.partition.trans hax hxy) hyb)
· exact (s.partition.trans (s.partition.trans hay
(s.partition.symm hxy)) hxb)
· by_cases hle : s.setSize x ≤ s.setSize y
· change ¬s.head x = s.head y at hxy
rw [show s.weightedUnion x y = (s.mergeToward x y, s.setSize x) by
simp [weightedUnion, hxy, hle]]
exact s.mergeToward_sameSet_iff hxy a b
· have hyx : ¬s.sameSet y x := by
intro h
exact hxy h.symm
change ¬s.head x = s.head y at hxy
change ¬s.head y = s.head x at hyx
rw [show s.weightedUnion x y = (s.mergeToward y x, s.setSize y) by
simp [weightedUnion, hxy, hle]]
rw [s.mergeToward_sameSet_iff hyx]
aesopWeighted union refines the abstract partition merge.
theorem weightedUnion_refines_merge (s : State n) (x y a b : Fin n) :
((s.weightedUnion x y).1.partition).sameSet a b ↔
((s.partition).merge x y).sameSet a b := by
rw [Partition.merge_sameSet_iff]
exact s.weightedUnion_sameSet_iff x y a bRedirecting a class preserves idempotent representative pointers.
theorem mergeToward_preserves_headInvariant (s : State n) {x y : Fin n}
(hinv : s.HeadInvariant) (hxy : ¬s.sameSet x y) :
(s.mergeToward x y).HeadInvariant := by
intro z
unfold sameSet at hxy
by_cases hz : s.head z = s.head x
· have hyx : ¬s.head (s.head y) = s.head x := by
rw [hinv y]
exact Ne.symm hxy
simpa [mergeToward, hz, hyx] using hinv y
· have hzx : ¬s.head (s.head z) = s.head x := by
rw [hinv z]
exact hz
simpa [mergeToward, hz, hzx] using hinv zWeighted union preserves the representative invariant.
theorem weightedUnion_preserves_headInvariant (s : State n) (x y : Fin n)
(hinv : s.HeadInvariant) :
(s.weightedUnion x y).1.HeadInvariant := by
by_cases hxy : s.sameSet x y
· change s.head x = s.head y at hxy
simpa [weightedUnion, hxy] using hinv
· by_cases hle : s.setSize x ≤ s.setSize y
· change ¬s.head x = s.head y at hxy
simpa [weightedUnion, hxy, hle] using
s.mergeToward_preserves_headInvariant hinv hxy
· have hyx : ¬s.sameSet y x := fun h => hxy h.symm
change ¬s.head x = s.head y at hxy
change ¬s.head y = s.head x at hyx
simpa [weightedUnion, hxy, hle] using
s.mergeToward_preserves_headInvariant hinv hyxA weighted union never charges more rewrites than either input set size.
theorem weightedUnion_cost_le_left (s : State n) (x y : Fin n) :
(s.weightedUnion x y).2 ≤ s.setSize x := by
simp only [weightedUnion]
split <;> rename_i hxy
· exact Nat.zero_le _
· split <;> rename_i hle
· exact Nat.le_refl _
· exact Nat.le_of_lt (Nat.lt_of_not_ge hle)Symmetric rewrite-cost bound for weighted union.
theorem weightedUnion_cost_le_right (s : State n) (x y : Fin n) :
(s.weightedUnion x y).2 ≤ s.setSize y := by
simp only [weightedUnion]
split <;> rename_i hxy
· exact Nat.zero_le _
· split <;> rename_i hle
· exact hle
· exact Nat.le_refl _If an element's representative pointer changes, the size recorded at its new representative is at least twice the old represented-set size.
theorem weightedUnion_changed_doubles (s : State n) (x y z : Fin n)
(hchanged : (s.weightedUnion x y).1.head z ≠ s.head z) :
2 * s.setSize z ≤ (s.weightedUnion x y).1.setSize z := by
by_cases hxy : s.sameSet x y
· change s.head x = s.head y at hxy
rw [show s.weightedUnion x y = (s, 0) by simp [weightedUnion, hxy]] at hchanged
exact (hchanged rfl).elim
· by_cases hle : s.setSize x ≤ s.setSize y
· change ¬s.head x = s.head y at hxy
rw [show s.weightedUnion x y = (s.mergeToward x y, s.setSize x) by
simp [weightedUnion, hxy, hle]] at hchanged ⊢
have hz : s.head z = s.head x := by
by_contra hz
apply hchanged
simp [mergeToward, hz]
unfold setSize at hle ⊢
simp [mergeToward, setSize, hz]
omega
· change ¬s.head x = s.head y at hxy
rw [show s.weightedUnion x y = (s.mergeToward y x, s.setSize y) by
simp [weightedUnion, hxy, hle]] at hchanged ⊢
have hz : s.head z = s.head y := by
by_contra hz
apply hchanged
simp [mergeToward, hz]
have hyx : s.head y ≠ s.head x := by
exact fun h => hxy h.symm
unfold setSize at hle ⊢
simp [mergeToward, setSize, hz]
omegaRepeated size doublings give an exponential lower bound.
theorem pow_le_of_repeated_doubling (sizeAt : Nat → Nat) (k : Nat)
(hzero : 1 ≤ sizeAt 0)
(hstep : ∀ i, i < k → 2 * sizeAt i ≤ sizeAt (i + 1)) :
2 ^ k ≤ sizeAt k := by
induction k with
| zero => simpa using hzero
| succ k ih =>
have ih' : 2 ^ k ≤ sizeAt k :=
ih (fun i hi => hstep i (Nat.lt_trans hi (Nat.lt_succ_self k)))
calc
2 ^ (k + 1) = 2 * 2 ^ k := by
rw [Nat.pow_succ]
omega
_ ≤ 2 * sizeAt k := Nat.mul_le_mul_left 2 ih'
_ ≤ sizeAt (k + 1) := hstep k (Nat.lt_succ_self k)An element whose class doubles on every move is rewritten at most logarithmically often.
theorem move_count_le_log2 {sizeAt : Nat → Nat} {k total : Nat}
(htotal : total ≠ 0)
(hzero : 1 ≤ sizeAt 0)
(hstep : ∀ i, i < k → 2 * sizeAt i ≤ sizeAt (i + 1))
(hfinal : sizeAt k ≤ total) :
k ≤ Nat.log2 total := by
apply (Nat.le_log2 htotal).2
exact Nat.le_trans (pow_le_of_repeated_doubling sizeAt k hzero hstep) hfinal
Summing the per-element logarithmic rewrite bound gives n log n.
theorem total_rewrites_le_n_mul_log2 (moves : Fin n → Nat)
(hmoves : ∀ z, moves z ≤ Nat.log2 n) :
∑ z, moves z ≤ n * Nat.log2 n := by
calc
∑ z, moves z ≤ ∑ _z : Fin n, Nat.log2 n := by
exact Finset.sum_le_sum fun z _ => hmoves z
_ = n * Nat.log2 n := by simpend Stateend LinkedListend Chapter21end CLRS