Imports
import Mathlib
import CLRSLean.Probability.FiniteExpectation
import CLRSLean.FourthEdition.Chapter_11.Section_11_2_Chained_Hash_Tables11.5. Perfect Hashing
This section formalises the two-level perfect-hashing scheme of CLRS §11.5: a
static key set is stored using a primary universal hash into m = n buckets,
and each bucket with n_j keys gets a secondary table of size m_j = n_j²,
which — by the birthday-style collision count — is collision-free with probability
≥ 1/2. The total expected secondary storage is O(n).
Main results:
-
Definition
PerfectHashTable: two-level perfect hash data structure. -
Theorem
perfectSearch_iff_mem: membership correctness (perfectSearch T x ↔ x ∈ T.keys), establishingO(1)worst-case search. -
Theorem
perfectHash_collision_free_prob_ge_half(Theorem 11.9): when hashingnkeys intom = n²slots under a universal family, the hash is collision-free with probability at least1/2. -
Theorem
exists_collision_free_secondary: forn ≥ 2keys inton²slots, an injective secondary hash exists. -
Theorem
perfectHash_expected_total_space_lt_2n(Theorem 11.10): whennkeys are hashed uniformly and independently intom = nprimary buckets, the expected total secondary storageE[Σ_j n_j²]is less than2n(henceO(n)). -
Theorem
perfectHash_expected_trials_le_two: in a truncated model oftindependent trials, the expected number of trials until a collision-free secondary hash is at most2(geometric bound with success probability ≥ 1/2). -
Theorem
perfectHash_expected_construction_time_le_const_n: the expected abstract budgetconstructionCostis less than5n. This formula is not itself a counter attached to a table constructor.
The Construction companion supplies an executed finite-trial constructor
with a guaranteed collision-free terminal fallback, array initialization and
placement, and a separate expected-work refinement to this abstract budget.
Secondary injectivity is required only for stored keys in the relevant bucket.
The sampling model uses all local-index hash assignments under SUHA, not a
constructed family of constant-time hash programs on arbitrary original keys.
RAM, hash-code generation, and arithmetic implementation costs are excluded.
Notation conventions used in this section:
-
n: number of keys -
m: number of primary buckets (andm = nfor Theorem 11.10) -
a : Fin n → Fin m: a hash assignment (SUHA independent-uniform model) -
A : Fin t → (Fin n → Fin (n^2)): a sequence oftindependent trial hashes ofnkeys inton²slots (construction-trial model) -
H : ι → (K → Fin m): a universal family of hash functions -
n_j: number of keys assigned to primary bucketj
namespace CLRSnamespace Chapter11open CLRS.Probabilityopen Finsetopen scoped ClassicalTwo-level perfect hash model
A PerfectHashTable for a finite set of keys uses a primary hash into m buckets
and, for each bucket j, a secondary hash that is collision-free on the keys
assigned to that bucket. The deterministic two-level lookup completes in O(1)
worst-case time (two table lookups, independent of n).
The fields sec and table are per-bucket; the invariant sec_inj ensures no two
stored keys in the same primary bucket share a secondary slot. Nonmembers may
collide with stored keys; lookup checks the stored value, so
table j (sec j x) = some x identifies x uniquely.
The set of keys stored in the table.
Primary hash function mapping each key to a primary bucket.
For each primary bucket j, a secondary hash function mapping keys to slot
indices. The codomain is ℕ; the actual table size per bucket is not needed for
correctness, only for the probabilistic space bound.
The secondary table: for each bucket j and slot s, optionally a key.
The secondary hash is collision-free on the keys in each primary bucket:
if x and y are both in keys, map to the same primary bucket, and get the
same secondary slot, then x = y.
Every key is stored in the table at the slot determined by its primary and secondary hash.
If the table stores a key at a slot, that key maps to that slot.
structure PerfectHashTable (K : Type) [DecidableEq K] (m : ℕ) : Type where keys : Finset K prim : K → Fin m sec : Fin m → K → ℕ table : Fin m → ℕ → Option K sec_inj : ∀ (j : Fin m) (x y : K),
x ∈ keys → y ∈ keys → prim x = j → prim y = j → sec j x = sec j y → x = y table_stores_keys : ∀ x ∈ keys, table (prim x) (sec (prim x) x) = some x table_only_keys : ∀ (j : Fin m) (s : ℕ) (x : K),
table j s = some x → x ∈ keys ∧ prim x = j ∧ sec j x = s
Two-level perfect-hash search: compute the primary bucket j = prim x, the
secondary slot s = sec j x, and check whether table j s holds x.
def perfectSearch [DecidableEq K] (T : PerfectHashTable K m) (x : K) : Prop :=
T.table (T.prim x) (T.sec (T.prim x) x) = some x
Membership correctness of two-level perfect-hash search. A key x is found
by perfectSearch exactly when x ∈ T.keys (CLRS §11.5). This establishes
O(1) worst-case search time (two table lookups).
theorem perfectSearch_iff_mem [DecidableEq K] (T : PerfectHashTable K m) (x : K) :
perfectSearch T x ↔ x ∈ T.keys := by
constructor
· intro h
have hmem := T.table_only_keys (T.prim x) (T.sec (T.prim x) x) x h
exact hmem.1
· intro h
exact T.table_stores_keys x hTheorem 11.9: secondary collision-free with probability at least 1/2
The fintypeExpect operator is monotone: if X ω ≤ Y ω for all ω, then
E[X] ≤ E[Y].
theorem fintypeExpect_mono {Ω : Type} [Fintype Ω] [DecidableEq Ω] {X Y : Ω → ℝ}
(hXY : ∀ ω, X ω ≤ Y ω) : fintypeExpect X ≤ fintypeExpect Y := by
unfold fintypeExpect
refine div_le_div_of_nonneg_right (Finset.sum_le_sum (fun ω _ => hXY ω)) ?_
positivity
fintypeExpect of a negated random variable is the negation of the expectation.
theorem fintypeExpect_neg {Ω : Type} [Fintype Ω] [DecidableEq Ω] (X : Ω → ℝ) :
fintypeExpect (fun ω => -X ω) = -fintypeExpect X := by
simp [fintypeExpect, Finset.sum_neg_distrib, neg_div]
Number of colliding unordered pairs {i, j} with i < j under a hash assignment
a : Fin n → Fin m. Each pair of distinct indices that hash to the same bucket
contributes 1.
noncomputable def collisionCount {m n : ℕ} (a : Fin n → Fin m) : ℝ :=
∑ i : Fin n, ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0)
collisionCount is nonnegative.
theorem collisionCount_nonneg {m n : ℕ} (a : Fin n → Fin m) : 0 ≤ collisionCount a := by
unfold collisionCount
apply Finset.sum_nonneg; intro i hi
apply Finset.sum_nonneg; intro j hj
by_cases h : i < j
· have : 0 ≤ indicator (a i = a j) := by
unfold indicator; split <;> norm_num
simp [h, this]
· simp [h, indicator]
Expected collisions under pairwise independent hashing (SUHA). For hash
assignments a : Fin n → Fin m, the expected number of colliding unordered pairs
is exactly n(n-1)/(2m).
theorem expectedCollisions_suha {m n : ℕ} (hm : 0 < m) :
fintypeExpect (fun a : Fin n → Fin m => collisionCount a)
= (n : ℝ) * ((n : ℝ) - 1) / (2 * (m : ℝ)) := by
haveI : Nonempty (Fin m) := ⟨⟨0, hm⟩⟩
unfold collisionCount
have hE : fintypeExpect (fun a : Fin n → Fin m =>
∑ i : Fin n, ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0))
= ∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 / (m : ℝ)) else 0) := by
rw [fintypeExpect_sum Finset.univ (fun (i : Fin n) (a : Fin n → Fin m) =>
∑ j : Fin n, (if i < j then indicator (a i = a j) else 0))]
refine Finset.sum_congr rfl (fun i _ => ?_)
rw [fintypeExpect_sum Finset.univ (fun (j : Fin n) (a : Fin n → Fin m) =>
if i < j then indicator (a i = a j) else 0)]
refine Finset.sum_congr rfl (fun j _ => ?_)
by_cases hlt : i < j
· simp [hlt, pairCollisionProb i j (ne_of_lt hlt) hm]
· have hcard : Fintype.card (Fin n → Fin m) ≠ 0 := Fintype.card_ne_zero
simp [hlt, fintypeExpect_const hcard 0]
have hpair : (∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 / (m : ℝ)) else 0))
= (1 / (m : ℝ)) * ((n : ℝ) * ((n : ℝ) - 1) / 2) := by
rw [← sum_upper_triangle n, Finset.mul_sum]
refine Finset.sum_congr rfl (fun i _ => ?_)
rw [Finset.mul_sum]
refine Finset.sum_congr rfl (fun j _ => ?_)
by_cases h : i < j <;> simp [h]
rw [hE, hpair]
ring
When n keys are hashed into m = n² slots under SUHA, the expected number of
collisions is less than 1/2 (for n ≥ 2).
theorem expectedCollisions_sq_lt_half {n : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun a : Fin n → Fin (n^2) => collisionCount a) < 1/2 := by
have hm : 0 < n^2 := by
have hnpos : 0 < n := by omega
positivity
rw [expectedCollisions_suha hm]
have hcalc : (n : ℝ) * ((n : ℝ) - 1) / (2 * ((n : ℝ)^2)) < 1/2 := by
have hnpos' : (n : ℝ) > 0 := by exact_mod_cast (show 0 < n from by omega)
have hpos : (0 : ℝ) < 2 * ((n : ℝ)^2) := by positivity
field_simp [hpos.ne']
nlinarith
have hden : ((n ^ 2 : ℕ) : ℝ) = (n : ℝ)^2 := by simp
simpa [hden] using hcalc
Markov's inequality for nonnegative-integer-valued random variables. If
X : Ω → ℕ, then P[X ≥ 1] ≤ E[X].
theorem markov_integer {Ω : Type} [Fintype Ω] [DecidableEq Ω] (X : Ω → ℕ) :
fintypeExpect (fun ω => if X ω ≥ 1 then (1 : ℝ) else 0) ≤
fintypeExpect (fun ω => (X ω : ℝ)) := by
have hpoint : ∀ ω, (if X ω ≥ 1 then (1 : ℝ) else 0) ≤ (X ω : ℝ) := by
intro ω
by_cases h : X ω ≥ 1
· have h' : (1 : ℝ) ≤ (X ω : ℝ) := by exact_mod_cast h
simp [h, h']
· simp [h]
exact fintypeExpect_mono hpoint
Theorem 11.9 (Perfect hashing: secondary collision-free with probability ≥ 1/2).
Let n keys be hashed into m = n² slots under a universal family (or under SUHA).
Then the hash assignment is collision-free with probability at least 1/2.
Equivalently, a secondary table of size n_j² for a bucket with n_j keys is
collision-free with probability at least 1/2 (CLRS Theorem 11.9).
theorem perfectHash_collision_free_prob_ge_half {n : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun a : Fin n → Fin (n^2) =>
indicator (∀ i j : Fin n, a i = a j → i = j))
≥ 1/2 := by
have hm : 0 < n^2 := by
have hnpos' : 0 < n := by omega
positivity
have hnpos : 0 < n := by omega
haveI : Nonempty (Fin n) := ⟨⟨0, hnpos⟩⟩
haveI : Nonempty (Fin n → Fin (n^2)) :=
⟨fun _ => ⟨0, show 0 < n^2 from hm⟩⟩
have hcard : Fintype.card (Fin n → Fin (n^2)) ≠ 0 := Fintype.card_ne_zero
-- `X a` is the number of colliding unordered pairs under `a`, as a ℕ.
let X (a : Fin n → Fin (n^2)) : ℕ :=
(Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))).card
-- `X a = 0` exactly when `a` is injective (collision-free)
have h_inj_iff : ∀ a : Fin n → Fin (n^2),
(∀ i j : Fin n, a i = a j → i = j) ↔ X a = 0 := by
intro a
dsimp [X]
constructor
· intro hinj
apply Finset.card_eq_zero.mpr
apply Finset.not_nonempty_iff_eq_empty.mp
intro hne
rcases hne with ⟨p, hp⟩
rcases Finset.mem_filter.mp hp with ⟨hp_univ, ⟨hlt, heq⟩⟩
exact hlt.ne' (hinj p.1 p.2 heq).symm
· intro hzero
intro i j heq
by_contra! hne
rcases lt_trichotomy i j with (hlt | heq' | hlt)
· have hmem : (i, j) ∈ Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n)) := by
simp [hlt, heq]
have hcard_ne_zero : (Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))).card ≠ 0 :=
Finset.card_ne_zero.mpr ⟨(i, j), hmem⟩
rw [hzero] at hcard_ne_zero
exact hcard_ne_zero rfl
· exact hne heq'
· have hmem : (j, i) ∈ Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n)) := by
simp [hlt, heq.symm]
have hcard_ne_zero : (Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))).card ≠ 0 :=
Finset.card_ne_zero.mpr ⟨(j, i), hmem⟩
rw [hzero] at hcard_ne_zero
exact hcard_ne_zero rfl
-- Rewrite the collision-free indicator in terms of `X`
have h_indicator_eq : (fun a : Fin n → Fin (n^2) => indicator (∀ i j : Fin n, a i = a j → i = j))
= (fun a : Fin n → Fin (n^2) => if X a = 0 then (1 : ℝ) else 0) := by
funext a; simp [indicator, h_inj_iff a]
rw [h_indicator_eq]
-- Relate `collisionCount` (real-valued) to `X` (ℕ-valued)
have h_collision_eq : (fun (a : Fin n → Fin (n^2)) => collisionCount a) =
(fun (a : Fin n → Fin (n^2)) => (X a : ℝ)) := by
funext a
dsimp [collisionCount, X]
have h1 : (∑ i : Fin n, ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0)) =
(∑ p : Fin n × Fin n, (if p.1 < p.2 then indicator (a p.1 = a p.2) else 0)) := by
simp [Fintype.sum_prod_type]
have h2 : (∑ p : Fin n × Fin n, (if p.1 < p.2 then indicator (a p.1 = a p.2) else 0)) =
(∑ p : Fin n × Fin n, (if p.1 < p.2 ∧ a p.1 = a p.2 then (1 : ℝ) else 0)) := by
refine Finset.sum_congr rfl (fun p _ => ?_)
by_cases hlt : p.1 < p.2
· simp [hlt, indicator]
· simp [hlt, indicator]
have h3 : (∑ p : Fin n × Fin n, (if p.1 < p.2 ∧ a p.1 = a p.2 then (1 : ℝ) else 0)) =
(Finset.card (Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))) : ℝ) := by
simp [Finset.sum_filter]
calc
collisionCount a = (∑ i : Fin n, ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0)) := rfl
_ = (∑ p : Fin n × Fin n, (if p.1 < p.2 then indicator (a p.1 = a p.2) else 0)) := h1
_ = (∑ p : Fin n × Fin n, (if p.1 < p.2 ∧ a p.1 = a p.2 then (1 : ℝ) else 0)) := h2
_ = (Finset.card (Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))) : ℝ) := h3
_ = (X a : ℝ) := rfl
have h_expected_X_lt_half :
fintypeExpect (fun a : Fin n → Fin (n^2) => (X a : ℝ)) < 1/2 := by
rw [← h_collision_eq]
exact expectedCollisions_sq_lt_half hn
-- Markov inequality: P[X ≥ 1] ≤ E[X]
have h_markov : fintypeExpect (fun a : Fin n → Fin (n^2) => if X a ≥ 1 then (1 : ℝ) else 0) ≤
fintypeExpect (fun a : Fin n → Fin (n^2) => (X a : ℝ)) :=
markov_integer X
-- `indicator(X = 0) = 1 - indicator(X ≥ 1)`
have h_decomp : (fun a : Fin n → Fin (n^2) => (if X a = 0 then (1 : ℝ) else 0)) =
(fun a : Fin n → Fin (n^2) => (1 : ℝ) - (if X a ≥ 1 then (1 : ℝ) else 0)) := by
funext a
by_cases h : X a = 0
· simp [h]
· have hpos : X a ≥ 1 := Nat.one_le_of_lt (Nat.pos_of_ne_zero h)
simp [h, hpos]
rw [h_decomp]
have h_expect_sub : fintypeExpect (fun a : Fin n → Fin (n^2) =>
(1 : ℝ) - (if X a ≥ 1 then (1 : ℝ) else 0)) =
(1 : ℝ) - fintypeExpect (fun a : Fin n → Fin (n^2) => (if X a ≥ 1 then (1 : ℝ) else 0)) := by
calc
fintypeExpect (fun a : Fin n → Fin (n^2) =>
(1 : ℝ) - (if X a ≥ 1 then (1 : ℝ) else 0))
= fintypeExpect (fun a : Fin n → Fin (n^2) =>
(1 : ℝ) + (-(if X a ≥ 1 then (1 : ℝ) else 0))) := by
refine congrArg fintypeExpect (funext fun a => ?_)
rfl
_ = fintypeExpect (fun _ : Fin n → Fin (n^2) => (1 : ℝ)) +
fintypeExpect (fun a : Fin n → Fin (n^2) =>
-(if X a ≥ 1 then (1 : ℝ) else 0)) :=
fintypeExpect_add _ _
_ = (1 : ℝ) + (-fintypeExpect (fun a : Fin n → Fin (n^2) =>
(if X a ≥ 1 then (1 : ℝ) else 0))) := by
simp [fintypeExpect_const hcard, fintypeExpect_neg]
_ = (1 : ℝ) - fintypeExpect (fun a : Fin n → Fin (n^2) =>
(if X a ≥ 1 then (1 : ℝ) else 0)) := by ring
rw [h_expect_sub]
have h_bound : fintypeExpect (fun a : Fin n → Fin (n^2) => if X a ≥ 1 then (1 : ℝ) else 0) < 1/2 := by
linarith
linarithTheorem 11.10: expected total space O(n)
The number of keys (out of n) that hash to a given bucket j under assignment a.
noncomputable def bucketSize {m n : ℕ} (a : Fin n → Fin m) (j : Fin m) : ℝ :=
∑ i : Fin n, indicator (a i = j)
The total secondary storage for a hash assignment a: sum over buckets of the
square of the bucket size, i.e. Σ_j n_j². This is the space used if each
bucket j gets a secondary table of size n_j².
noncomputable def totalSecondarySpace {m n : ℕ} (a : Fin n → Fin m) : ℝ :=
∑ j : Fin m, (bucketSize a j) ^ 2
The algebraic identity Σ_j n_j² = Σ_i Σ_k indicator(a i = a k). This expands
the sum of squares into a double sum over key pairs (CLRS proof of Theorem 11.10).
theorem totalSecondarySpace_eq_sum_indicator {m n : ℕ} (a : Fin n → Fin m) :
totalSecondarySpace a = ∑ i : Fin n, ∑ k : Fin n, indicator (a i = a k) := by
unfold totalSecondarySpace bucketSize
calc
∑ j : Fin m, ((∑ i : Fin n, indicator (a i = j)) : ℝ) ^ 2
= ∑ j : Fin m, (∑ i : Fin n, indicator (a i = j)) * (∑ k : Fin n, indicator (a k = j)) := by
simp [sq]
_ = ∑ j : Fin m, ∑ i : Fin n, ∑ k : Fin n, indicator (a i = j) * indicator (a k = j) := by
refine Finset.sum_congr rfl (fun j hj => ?_)
calc
(∑ i : Fin n, indicator (a i = j)) * (∑ k : Fin n, indicator (a k = j))
= ∑ k : Fin n, (∑ i : Fin n, indicator (a i = j)) * indicator (a k = j) := by
rw [Finset.mul_sum]
_ = ∑ k : Fin n, ∑ i : Fin n, indicator (a i = j) * indicator (a k = j) := by
refine Finset.sum_congr rfl (fun k hk => ?_)
rw [Finset.sum_mul]
_ = ∑ i : Fin n, ∑ k : Fin n, indicator (a i = j) * indicator (a k = j) := by
rw [Finset.sum_comm]
_ = ∑ i : Fin n, ∑ k : Fin n, ∑ j : Fin m, indicator (a i = j) * indicator (a k = j) := by
calc
∑ j : Fin m, ∑ i : Fin n, ∑ k : Fin n, indicator (a i = j) * indicator (a k = j)
= ∑ i : Fin n, ∑ j : Fin m, ∑ k : Fin n, indicator (a i = j) * indicator (a k = j) := by
rw [Finset.sum_comm]
_ = ∑ i : Fin n, ∑ k : Fin n, ∑ j : Fin m, indicator (a i = j) * indicator (a k = j) := by
refine Finset.sum_congr rfl (fun i hi => ?_)
rw [Finset.sum_comm]
_ = ∑ i : Fin n, ∑ k : Fin n, indicator (a i = a k) := by
refine Finset.sum_congr rfl (fun i _ => Finset.sum_congr rfl (fun k _ => ?_))
simp [indicator, Finset.sum_ite_eq, Finset.mem_univ]
Theorem 11.10 (Expected total space is O(n)). When n keys are hashed
uniformly and independently into m = n primary buckets, the expected total
secondary storage E[Σ_j n_j²] is strictly less than 2n (CLRS Theorem 11.10).
theorem perfectHash_expected_total_space_lt_2n {n : ℕ} (hn : 0 < n) :
fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) < 2 * (n : ℝ) := by
have hm : 0 < n := hn
haveI : Nonempty (Fin n) := ⟨⟨0, hn⟩⟩
have hcard : Fintype.card (Fin n → Fin n) ≠ 0 := Fintype.card_ne_zero
-- Algebraic identity: Σ_j n_j² = n + Σ_{i≠k} indicator(a i = a k)
have h_identity : ∀ a : Fin n → Fin n,
totalSecondarySpace a = (n : ℝ) + ∑ i : Fin n, ∑ k : Fin n,
(if i ≠ k then indicator (a i = a k) else 0) := by
intro a
calc
totalSecondarySpace a = ∑ i : Fin n, ∑ k : Fin n, indicator (a i = a k) :=
totalSecondarySpace_eq_sum_indicator a
_ = (∑ i : Fin n, indicator (a i = a i)) +
(∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
calc
∑ i : Fin n, ∑ k : Fin n, indicator (a i = a k)
= ∑ i : Fin n, (indicator (a i = a i) + ∑ k : Fin n,
(if i ≠ k then indicator (a i = a k) else 0)) := by
refine Finset.sum_congr rfl (fun i hi => ?_)
have h_inner : ∑ k : Fin n, indicator (a i = a k)
= indicator (a i = a i) + ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := by
calc
∑ k : Fin n, indicator (a i = a k)
= ∑ k : Fin n, ((if i = k then indicator (a i = a i) else 0) +
(if i ≠ k then indicator (a i = a k) else 0)) := by
refine Finset.sum_congr rfl (fun k hk => ?_)
by_cases hik : i = k
· subst hik; simp
· simp [hik]
_ = (∑ k : Fin n, (if i = k then indicator (a i = a i) else 0)) +
(∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
simp [Finset.sum_add_distrib]
_ = indicator (a i = a i) + ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := by
simp [Finset.sum_ite_eq, Finset.mem_univ]
calc
∑ k : Fin n, indicator (a i = a k)
= indicator (a i = a i) + ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := h_inner
_ = indicator (a i = a i) + ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := rfl
_ = (∑ i : Fin n, indicator (a i = a i)) +
(∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
simp [Finset.sum_add_distrib]
_ = (n : ℝ) + ∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := by
simp [indicator, Finset.sum_const, Finset.card_univ, Fintype.card_fin]
-- Use the identity inside the expectation
have h_expect_identity :
fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) =
(n : ℝ) + fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
calc
fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) =
fintypeExpect (fun a : Fin n → Fin n => (n : ℝ) + ∑ i : Fin n, ∑ k : Fin n,
(if i ≠ k then indicator (a i = a k) else 0)) := by
refine congrArg fintypeExpect (funext h_identity)
_ = fintypeExpect (fun _ : Fin n → Fin n => (n : ℝ)) +
fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) :=
fintypeExpect_add _ _
_ = (n : ℝ) + fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
simp [fintypeExpect_const hcard, Fintype.card_fin]
rw [h_expect_identity]
-- Compute the remaining expectation: E[Σ_{i≠k} indicator(a i = a k)] = n*(n-1)*(1/n) = n-1
have h_cross_expect :
fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0))
= (n : ℝ) - 1 := by
calc
fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0))
= ∑ i : Fin n, fintypeExpect (fun a : Fin n → Fin n =>
∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
rw [fintypeExpect_sum Finset.univ]
_ = ∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then
fintypeExpect (fun a : Fin n → Fin n => indicator (a i = a k)) else 0) := by
refine Finset.sum_congr rfl (fun i _ => ?_)
rw [fintypeExpect_sum Finset.univ]
refine Finset.sum_congr rfl (fun k _ => ?_)
by_cases hne : i ≠ k
· simp [hne, pairCollisionProb i k hne hm]
· simp [hne, fintypeExpect_const hcard, Fintype.card_fin]
_ = ∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then (1 / (n : ℝ)) else 0) := by
refine Finset.sum_congr rfl (fun i _ => Finset.sum_congr rfl (fun k _ => ?_))
by_cases hne : i ≠ k
· rw [pairCollisionProb i k hne hm]
· simp [hne]
_ = (n : ℝ) - 1 := by
have h_inner_sum : ∀ i : Fin n, (∑ k : Fin n, (if i ≠ k then (1 / (n : ℝ)) else 0)) = ((n : ℝ) - 1) / (n : ℝ) := by
intro i
calc
(∑ k : Fin n, (if i ≠ k then (1 / (n : ℝ)) else 0))
= (∑ k : Fin n, (if i ≠ k then (1 : ℝ) else 0)) * (1 / (n : ℝ)) := by
simp [Finset.mul_sum, mul_comm]
_ = ((n : ℝ) - 1) * (1 / (n : ℝ)) := by
have hsum : (∑ k : Fin n, (if i ≠ k then (1 : ℝ) else 0)) = (n : ℝ) - 1 := by
calc
(∑ k : Fin n, (if i ≠ k then (1 : ℝ) else 0))
= (∑ k : Fin n, ((1 : ℝ) - (if i = k then (1 : ℝ) else 0))) := by
refine Finset.sum_congr rfl (fun k hk => ?_)
by_cases hik : i = k
· subst hik; simp
· simp [hik]
_ = (∑ k : Fin n, (1 : ℝ)) - (∑ k : Fin n, (if i = k then (1 : ℝ) else 0)) := by
simp [Finset.sum_add_distrib]
_ = (n : ℝ) - 1 := by simp [Fintype.card_fin, Finset.sum_ite_eq, Finset.mem_univ]
rw [hsum]
_ = ((n : ℝ) - 1) / (n : ℝ) := by ring
calc
(∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then (1 / (n : ℝ)) else 0))
= ∑ i : Fin n, (((n : ℝ) - 1) / (n : ℝ)) := by
refine Finset.sum_congr rfl (fun i hi => ?_); rw [h_inner_sum i]
_ = (n : ℝ) * (((n : ℝ) - 1) / (n : ℝ)) := by
simp [Finset.sum_const, Finset.card_univ, Fintype.card_fin]
_ = (n : ℝ) - 1 := by
field_simp [show (n : ℝ) ≠ 0 from by exact_mod_cast hn.ne']
rw [h_cross_expect]
nlinarithConstruction trials: expected trials to a collision-free secondary hash
A hash assignment a : Fin n → Fin (n^2) of n keys into n² slots is
collision-free exactly when it is injective.
abbrev collisionFree {n : ℕ} (a : Fin n → Fin (n^2)) : Prop :=
∀ i j : Fin n, a i = a j → i = j
Split a sequence of k + 1 independent trial hashes into the first k
trials and the last trial. This witnesses that the prefix coordinates are
independent of the last coordinate.
noncomputable def trialsSplitLast {n k : ℕ} :
(Fin (k + 1) → (Fin n → Fin (n^2))) ≃
(Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) where
toFun A := ((fun j : Fin k => A (Fin.castSucc j)), A ⟨k, Nat.lt_succ_self k⟩)
invFun q := fun x : Fin (k + 1) =>
if hx : x.val < k then q.1 ⟨x.val, hx⟩ else q.2
left_inv A := by
funext x
by_cases hx : x.val < k
· simp [hx]
· simp [hx]
have hx' : x = ⟨k, Nat.lt_succ_self k⟩ := by
apply Fin.ext
change x.val = k
omega
rw [hx']
right_inv q := by
obtain ⟨P, L⟩ := q
refine Prod.ext ?_ ?_
· funext j
simp
· simp
Split a sequence of t independent trial hashes into the first k trials
and the remaining t - k trials, for k ≤ t. This witnesses that the prefix
coordinates are independent of the suffix coordinates.
noncomputable def trialsSplitPrefix {n t k : ℕ} (hkt : k ≤ t) :
(Fin t → (Fin n → Fin (n^2))) ≃
(Fin k → (Fin n → Fin (n^2))) × (Fin (t - k) → (Fin n → Fin (n^2))) where
toFun A := ((fun j : Fin k => A (Fin.castLE hkt j)),
(fun j : Fin (t - k) => A ⟨k + j.val, by omega⟩))
invFun q := fun x : Fin t =>
if hx : x.val < k then q.1 ⟨x.val, hx⟩ else q.2 ⟨x.val - k, by omega⟩
left_inv A := by
funext x
by_cases hx : x.val < k
· simp [hx]
· simp [hx]
apply congrArg A
apply Fin.ext
change k + (x.val - k) = x.val
omega
right_inv q := by
obtain ⟨P, S⟩ := q
refine Prod.ext ?_ ?_
· funext j
simp
· funext j
have hnot : ¬ k + j.val < k := by omega
simp [hnot]
Independent-trials failure bound. In k independent trials, each of which
produces a collision-free hash of n ≥ 2 keys into n² slots with probability
at least 1/2 (Theorem 11.9), the probability that all k trials fail is at
most (1/2)^k (CLRS §11.5).
theorem perfectHash_prefix_fail_prob_le {n k : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (A j))) ≤ (1/2 : ℝ)^k := by
induction k with
| zero =>
have htrue : ∀ A : Fin 0 → (Fin n → Fin (n^2)),
(∀ j : Fin 0, ¬ collisionFree (A j)) := by
intro A j
exact Fin.elim0 j
haveI : Nonempty (Fin n → Fin (n^2)) := ⟨fun _ => ⟨0, by
have hnpos : 0 < n := by omega
positivity⟩⟩
have hcard : Fintype.card (Fin 0 → (Fin n → Fin (n^2))) ≠ 0 := Fintype.card_ne_zero
calc
fintypeExpect (fun A : Fin 0 → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin 0, ¬ collisionFree (A j)))
= fintypeExpect (fun _ : Fin 0 → (Fin n → Fin (n^2)) => (1 : ℝ)) := by
refine congrArg fintypeExpect (funext fun A => ?_)
have hP : (∀ j : Fin 0, ¬ collisionFree (A j)) := htrue A
unfold indicator
rw [if_pos hP]
_ = 1 := by
simp [fintypeExpect_const hcard]
_ ≤ (1/2 : ℝ)^0 := by
simp
| succ k ih =>
have hnpos : 0 < n := by omega
haveI : Nonempty (Fin n) := ⟨⟨0, hnpos⟩⟩
haveI : Nonempty (Fin n → Fin (n^2)) := ⟨fun _ => ⟨0, by positivity⟩⟩
have hcardΩ : Fintype.card (Fin n → Fin (n^2)) ≠ 0 := Fintype.card_ne_zero
-- `∀ j : Fin (k+1), ¬ CF (A j)` splits as the prefix conjunction and the
-- last trial
have hsplit : ∀ A : Fin (k + 1) → (Fin n → Fin (n^2)),
indicator (∀ j : Fin (k + 1), ¬ collisionFree (A j))
= indicator ((∀ j : Fin k, ¬ collisionFree (A (Fin.castSucc j))) ∧
¬ collisionFree (A ⟨k, Nat.lt_succ_self k⟩)) := by
intro A
have hiff : (∀ j : Fin (k + 1), ¬ collisionFree (A j)) ↔
(∀ j : Fin k, ¬ collisionFree (A (Fin.castSucc j))) ∧
¬ collisionFree (A ⟨k, Nat.lt_succ_self k⟩) := by
constructor
· intro h
constructor
· intro j
exact h (Fin.castSucc j)
· exact h ⟨k, Nat.lt_succ_self k⟩
· rintro ⟨hpre, hlast⟩ j
by_cases hj : j.val < k
· have hj' : j = Fin.castSucc ⟨j.val, hj⟩ := by
ext
rfl
rw [hj']
exact hpre ⟨j.val, hj⟩
· have hj' : j = ⟨k, Nat.lt_succ_self k⟩ := by
apply Fin.ext
change j.val = k
omega
rw [hj']
exact hlast
unfold indicator
by_cases hP1 : ∀ j : Fin (k + 1), ¬collisionFree (A j)
· rw [if_pos hP1, if_pos (hiff.mp hP1)]
· rw [if_neg hP1, if_neg (fun hP2 => hP1 (hiff.mpr hP2))]
-- reindex the product sample space so the prefix and last trial separate
have he := fintypeExpect_equiv (trialsSplitLast (n := n) (k := k))
(fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2)))
have hLHS : fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k + 1), ¬ collisionFree (A j)))
= fintypeExpect (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2))) := by
calc
fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k + 1), ¬ collisionFree (A j)))
= fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (A (Fin.castSucc j))) ∧
¬ collisionFree (A ⟨k, Nat.lt_succ_self k⟩))) := by
refine congrArg fintypeExpect (funext fun A => hsplit A)
_ = fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree ((trialsSplitLast (n := n) (k := k) A).1 j)) ∧
¬ collisionFree ((trialsSplitLast (n := n) (k := k) A).2))) := by
refine congrArg fintypeExpect (funext fun A => ?_)
simp [trialsSplitLast]
_ = fintypeExpect (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2))) := he
-- the conjunction of the independent prefix event and the last-trial event
-- factorizes as a product of expectations
have hprod : fintypeExpect (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2)))
= fintypeExpect (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j)))
* fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a)) := by
have hsplit2 : (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2)))
= (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
(fun B : Fin k → (Fin n → Fin (n^2)) => indicator (∀ j : Fin k, ¬ collisionFree (B j))) p.1
* (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a)) p.2) := by
funext p
by_cases hpre : ∀ j : Fin k, ¬ collisionFree (p.1 j)
· by_cases hlast : ¬ collisionFree (p.2)
· simp [indicator, hpre, hlast]
· simp [indicator, hpre, hlast]
· by_cases hlast : ¬ collisionFree (p.2)
· simp [indicator, hpre, hlast]
· simp [indicator, hpre, hlast]
rw [hsplit2]
exact expect_mul_of_indep (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j)))
(fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a))
-- a single trial fails with probability at most 1/2 (Theorem 11.9)
have hfail : fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a)) ≤ 1/2 := by
have hE : fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a))
= 1 - fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (collisionFree a)) := by
calc
fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a))
= fintypeExpect (fun a : Fin n → Fin (n^2) => (1 : ℝ) - indicator (collisionFree a)) := by
refine congrArg fintypeExpect (funext fun a => ?_)
by_cases h : collisionFree a
· unfold indicator
rw [if_neg (fun hna => hna h), if_pos h]
simp
· unfold indicator
rw [if_pos h, if_neg h]
simp
_ = fintypeExpect (fun a : Fin n → Fin (n^2) => (1 : ℝ) + (-indicator (collisionFree a))) := by
refine congrArg fintypeExpect (funext fun a => ?_)
ring
_ = fintypeExpect (fun _ : Fin n → Fin (n^2) => (1 : ℝ)) +
fintypeExpect (fun a : Fin n → Fin (n^2) => -indicator (collisionFree a)) := fintypeExpect_add _ _
_ = 1 - fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (collisionFree a)) := by
rw [fintypeExpect_const hcardΩ, fintypeExpect_neg]
ring
rw [hE]
have hcf : 1/2 ≤ fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (collisionFree a)) := by
simpa [collisionFree] using (perfectHash_collision_free_prob_ge_half (n := n) hn)
linarith
-- assemble: E[prefix ∧ last] = E[prefix] · E[last] ≤ (1/2)^k · (1/2)
calc
fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k + 1), ¬ collisionFree (A j)))
= fintypeExpect (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j)))
* fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a)) := by
rw [hLHS, hprod]
_ ≤ (1/2 : ℝ)^k * (1/2) := by
exact mul_le_mul ih hfail
(fintypeExpect_nonneg (fun a : Fin n → Fin (n^2) => by
unfold indicator
split <;> norm_num))
(by positivity)
_ = (1/2 : ℝ)^(k + 1) := by
simp [pow_succ]
The probability that the first k of t independent trials all fail is at
most (1/2)^k: the remaining t - k trials are independent of the prefix and
marginalise out.
theorem perfectHash_trials_prefix_fail_prob_le {n k t : ℕ} (hn : 2 ≤ n) (hkt : k ≤ t) :
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (A (Fin.castLE hkt j)))) ≤ (1/2 : ℝ)^k := by
haveI : Nonempty (Fin n → Fin (n^2)) := ⟨fun _ => ⟨0, by
have hnpos : 0 < n := by omega
positivity⟩⟩
have hcard_suffix : Fintype.card (Fin (t - k) → (Fin n → Fin (n^2))) ≠ 0 := Fintype.card_ne_zero
have hpre : fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (A (Fin.castLE hkt j))))
= fintypeExpect (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j))) := by
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (A (Fin.castLE hkt j))))
= fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree ((trialsSplitPrefix hkt A).1 j))) := by
refine congrArg fintypeExpect (funext fun A => ?_)
simp [trialsSplitPrefix]
_ = fintypeExpect (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin (t - k) → (Fin n → Fin (n^2))) =>
indicator (∀ j : Fin k, ¬ collisionFree (p.1 j))) := by
exact fintypeExpect_equiv (trialsSplitPrefix hkt)
(fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin (t - k) → (Fin n → Fin (n^2))) =>
indicator (∀ j : Fin k, ¬ collisionFree (p.1 j)))
_ = fintypeExpect (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j))) := by
exact fintypeExpect_fst hcard_suffix
(fun B : Fin k → (Fin n → Fin (n^2)) => indicator (∀ j : Fin k, ¬ collisionFree (B j)))
rw [hpre]
exact perfectHash_prefix_fail_prob_le hn
The number of failed trials before the first collision-free trial in a
sequence of t independent trials. Equivalently, the sum over k of the
indicators "the first k + 1 trials all fail": each failing trial before the
first success contributes exactly one such prefix. If every trial fails, the
value is t.
noncomputable def failedTrials {n t : ℕ} (A : Fin t → (Fin n → Fin (n^2))) : ℕ :=
(Finset.univ.filter (fun k : Fin t =>
∀ j : Fin (k.val + 1), ¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))).card
failedTrials decomposes as the sum over trial prefixes of the indicator
that the first k + 1 trials all fail.
theorem failedTrials_eq_sum {n t : ℕ} (A : Fin t → (Fin n → Fin (n^2))) :
failedTrials A = ∑ k : Fin t, (if (∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j))) then 1 else 0) := by
unfold failedTrials
-- (Finset.univ.filter p).card = ∑ k ∈ univ, if p k then 1 else 0
simpa using (Finset.sum_boole (fun k : Fin t => ∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))
(Finset.univ : Finset (Fin t))).symm
Expected failed trials before a collision-free hash. In the truncated
model of t independent trials (each collision-free with probability at least
1/2, Theorem 11.9), the expected number of failed trials before the first
collision-free trial is at most 1 — via the tail-sum identity
E[F] = Σ_k P[first k trials fail] ≤ Σ_k (1/2)^k = 1 (CLRS §11.5).
theorem perfectHash_expected_failedTrials_le_one {n t : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ)) ≤ 1 := by
have hdecomp : (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ))
= (fun A : Fin t → (Fin n → Fin (n^2)) => ∑ k : Fin t,
indicator (∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))) := by
funext A
rw [failedTrials_eq_sum A]
simp [indicator, Nat.cast_sum]
have hlin : fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ))
= ∑ k : Fin t, fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))) := by
rw [hdecomp]
exact fintypeExpect_sum Finset.univ _
have hterm : ∀ k : Fin t, fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))) ≤ (1/2 : ℝ)^(k.val + 1) := by
intro k
exact perfectHash_trials_prefix_fail_prob_le hn (Nat.succ_le_of_lt k.isLt)
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ))
≤ ∑ k : Fin t, (1/2 : ℝ)^(k.val + 1) := by
rw [hlin]
exact Finset.sum_le_sum (fun k _ => hterm k)
_ ≤ 1 := by
-- ∑ k : Fin t, (1/2)^(k.val + 1) = (1/2) · ∑ k : Fin t, (1/2)^k.val ≤ (1/2) · 2 = 1
have hfactor : (∑ k : Fin t, (1/2 : ℝ)^(k.val + 1)) = (1/2) * (∑ k : Fin t, (1/2 : ℝ)^k.val) := by
calc
(∑ k : Fin t, (1/2 : ℝ)^(k.val + 1))
= (∑ k : Fin t, (1/2 : ℝ)^k.val * (1/2)) := by
refine Finset.sum_congr rfl (fun k _ => ?_)
rw [pow_succ]
_ = (1/2) * (∑ k : Fin t, (1/2 : ℝ)^k.val) := by
rw [Finset.mul_sum]
simp [mul_comm]
rw [hfactor]
have hgeom : (∑ k : Fin t, (1/2 : ℝ)^k.val) ≤ 2 := by
have hrange : (∑ k : Fin t, (1/2 : ℝ)^k.val) = ∑ i ∈ Finset.range t, (1/2 : ℝ)^i := by
rw [Fin.sum_univ_eq_sum_range (fun i : ℕ => (1/2 : ℝ)^i) t]
rw [hrange]
have hgeom_mul := geom_sum_mul_of_le_one (x := (1/2 : ℝ)) (by norm_num) t
-- (∑ i ∈ range t, (1/2)^i) * (1 - 1/2) = 1 - (1/2)^t, so the product is ≤ 1
have hmul : (∑ i ∈ Finset.range t, (1/2 : ℝ)^i) * (1/2) ≤ 1 := by
norm_num at hgeom_mul
rw [hgeom_mul]
have hpow : 0 ≤ (1/2 : ℝ)^t := by positivity
nlinarith
nlinarith
nlinarith
The number of trials performed until a collision-free secondary hash is
obtained, in a truncated model of t independent trials: the failed trials
before the first collision-free trial, plus the successful trial itself. If
none of the t trials is collision-free, the value is t + 1.
This is a lower truncation of an unbounded waiting time, not an upper estimate.
A constructor using a guaranteed terminal fallback may instead interpret the
last unit as its successful fallback attempt; that requires a separate bridge.
noncomputable def trialsUntilCollisionFree {n t : ℕ} (A : Fin t → (Fin n → Fin (n^2))) : ℕ :=
failedTrials A + 1
Expected number of trials to obtain a collision-free secondary hash. In
the truncated model of t independent trials — each trial hashes n ≥ 2 keys
into n² slots and is collision-free with probability at least 1/2 (Theorem
11.9) — the expected number of trials performed up to and including the first
collision-free trial is at most 2 (CLRS §11.5). This theorem bounds each finite truncation. It does not by itself supply
an infinite trial process or a limit theorem for its expectation.
theorem perfectHash_expected_trials_le_two {n t : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (trialsUntilCollisionFree A : ℝ)) ≤ 2 := by
unfold trialsUntilCollisionFree
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => ((failedTrials A + 1 : ℕ) : ℝ))
= fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ) + 1) := by
refine congrArg fintypeExpect (funext fun A => ?_)
simp
_ = fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ)) + 1 := by
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ) + 1)
= fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ)) +
fintypeExpect (fun _ : Fin t → (Fin n → Fin (n^2)) => (1 : ℝ)) := fintypeExpect_add _ _
_ = fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ)) + 1 := by
haveI : Nonempty (Fin n → Fin (n^2)) := ⟨fun _ => ⟨0, by
have hnpos : 0 < n := by omega
positivity⟩⟩
have hcard : Fintype.card (Fin t → (Fin n → Fin (n^2))) ≠ 0 := Fintype.card_ne_zero
simp [fintypeExpect_const hcard]
_ ≤ 2 := by
linarith [perfectHash_expected_failedTrials_le_one (n := n) (t := t) hn]
Existence of a collision-free secondary hash. For n ≥ 2 keys hashed into
n² slots, some hash assignment is injective. This follows from Theorem 11.9:
the collision-free probability is at least 1/2 > 0, so the event cannot be
empty (CLRS §11.5).
theorem exists_collision_free_secondary {n : ℕ} (hn : 2 ≤ n) :
∃ a : Fin n → Fin (n^2), ∀ i j : Fin n, a i = a j → i = j := by
by_contra h
have hnone : ∀ a : Fin n → Fin (n^2), ¬ (∀ i j : Fin n, a i = a j → i = j) := by
intro a ha
exact h ⟨a, ha⟩
have hzero : fintypeExpect (fun a : Fin n → Fin (n^2) =>
indicator (∀ i j : Fin n, a i = a j → i = j)) = 0 := by
unfold fintypeExpect
rw [show (∑ a : Fin n → Fin (n ^ 2), indicator (∀ i j : Fin n, a i = a j → i = j)) = 0 by
apply Finset.sum_eq_zero
intro a ha
exact if_neg (hnone a)]
simp
have hcf := perfectHash_collision_free_prob_ge_half (n := n) hn
linarithAbstract construction budget and its finite expectation
Historical abstract two-level budget: primary size plus twice the sum of
squared bucket sizes. This is an analysis expression, not measured execution.
The companion Construction module supplies a counted constructor and a
constant-factor conditional-expectation bridge to this expression.
noncomputable def constructionCost {n : ℕ} (a : Fin n → Fin n) : ℝ :=
(n : ℝ) + 2 * totalSecondarySpace aExpected finite trial-count budget times squared bucket size, for at least two keys. The expression charges a supplied square cost per trial; actual collision checking, initialization, and placement are counted in the companion.
theorem perfectHash_expected_bucket_cost_le {n t : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ) * (n : ℝ)^2) ≤ 2 * (n : ℝ)^2 := by
have hsq : 0 ≤ (n : ℝ)^2 := by positivity
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ) * (n : ℝ)^2)
= (n : ℝ)^2 * fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ)) := by
have hswap : (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ) * (n : ℝ)^2)
= (fun A : Fin t → (Fin n → Fin (n^2)) => (n : ℝ)^2 * (trialsUntilCollisionFree A : ℝ)) := by
funext A
ring
rw [hswap]
exact fintypeExpect_const_mul ((n : ℝ)^2) (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ))
_ ≤ (n : ℝ)^2 * 2 := by
exact mul_le_mul_of_nonneg_left (perfectHash_expected_trials_le_two hn) hsq
_ = 2 * (n : ℝ)^2 := by
ring
The historical abstract construction budget has expectation below 5n
under uniform primary assignment. The theorem name is retained for compatibility;
executed construction costs require the companion's refinement theorem.
theorem perfectHash_expected_construction_time_le_const_n {n : ℕ} (hn : 0 < n) :
fintypeExpect (fun a : Fin n → Fin n => constructionCost a) < 5 * (n : ℝ) := by
have hlin : fintypeExpect (fun a : Fin n → Fin n => constructionCost a)
= (n : ℝ) + 2 * fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) := by
unfold constructionCost
calc
fintypeExpect (fun a : Fin n → Fin n => (n : ℝ) + 2 * totalSecondarySpace a)
= fintypeExpect (fun _ : Fin n → Fin n => (n : ℝ)) +
fintypeExpect (fun a : Fin n → Fin n => 2 * totalSecondarySpace a) :=
fintypeExpect_add _ _
_ = (n : ℝ) + 2 * fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) := by
haveI : Nonempty (Fin n) := ⟨⟨0, hn⟩⟩
have hcard : Fintype.card (Fin n → Fin n) ≠ 0 := Fintype.card_ne_zero
rw [fintypeExpect_const hcard, fintypeExpect_const_mul]
rw [hlin]
have hspace := perfectHash_expected_total_space_lt_2n hn
nlinarithend Chapter11end CLRSDefinitions and proofs
CLRSLean.FourthEdition.Chapter_11.Section_11_5_Perfect_Hashing.Construction.Secondary
Executed finite secondary perfect-hash construction
Every candidate runs a collision check, initializes its secondary slot array,
and places its local key indices. The returned ledger counts hash evaluations,
equality comparisons, slot initialization, and indexed writes. Supplied finite
traces stop on success; exhaustion executes a deterministic injective fallback.
Thus the actual attempts equal the existing failedTrials + 1 statistic.
Hashes act on local indices Fin n. Hash-family sampling and representation
costs are outside this indexed-operation model. The finite expectation concerns
uniform full assignments, and is not a theorem about an unbounded retry process.
namespace CLRS.Chapter11.PerfectConstructionHash equality tests, charged for two hash evaluations and one comparison.
def differsFrom (a : α → β) [DecidableEq β] (x : α) : List α → Bool × Nat
| [] => (true, 0)
| y :: ys =>
if a x = a y then (false, 3)
else let rest := differsFrom a x ys; (rest.1, rest.2 + 3)theorem differsFrom_correct (a : α → β) [DecidableEq β] (x : α) (xs : List α) :
(differsFrom a x xs).1 = true ↔ ∀ y ∈ xs, a x ≠ a y := by
induction xs with
| nil => simp [differsFrom]
| cons y ys ih => by_cases h : a x = a y <;> simp [differsFrom, h, ih]theorem differsFrom_work_le (a : α → β) [DecidableEq β] (x : α) (xs : List α) :
(differsFrom a x xs).2 ≤ 3 * xs.length := by
induction xs with
| nil => simp [differsFrom]
| cons y ys ih => simp only [differsFrom]; split <;> simp_all; omegaCheck all distinct input positions, stopping at the first duplicate hash.
def checkDistinct (a : α → β) [DecidableEq β] : List α → Bool × Nat
| [] => (true, 0)
| x :: xs =>
let head := differsFrom a x xs
if head.1 then
let tail := checkDistinct a xs
(tail.1, head.2 + tail.2)
else (false, head.2)theorem checkDistinct_correct (a : α → β) [DecidableEq β] (xs : List α) :
(checkDistinct a xs).1 = true ↔ (xs.map a).Nodup := by
induction xs with
| nil => simp [checkDistinct]
| cons x xs ih =>
simp only [checkDistinct]
split
next h =>
have hh := (differsFrom_correct a x xs).mp h
simp only [List.map_cons, List.nodup_cons, List.mem_map]
constructor
· intro ht
exact ⟨by rintro ⟨y, hy, he⟩; exact hh y hy he.symm, ih.mp ht⟩
· intro ht; exact ih.mpr ht.2
next h =>
simp only [Bool.false_eq_true, List.map_cons, List.nodup_cons, false_iff]
intro hn
apply h
apply (differsFrom_correct a x xs).mpr
intro y hy he
exact hn.1 (List.mem_map.mpr ⟨y, hy, he.symm⟩)theorem checkDistinct_work_le (a : α → β) [DecidableEq β] (xs : List α) :
(checkDistinct a xs).2 ≤ 3 * xs.length ^ 2 := by
induction xs with
| nil => simp [checkDistinct]
| cons x xs ih =>
have hh := differsFrom_work_le a x xs
simp only [checkDistinct]
split <;> simp only [List.length_cons] <;> nlinarithabbrev Hash (n : Nat) := Fin n → Fin (n ^ 2)
theorem check_hash_correct {n : Nat} (a : Hash n) :
(checkDistinct a (List.finRange n)).1 = true ↔ collisionFree a := by
rw [checkDistinct_correct, List.nodup_map_iff_inj_on (List.nodup_finRange n)]
simp [collisionFree]Initialize actual empty secondary slots, counting each array push.
def emptySlots (α : Type*) : Nat → Array (Option α) × Nat
| 0 => (#[], 0)
| m + 1 => let prev := emptySlots α m; (prev.1.push none, prev.2 + 1)@[simp] theorem emptySlots_array (α : Type*) (m : Nat) :
(emptySlots α m).1 = Array.replicate m none := by
induction m with
| zero => simp [emptySlots]
| succ m ih => simp [emptySlots, ih, Array.replicate_succ]@[simp] theorem emptySlots_work (α : Type*) (m : Nat) : (emptySlots α m).2 = m := by
induction m with
| zero => rfl
| succ m ih => simp [emptySlots, ih]Place each key in its selected slot. A step charges a hash evaluation and an indexed write; initialization and collision checks are counted separately.
def place {n : Nat} (a : Hash n) : List (Fin n) → Array (Option (Fin n)) →
Array (Option (Fin n)) × Nat
| [], out => (out, 0)
| i :: is, out =>
let rest := place a is out
(rest.1.setIfInBounds (a i).val (some i), rest.2 + 2)@[simp] theorem place_size {n : Nat} (a : Hash n) (xs : List (Fin n)) (out : Array (Option (Fin n))) :
(place a xs out).1.size = out.size := by
induction xs with
| nil => rfl
| cons i xs ih => simp [place, ih]@[simp] theorem place_work {n : Nat} (a : Hash n) (xs : List (Fin n)) (out : Array (Option (Fin n))) :
(place a xs out).2 = 2 * xs.length := by
induction xs with
| nil => simp [place]
| cons i xs ih => simp [place, ih]; omegaEach slot contains the first input key assigned there, or its initial value.
theorem place_get {n : Nat} (a : Hash n) (xs : List (Fin n))
(out : Array (Option (Fin n))) (s : Nat) (hs : s < out.size) :
(place a xs out).1[s]'(by simpa using hs) =
(xs.find? (fun i => (a i).val == s)).orElse (fun _ => out[s]) := by
induction xs with
| nil => simp [place]
| cons i xs ih =>
simp only [place]
rw [Array.getElem_setIfInBounds (by simpa using hs)]
by_cases he : (a i).val = s <;> simp [he, List.find?, ih, Bool.beq_eq_decide_eq]structure Attempt (n : Nat) where
hash : Hash n
slots : Array (Option (Fin n))
success : Bool
work : NatAn actual collision check followed by secondary-array placement.
def attempt {n : Nat} (a : Hash n) : Attempt n :=
let checked := checkDistinct a (List.finRange n)
let initial := emptySlots (Fin n) (n ^ 2)
let filled := place a (List.finRange n) initial.1
⟨a, filled.1, checked.1, checked.2 + initial.2 + filled.2⟩@[simp] theorem attempt_hash {n : Nat} (a : Hash n) : (attempt a).hash = a := rfl@[simp] theorem attempt_size {n : Nat} (a : Hash n) : (attempt a).slots.size = n ^ 2 := by
simp [attempt]@[simp] theorem attempt_success {n : Nat} (a : Hash n) :
(attempt a).success = true ↔ collisionFree a := check_hash_correct a
theorem attempt_work_le {n : Nat} (a : Hash n) : (attempt a).work ≤ 6 * n ^ 2 := by
have h := checkDistinct_work_le a (List.finRange n)
have hn : n ≤ n ^ 2 := by nlinarith
simpa only [attempt, emptySlots_work, place_work, List.length_finRange] using
(show (checkDistinct a (List.finRange n)).2 + n ^ 2 + 2 * n ≤ 6 * n ^ 2 by
simp only [List.length_finRange] at h
nlinarith)
theorem attempt_get {n : Nat} (a : Hash n) (s : Nat) (hs : s < n ^ 2) :
(attempt a).slots[s]'(by simpa using hs) =
(List.finRange n).find? (fun i => (a i).val == s) := by
have h := place_get a (List.finRange n) (emptySlots (Fin n) (n ^ 2)).1 s (by simpa using hs)
simpa [attempt] using h
theorem attempt_stores {n : Nat} (a : Hash n) (ha : collisionFree a) (i : Fin n) :
(attempt a).slots[(a i).val]'(by simp) = some i := by
rw [attempt_get a _ (a i).isLt]
cases h : (List.finRange n).find? (fun j => (a j).val == (a i).val) with
| none =>
have hh := List.find?_eq_none.mp h i (by simp)
simp at hh
| some j =>
have hh := List.find?_some h
have he : a j = a i := Fin.ext (by simpa using hh)
rw [ha j i he]The terminal assignment is explicitly injective, including the empty domain.
def fallback (n : Nat) : Hash n := fun i =>
⟨i.val, lt_of_lt_of_le i.isLt (by nlinarith : n ≤ n ^ 2)⟩theorem fallback_injective (n : Nat) : collisionFree (fallback n) := by
intro i j hij
exact Fin.ext (congrArg (fun x : Fin (n ^ 2) => x.val) hij)structure Build (n : Nat) where
selected : Attempt n
attempts : Nat
work : NatTry supplied candidates in order; after their exhaustion execute one explicit injective fallback. Every returned table therefore succeeds.
def build {n : Nat} : List (Hash n) → Build n
| [] => let final := attempt (fallback n); ⟨final, 1, final.work⟩
| a :: as =>
let trial := attempt a
if trial.success then ⟨trial, 1, trial.work⟩
else let rest := build as; ⟨rest.selected, rest.attempts + 1, trial.work + rest.work⟩theorem build_success {n : Nat} (as : List (Hash n)) : (build as).selected.success = true := by
induction as with
| nil => simpa [build] using (attempt_success (fallback n)).mpr (fallback_injective n)
| cons a as ih =>
simp only [build]
split
· assumption
· exact ihtheorem build_work_le {n : Nat} (as : List (Hash n)) :
(build as).work ≤ 6 * n ^ 2 * (build as).attempts := by
induction as with
| nil => simpa [build] using attempt_work_le (fallback n)
| cons a as ih =>
have h := attempt_work_le a
simp only [build]
split <;> simp only <;> nlinarithdef failedPrefix {n : Nat} : List (Hash n) → Nat
| [] => 0
| a :: as => if (attempt a).success then 0 else failedPrefix as + 1theorem build_attempts {n : Nat} (as : List (Hash n)) :
(build as).attempts = failedPrefix as + 1 := by
induction as with
| nil => simp [build, failedPrefix]
| cons a as ih => simp only [build, failedPrefix]; split <;> simp_all
lemma failedTrials_cons {n t : Nat} (A : Fin (t + 1) → Hash n) :
failedTrials A = if collisionFree (A 0) then 0 else
failedTrials (fun j : Fin t => A j.succ) + 1 := by
classical
rw [failedTrials_eq_sum, Fin.sum_univ_succ]
have htail (k : Fin t) :
(∀ j : Fin (k.succ.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.succ.isLt) j))) ↔
(¬ collisionFree (A 0) ∧ ∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j).succ)) := by
rw [Fin.forall_fin_succ]
rfl
have hzero :
(∀ j : Fin ((0 : Fin (t + 1)).val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt (0 : Fin (t + 1)).isLt) j))) ↔
¬ collisionFree (A 0) := by
constructor
· intro hh
exact hh 0
· intro hh j
have hj : Fin.castLE (Nat.succ_le_of_lt (0 : Fin (t + 1)).isLt) j = 0 := by
apply Fin.ext
have h := j.isLt
change j.val < 1 at h
change j.val = 0
omega
rw [hj]
exact hh
simp_rw [htail, hzero]
by_cases h : collisionFree (A 0)
· have hh : collisionFree (A 0) ↔ True := iff_true_intro h
simp only [hh, not_true_eq_false, false_and, ite_false, Finset.sum_const_zero,
zero_add, ite_true]
· simp only [h, not_false_eq_true, true_and, ite_true, ite_false]
rw [failedTrials_eq_sum]
omega
theorem failedPrefix_ofFn {n t : Nat} (A : Fin t → Hash n) :
failedPrefix (List.ofFn A) = failedTrials A := by
classical
induction t with
| zero => simp [List.ofFn_zero, failedPrefix, failedTrials]
| succ t ih =>
rw [List.ofFn_succ, failedPrefix, failedTrials_cons]
simp only [attempt_success, ih]def buildTrace {n : Nat} : {t : Nat} → (Fin t → Hash n) → Build n
| 0, _ => build []
| _ + 1, A =>
let current := attempt (A 0)
if current.success then ⟨current, 1, current.work⟩
else
let rest := buildTrace (fun j => A j.succ)
⟨rest.selected, rest.attempts + 1, current.work + rest.work⟩theorem buildTrace_eq {n t : Nat} (A : Fin t → Hash n) :
buildTrace A = build (List.ofFn A) := by
induction t with
| zero => simp [buildTrace]
| succ t ih => simp [buildTrace, List.ofFn_succ, build, ih]The legacy finite trial random variable is the exact attempt count of finite sampling followed by the terminal injective fallback.
theorem buildTrace_attempts {n t : Nat} (A : Fin t → Hash n) :
(buildTrace A).attempts = trialsUntilCollisionFree A := by
rw [buildTrace_eq, build_attempts, failedPrefix_ofFn, trialsUntilCollisionFree]theorem buildTrace_work_le {n t : Nat} (A : Fin t → Hash n) :
(buildTrace A).work ≤ 6 * n ^ 2 * trialsUntilCollisionFree A := by
simpa [← buildTrace_attempts A, buildTrace_eq] using build_work_le (List.ofFn A)open CLRS.ProbabilityExpected measured secondary work over the stated finite SUHA trace space.
theorem expected_buildTrace_work_le {n t : Nat} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin t → Hash n => ((buildTrace A).work : ℝ)) ≤
12 * (n : ℝ) ^ 2 := by
classical
calc
fintypeExpect (fun A : Fin t → Hash n => ((buildTrace A).work : ℝ)) ≤
fintypeExpect (fun A : Fin t → Hash n =>
6 * (n : ℝ) ^ 2 * (trialsUntilCollisionFree A : ℝ)) := by
apply fintypeExpect_mono
intro A
exact_mod_cast buildTrace_work_le A
_ = (6 * (n : ℝ) ^ 2) *
fintypeExpect (fun A : Fin t → Hash n => (trialsUntilCollisionFree A : ℝ)) :=
fintypeExpect_const_mul _ _
_ ≤ (6 * (n : ℝ) ^ 2) * 2 :=
mul_le_mul_of_nonneg_left (perfectHash_expected_trials_le_two hn) (by positivity)
_ = _ := by ringEvery selected result is a real executed secondary attempt.
theorem build_selected_eq {n : Nat} (as : List (Hash n)) :
(build as).selected = attempt (build as).selected.hash := by
induction as with
| nil => rfl
| cons a as ih => simp only [build]; split <;> first | rfl | exact ih
theorem build_hash_injective {n : Nat} (as : List (Hash n)) :
collisionFree (build as).selected.hash := by
have h := build_success as
rw [build_selected_eq] at h
exact attempt_success _ |>.mp h
theorem attempt_only {n : Nat} (a : Hash n) (s : Nat) (i : Fin n)
(h : (attempt a).slots[s]?.join = some i) : (a i).val = s := by
by_cases hs : s < n ^ 2
· have hsize : s < (attempt a).slots.size := by simpa using hs
rw [Array.getElem?_eq_getElem hsize, Option.join_some, attempt_get a s hs] at h
have hh := List.find?_some h
simpa using hh
· have hsize : ¬ s < (attempt a).slots.size := by simpa using hs
simp [Array.getElem?_eq_none (Nat.le_of_not_gt hsize)] at hPackage the constructed array as a one-bucket perfect table. All key slots come from the returned placement execution, rather than from a specification search.
def tableOfBuild {n : Nat} (as : List (Hash n)) : PerfectHashTable (Fin n) 1 where
keys := Finset.univ
prim := fun _ => 0
sec := fun _ i => ((build as).selected.hash i).val
table := fun _ s => (build as).selected.slots[s]?.join
sec_inj := by
intro j x y hx hy hpx hpy hs
exact build_hash_injective as x y (Fin.ext hs)
table_stores_keys := by
intro i hi
rw [build_selected_eq]
have hs := attempt_stores (build as).selected.hash (build_hash_injective as) i
simp only [attempt_hash]
rw [Array.getElem?_eq_getElem (by simp), Option.join_some]
exact hs
table_only_keys := by
intro j s i hi
refine ⟨by simp, Subsingleton.elim _ _, ?_⟩
rw [build_selected_eq] at hi
exact attempt_only _ s i hiThe successful finite builder has a verified membership-query interface.
theorem tableOfBuild_search {n : Nat} (as : List (Hash n)) (i : Fin n) :
perfectSearch (tableOfBuild as) i := by
rw [perfectSearch_iff_mem]
simp [tableOfBuild]
theorem build_work_le_small {n : Nat} (hn : n ≤ 1) (as : List (Hash n)) :
(build as).work ≤ 6 * n ^ 2 := by
cases as with
| nil => simpa [build] using attempt_work_le (fallback n)
| cons a as =>
have hc : collisionFree a := by
intro i j hij
apply Fin.ext
have hi := i.isLt
have hj := j.isLt
omega
have hs := (attempt_success a).mpr hc
simpa only [build, hs, Bool.true_eq, ↓reduceIte] using attempt_work_le aEmpty and singleton buckets are handled directly; the SUHA retry bound is needed only for buckets with at least two keys.
theorem expected_buildTrace_work_le_all (n t : Nat) :
fintypeExpect (fun A : Fin t → Hash n => ((buildTrace A).work : ℝ)) ≤
12 * (n : ℝ) ^ 2 := by
classical
by_cases hn : 2 ≤ n
· exact expected_buildTrace_work_le hn
haveI : Nonempty (Fin t → Hash n) := ⟨fun _ => fallback n⟩
calc
fintypeExpect (fun A : Fin t → Hash n => ((buildTrace A).work : ℝ)) ≤
fintypeExpect (fun _ : Fin t → Hash n => 6 * (n : ℝ) ^ 2) := by
apply fintypeExpect_mono
intro A
rw [buildTrace_eq]
exact_mod_cast build_work_le_small (by omega : n ≤ 1) (List.ofFn A)
_ = 6 * (n : ℝ) ^ 2 := fintypeExpect_const Fintype.card_ne_zero _
_ ≤ _ := by nlinarith [sq_nonneg (n : ℝ)]end CLRS.Chapter11.PerfectConstructionCLRSLean.FourthEdition.Chapter_11.Section_11_5_Perfect_Hashing.Construction.TwoLevel
Measured assembly of all secondary tables
The constructor actually distributes original Fin n payloads into primary
buckets, caches bucket sizes, and executes and retains every secondary builder.
The measured indexed-operation work has conditional expectation at most nine
times constructionCost and unconditional expectation below 45 * n.
Secondary tables hash and store local bucket indices. Original payload buckets remain in the result; converting an original query key to its local index is a separate representation boundary. This does not claim constant-time hashing on an arbitrary original key universe or machine runtime for persistent arrays.
namespace CLRS.Chapter11.PerfectConstructionopen CLRS.ProbabilityUniform dependent-product sampling has the expected one-coordinate marginal.
theorem expect_pi_apply {ι : Type} [Fintype ι] [DecidableEq ι]
(Ω : ι → Type) [∀ i, Fintype (Ω i)] [∀ i, DecidableEq (Ω i)]
[∀ i, Nonempty (Ω i)] (j : ι) (X : Ω j → ℝ) :
fintypeExpect (fun w : (i : ι) → Ω i => X (w j)) = fintypeExpect X := by
classical
calc
fintypeExpect (fun w : (i : ι) → Ω i => X (w j)) =
fintypeExpect (fun p : Ω j × ((i : {i : ι // i ≠ j}) → Ω i.val) => X p.1) :=
fintypeExpect_equiv (Equiv.piSplitAt j Ω) (fun p => X p.1)
_ = _ := fintypeExpect_fst Fintype.card_ne_zero Xstructure Assembly {m : Nat} (sizes : Fin m → Nat) where
buckets : Array (Σ j : Fin m, Build (sizes j))
work : NatExecute every bucket constructor, storing its returned table in the output array. Each push of a completed bucket record contributes one more operation.
def assemble {m t : Nat} (sizes : Fin m → Nat)
(A : (j : Fin m) → Fin t → Hash (sizes j)) :
List (Fin m) → Array (Σ j : Fin m, Build (sizes j)) → Assembly sizes
| [], out => ⟨out, 0⟩
| j :: js, out =>
let built := buildTrace (A j)
let rest := assemble sizes A js (out.push ⟨j, built⟩)
⟨rest.buckets, built.work + 1 + rest.work⟩theorem assemble_result {m t : Nat} (sizes : Fin m → Nat)
(A : (j : Fin m) → Fin t → Hash (sizes j))
(js : List (Fin m)) (out : Array (Σ j : Fin m, Build (sizes j))) :
(assemble sizes A js out).buckets.toList =
out.toList ++ js.map (fun j => ⟨j, buildTrace (A j)⟩) := by
induction js generalizing out with
| nil => simp [assemble]
| cons j js ih => simp [assemble, ih, List.append_assoc]theorem assemble_work {m t : Nat} (sizes : Fin m → Nat)
(A : (j : Fin m) → Fin t → Hash (sizes j))
(js : List (Fin m)) (out : Array (Σ j : Fin m, Build (sizes j))) :
(assemble sizes A js out).work = js.length + (js.map (fun j => (buildTrace (A j)).work)).sum := by
induction js generalizing out with
| nil => simp [assemble]
| cons j js ih => simp [assemble, ih]; omega
theorem assemble_success {m t : Nat} (sizes : Fin m → Nat)
(A : (j : Fin m) → Fin t → Hash (sizes j)) :
∀ entry ∈ (assemble sizes A (List.finRange m) #[]).buckets.toList,
entry.2.selected.success = true := by
rw [assemble_result]
simp only [List.nil_append, List.mem_map]
rintro entry ⟨j, hj, rfl⟩
simpa only [buildTrace_eq] using build_success _Expected work of the actual assembly over independent finite per-bucket trace spaces; no assumption on empty/singleton bucket sizes is needed.
theorem expected_assemble_work_le {m : Nat} (sizes : Fin m → Nat) (t : Nat) :
fintypeExpect (fun A : (j : Fin m) → Fin t → Hash (sizes j) =>
((assemble sizes A (List.finRange m) #[]).work : ℝ)) ≤
(m : ℝ) + 12 * ∑ j : Fin m, (sizes j : ℝ) ^ 2 := by
classical
haveI (j : Fin m) : Nonempty (Fin t → Hash (sizes j)) := ⟨fun _ => fallback (sizes j)⟩
have hex : (fun A : (j : Fin m) → Fin t → Hash (sizes j) =>
((assemble sizes A (List.finRange m) #[]).work : ℝ)) =
(fun A => (m : ℝ) + ∑ j : Fin m, ((buildTrace (A j)).work : ℝ)) := by
funext A
rw [assemble_work]
simp [← List.ofFn_id, List.map_ofFn, List.sum_ofFn]
rw [hex, fintypeExpect_add, fintypeExpect_const Fintype.card_ne_zero,
fintypeExpect_sum]
have hmarginal (j : Fin m) :
fintypeExpect (fun A : (j : Fin m) → Fin t → Hash (sizes j) =>
((buildTrace (A j)).work : ℝ)) ≤ 12 * (sizes j : ℝ) ^ 2 := by
rw [expect_pi_apply (fun j : Fin m => Fin t → Hash (sizes j)) j
(fun B : Fin t → Hash (sizes j) => ((buildTrace B).work : ℝ))]
exact expected_buildTrace_work_le_all (sizes j) t
calc
(m : ℝ) + ∑ j : Fin m,
fintypeExpect (fun A : (j : Fin m) → Fin t → Hash (sizes j) =>
((buildTrace (A j)).work : ℝ)) ≤
(m : ℝ) + ∑ j : Fin m, 12 * (sizes j : ℝ) ^ 2 := by
gcongr with j
exact hmarginal j
_ = _ := by rw [Finset.mul_sum]Count a bucket's length by traversing its elements once.
def measureLength : List α → Nat × Nat
| [] => (0, 0)
| _ :: xs => let rest := measureLength xs; (rest.1 + 1, rest.2 + 1)@[simp] theorem measureLength_result (xs : List α) : (measureLength xs).1 = xs.length := by
induction xs <;> simp_all [measureLength]@[simp] theorem measureLength_work (xs : List α) : (measureLength xs).2 = xs.length := by
induction xs <;> simp_all [measureLength]Cache bucket cardinalities once, counting both visits and cache writes.
def measureBuckets : List (List α) → Array Nat → Array Nat × Nat
| [], out => (out, 0)
| b :: bs, out =>
let measured := measureLength b
let rest := measureBuckets bs (out.push measured.1)
(rest.1, measured.2 + 1 + rest.2)theorem measureBuckets_result (bs : List (List α)) (out : Array Nat) :
(measureBuckets bs out).1.toList = out.toList ++ bs.map List.length := by
induction bs generalizing out with
| nil => simp [measureBuckets]
| cons b bs ih => simp [measureBuckets, ih, List.append_assoc]theorem measureBuckets_work (bs : List (List α)) (out : Array Nat) :
(measureBuckets bs out).2 = bs.length + bs.flatten.length := by
induction bs generalizing out with
| nil => simp [measureBuckets]
| cons b bs ih => simp [measureBuckets, ih]; omegastructure Primary (n : Nat) where
buckets : Array (List (Fin n))
sizes : Array Nat
work : NatOne primary distribution and one bucket-cardinality traversal. Subsequent secondary attempts read cached sizes instead of recomputing the partition.
def preparePrimary {n : Nat} (a : Fin n → Fin n) : Primary n :=
let initial := Chapter08.CountingExecution.initializeBuckets (α := Fin n) n
let distributed := Chapter08.CountingExecution.distribute
(fun i => (a i).val) (List.finRange n) initial.1
let measured := measureBuckets distributed.buckets.toList #[]
⟨distributed.buckets, measured.1,
initial.2 + 2 * distributed.inputs + 3 * distributed.updates + measured.2⟩@[simp] theorem preparePrimary_bucket_size {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).buckets.size = n := by
simp [preparePrimary]theorem preparePrimary_buckets {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).buckets.toList =
(List.range n).map (Chapter08.bucket (fun i => (a i).val) (List.finRange n)) := by
simp [preparePrimary, Chapter08.CountingExecution.distribute_toList]theorem preparePrimary_sizes {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).sizes.toList =
(preparePrimary a).buckets.toList.map List.length := by
simp [preparePrimary, measureBuckets_result]
@[simp] theorem preparePrimary_sizes_size {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).sizes.size = n := by
rw [← Array.length_toList, preparePrimary_sizes]
simp
theorem preparePrimary_total_length {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).buckets.toList.flatten.length = n := by
rw [preparePrimary_buckets]
cases n with
| zero => simp
| succ n =>
change (Chapter08.countingSortBy n (fun i => (a i).val) (List.finRange (n + 1))).length = n + 1
have hp := Chapter08.countingSortBy_perm n (fun i => (a i).val) (List.finRange (n + 1))
(by intro i hi; change (a i).val ≤ n; have h := (a i).isLt; omega)
simpa using hp.length_eq
theorem preparePrimary_work {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).work = 8 * n := by
have hu : (Chapter08.CountingExecution.distribute
(fun i : Fin n => (a i).val) (List.finRange n) (Array.replicate n [])).updates = n := by
simpa using Chapter08.CountingExecution.distribute_updates_eq
(fun i : Fin n => (a i).val) (List.finRange n) (Array.replicate n [])
(by intro i hi; simp)
have hl := preparePrimary_total_length a
unfold preparePrimary at hl ⊢
simp only [Chapter08.CountingExecution.initializeBuckets_visits,
Chapter08.CountingExecution.distribute_inputs, hu, List.length_finRange,
measureBuckets_work, Array.length_toList, Chapter08.CountingExecution.distribute_size,
Chapter08.CountingExecution.initializeBuckets_array, Array.size_replicate] at hl ⊢
rw [hl]
omegaA stored cached cardinality, not a repeated list-length computation.
def bucketCard {n : Nat} (a : Fin n → Fin n) (j : Fin n) : Nat :=
(preparePrimary a).sizes[j.val]'(by simp)theorem bucketCard_eq {n : Nat} (a : Fin n → Fin n) (j : Fin n) :
bucketCard a j = (Chapter08.bucket (fun i => (a i).val) (List.finRange n) j.val).length := by
unfold bucketCard
simp only [← Array.getElem_toList, preparePrimary_sizes, preparePrimary_buckets,
List.getElem_map, List.getElem_range]
theorem bucketCard_cast {n : Nat} (a : Fin n → Fin n) (j : Fin n) :
(bucketCard a j : ℝ) = bucketSize a j := by
rw [bucketCard_eq, Chapter08.bucket_length_eq_card]
exact (Chapter08.bucketOccupancy_eq_card a j).symm
theorem bucketCard_sq_sum {n : Nat} (a : Fin n → Fin n) :
(∑ j : Fin n, (bucketCard a j : ℝ) ^ 2) = totalSecondarySpace a := by
unfold totalSecondarySpace
exact Finset.sum_congr rfl (fun j _ => by rw [bucketCard_cast])structure TwoLevel {n : Nat} (a : Fin n → Fin n) where
primary : Primary n
secondary : Assembly (bucketCard a)
work : NatConstruct every actual secondary array from finite supplied trial traces, retaining the primary payload buckets and all completed secondary arrays.
def buildTwoLevel {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j)) : TwoLevel a :=
let primary := preparePrimary a
let secondary := assemble (fun j => primary.sizes[j.val]'(by simp [primary])) A (List.finRange n) #[]
⟨primary, secondary, primary.work + secondary.work⟩All secondary tables returned by the complete constructor succeeded.
theorem buildTwoLevel_success {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j)) :
∀ entry ∈ (buildTwoLevel a A).secondary.buckets.toList,
entry.2.selected.success = true :=
assemble_success (bucketCard a) AConditional expected measured work is bounded by the pre-existing analytic budget up to an explicit operation-count constant.
theorem expected_buildTwoLevel_work_le_budget {n : Nat} (a : Fin n → Fin n) (t : Nat) :
fintypeExpect (fun A : (j : Fin n) → Fin t → Hash (bucketCard a j) =>
((buildTwoLevel a A).work : ℝ)) ≤ 9 * constructionCost a := by
classical
haveI (j : Fin n) : Nonempty (Fin t → Hash (bucketCard a j)) :=
⟨fun _ => fallback (bucketCard a j)⟩
have he := expected_assemble_work_le (bucketCard a) t
rw [bucketCard_sq_sum] at he
have hex : (fun A : (j : Fin n) → Fin t → Hash (bucketCard a j) =>
((buildTwoLevel a A).work : ℝ)) =
(fun A => (8 * n : ℝ) + ((assemble (bucketCard a) A (List.finRange n) #[]).work : ℝ)) := by
funext A
change (((preparePrimary a).work + (assemble (bucketCard a) A (List.finRange n) #[]).work : Nat) : ℝ) = _
rw [preparePrimary_work]
push_cast
rfl
rw [hex, fintypeExpect_add, fintypeExpect_const Fintype.card_ne_zero]
have hs : 0 ≤ totalSecondarySpace a := by
unfold totalSecondarySpace
exact Finset.sum_nonneg (fun j _ => sq_nonneg _)
unfold constructionCost
nlinarithAverage actual finite-with-fallback construction work is linear under the primary SUHA assignment and the conditional independent secondary trace spaces.
theorem expected_buildTwoLevel_work_lt {n : Nat} (hn : 0 < n) (t : Nat) :
fintypeExpect (fun a : Fin n → Fin n =>
fintypeExpect (fun A : (j : Fin n) → Fin t → Hash (bucketCard a j) =>
((buildTwoLevel a A).work : ℝ))) < 45 * (n : ℝ) := by
classical
calc
fintypeExpect (fun a : Fin n → Fin n =>
fintypeExpect (fun A : (j : Fin n) → Fin t → Hash (bucketCard a j) =>
((buildTwoLevel a A).work : ℝ))) ≤
fintypeExpect (fun a : Fin n → Fin n => 9 * constructionCost a) := by
apply fintypeExpect_mono
intro a
exact expected_buildTwoLevel_work_le_budget a t
_ = 9 * fintypeExpect (fun a : Fin n → Fin n => constructionCost a) :=
fintypeExpect_const_mul 9 _
_ < 9 * (5 * (n : ℝ)) :=
mul_lt_mul_of_pos_left (perfectHash_expected_construction_time_le_const_n hn) (by norm_num)
_ = _ := by ringEach returned secondary array stores every local key index at the slot selected by the hash returned by that same execution.
theorem buildTwoLevel_stores {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j)) :
∀ entry ∈ (buildTwoLevel a A).secondary.buckets.toList,
∀ i : Fin (bucketCard a entry.1),
entry.2.selected.slots[(entry.2.selected.hash i).val]?.join = some i := by
change ∀ entry ∈ (assemble (bucketCard a) A (List.finRange n) #[]).buckets.toList, _
rw [assemble_result]
simp only [List.nil_append, List.mem_map]
rintro entry ⟨j, hj, rfl⟩ i
simp only [buildTrace_eq]
change (build (List.ofFn (A j))).selected.slots[
((build (List.ofFn (A j))).selected.hash i).val]?.join = some i
rw [build_selected_eq]
simp only [attempt_hash]
rw [Array.getElem?_eq_getElem (by simp), Option.join_some]
exact attempt_stores _ (build_hash_injective (List.ofFn (A j))) i
theorem preparePrimary_bucket_length {n : Nat} (a : Fin n → Fin n) (j : Fin n) :
((preparePrimary a).buckets[j.val]'(by simp)).length = bucketCard a j := by
rw [bucketCard_eq]
simp only [← Array.getElem_toList, preparePrimary_buckets,
List.getElem_map, List.getElem_range]Resolve a supplied local index through its selected secondary slot to the original payload. Supplying the local index is an explicit interface requirement.
def recoverPayload {n : Nat} {a : Fin n → Fin n} (built : TwoLevel a)
(entry : Σ j : Fin n, Build (bucketCard a j)) (i : Fin (bucketCard a entry.1)) :
Option (Fin n) := do
let localIndex ← entry.2.selected.slots[(entry.2.selected.hash i).val]?.join
let payloads ← built.primary.buckets[entry.1.val]?
payloads[localIndex.val]?
theorem buildTwoLevel_recovers {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j))
(entry : Σ j : Fin n, Build (bucketCard a j))
(he : entry ∈ (buildTwoLevel a A).secondary.buckets.toList)
(i : Fin (bucketCard a entry.1)) :
recoverPayload (buildTwoLevel a A) entry i =
some (((preparePrimary a).buckets[entry.1.val]'(by simp))[i.val]'(by
rw [preparePrimary_bucket_length]; exact i.isLt)) := by
unfold recoverPayload
rw [buildTwoLevel_stores a A entry he i]
change ((preparePrimary a).buckets[entry.1.val]?.bind fun xs => xs[i.val]?) = _
rw [Array.getElem?_eq_getElem (by simp), Option.bind_some,
List.getElem?_eq_getElem (by rw [preparePrimary_bucket_length]; exact i.isLt)]
theorem preparePrimary_covers {n : Nat} (a : Fin n → Fin n) (x : Fin n) :
∃ i : Fin (bucketCard a (a x)),
((preparePrimary a).buckets[(a x).val]'(by simp))[i.val]'(by
rw [preparePrimary_bucket_length]; exact i.isLt) = x := by
have hm : x ∈ (preparePrimary a).buckets[(a x).val]'(by simp) := by
simp only [← Array.getElem_toList, preparePrimary_buckets,
List.getElem_map, List.getElem_range]
simp [CLRS.Chapter08.bucket]
obtain ⟨i, hi, hx⟩ := List.getElem_of_mem hm
exact ⟨⟨i, by rwa [preparePrimary_bucket_length] at hi⟩, hx⟩Every original stored payload can be recovered through its actual returned secondary table, given its local index. This does not construct an inverse map from arbitrary original query keys to local indices.
theorem buildTwoLevel_recovers_original {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j)) (x : Fin n) :
∃ i : Fin (bucketCard a (a x)),
recoverPayload (buildTwoLevel a A) ⟨a x, buildTrace (A (a x))⟩ i = some x := by
obtain ⟨i, hi⟩ := preparePrimary_covers a x
refine ⟨i, ?_⟩
rw [buildTwoLevel_recovers, hi]
change (⟨a x, buildTrace (A (a x))⟩ : Σ j : Fin n, Build (bucketCard a j)) ∈
(assemble (bucketCard a) A (List.finRange n) #[]).buckets.toList
rw [assemble_result]
simp only [List.nil_append, List.mem_map]
exact ⟨a x, List.mem_finRange _, rfl⟩end CLRS.Chapter11.PerfectConstruction