Chapter 27 — Online Algorithms
CLRS, fourth edition · Lean 4 formalization
The proofs below use the models and assumptions described in the scope and implementation notes.
Imports
import Mathlib.Data.Real.Basic
import Mathlib.Tactic27.1. Waiting for an Elevator
This section formalizes the rent-or-buy (ski rental) problem that opens
CLRS §27.1 on online algorithms. You can rent the equipment for r per day or
buy it for a one-time cost p, without knowing how many days you will need it.
An online algorithm must choose rent-or-buy as the days arrive; the offline
optimum knows the total number of days in advance. The elevator scenario of
the section — wait for the elevator (renting, one time unit at a time) and if it
does not come by time S - E, take the stairs (buying at cost S) — is the
same problem with r = 1 and p scaled by the elevator ride time E.
Main results:
-
Definition
SkiRental.rentThenBuyCost: the cost of the deterministic strategy that rents for the firstadays and buys on daya+1if the trip outlasts it. -
Definition
SkiRental.optCost: the optimal offline cost,min (T*r) p. -
Definition
SkiRental.IsCompetitive: a strategy isc-competitive when its cost is at mostctimes the optimal offline cost on every input. -
Theorem
SkiRental.rentThenBuy_two_competitive(Theorem 27.1, upper bound): any strategy that rentsadays witha*r < p ≤ (a+1)*r— rent strictly less than the break-even point, buy no later than the day renting overtakes buying — is2-competitive. -
Definition
SkiRental.StrategyandSkiRental.onlineCost: the general causal model of a deterministic online strategy (its first buy day, or never). -
Theorem
SkiRental.rentThenBuy_lower_bound/SkiRental.skiRental_lower_bound(Theorem 27.1, lower bound): no deterministic strategy beats2 - r/p. -
Definition
Elevator.costandElevator.optCost: the elevator instance. -
Theorem
Elevator.elevator_two_competitive: the "waitS - Ethen take the stairs" strategy is2-competitive. -
Theorem
Elevator.elevator_lower_bound: no deterministic wait beats2 - E/S. -
Lemma
Elevator.elevator_worst_case_ratio: when the elevator comes late the cost is exactly(2 - E/S) * S, the competitive ratio stated in CLRS §27.1.
The model is deliberately thin: costs are real numbers, the input is the total
number of days T : ℕ, and a deterministic online strategy is its first buy day
(a "rent a days then buy" threshold, or none for "never buy"), which is the
general causal model — an online strategy decides rent-or-buy day by day without
seeing the future, and buying is irreversible. The deterministic lower bound
2 - r/p (and its elevator form 2 - E/S) is formalized as an adversarial
construction: for every strategy there is a finite input on which it pays at
least 2 - r/p times the offline optimum.
Notation conventions used in this section:
-
r: the daily rental cost -
p: the one-time purchase cost -
a: the number of days a strategy rents before buying -
T: the total number of days (the input, unknown to the online algorithm) -
E: the elevator ride time -
S: the time the stairs take -
w: the number of seconds a strategy waits before taking the stairs
noncomputable sectionnamespace CLRSnamespace SkiRental
The cost of the deterministic online strategy that rents for the first a
days and, if the trip lasts longer than a days, buys on day a+1, on an
input of T days: it pays T * r (renting every day) when T ≤ a, and
a * r + p (renting a days then buying) otherwise. r is the per-day rental
cost and p the one-time purchase cost (CLRS §27.1).
def rentThenBuyCost (r p : ℝ) (a T : ℕ) : ℝ :=
if T ≤ a then (T : ℝ) * r else (a : ℝ) * r + p
The optimal offline cost for a trip of T days: rent every day (cost
T * r) or buy once (cost p), whichever is cheaper. The offline optimum
knows T in advance.
def optCost (r p : ℝ) (T : ℕ) : ℝ :=
min ((T : ℝ) * r) p
A deterministic strategy a is c-competitive when, on every input T,
its cost is at most c times the optimal offline cost.
def IsCompetitive (r p c : ℝ) (a : ℕ) : Prop :=
∀ T : ℕ, rentThenBuyCost r p a T ≤ c * optCost r p TOn a trip short enough that renting every day is no more than buying, the offline optimum is to rent every day.
lemma optCost_eq_rent (r p : ℝ) (T : ℕ) (h : (T : ℝ) * r ≤ p) :
optCost r p T = (T : ℝ) * r := by
rw [optCost]
exact min_eq_left hOn a trip long enough that buying costs no more than renting every day, the offline optimum is to buy.
lemma optCost_eq_buy (r p : ℝ) (T : ℕ) (h : p ≤ (T : ℝ) * r) :
optCost r p T = p := by
rw [optCost]
exact min_eq_right h
A short trip's cost under the rent-a-then-buy strategy is pure renting.
lemma rentThenBuyCost_short (r p : ℝ) (a T : ℕ) (h : T ≤ a) :
rentThenBuyCost r p a T = (T : ℝ) * r := by
rw [rentThenBuyCost]
exact if_pos h
A long trip's cost under the rent-a-then-buy strategy is renting a days
and then buying.
lemma rentThenBuyCost_long (r p : ℝ) (a T : ℕ) (h : ¬ T ≤ a) :
rentThenBuyCost r p a T = (a : ℝ) * r + p := by
rw [rentThenBuyCost]
exact if_neg h
Theorem 27.1 (rent-or-buy is 2-competitive), upper bound. Let r > 0 be
the daily rental cost and p > 0 the purchase cost. A strategy that rents for
a days with a * r < p ≤ (a + 1) * r — renting strictly less than the
break-even point, and buying no later than the day renting overtakes buying —
is 2-competitive: whatever the number of days T, it pays at most twice the
optimal offline cost. (CLRS §27.1 shows the strategy that waits S - E seconds
before taking the stairs is exactly this with competitive ratio 2 - E/S.)
set_option linter.unusedVariables false in
theorem rentThenBuy_two_competitive (r p : ℝ) {a : ℕ} (hr : 0 < r) (hp : 0 < p)
(hlt : (a : ℝ) * r < p) (hge : p ≤ ((a + 1 : ℕ) : ℝ) * r) :
IsCompetitive r p 2 a := by
intro T
by_cases hT : T ≤ a
· -- Short trip: the strategy rents every day, exactly the optimum.
have hcost : rentThenBuyCost r p a T = (T : ℝ) * r :=
rentThenBuyCost_short r p a T hT
have hTle : (T : ℝ) ≤ (a : ℝ) := by exact_mod_cast hT
have hTrp : (T : ℝ) * r ≤ p := by
calc
(T : ℝ) * r ≤ (a : ℝ) * r := mul_le_mul_of_nonneg_right hTle (le_of_lt hr)
_ ≤ p := le_of_lt hlt
have hopt : optCost r p T = (T : ℝ) * r := optCost_eq_rent r p T hTrp
rw [hcost, hopt]
have hnn : 0 ≤ (T : ℝ) * r := mul_nonneg (Nat.cast_nonneg T) (le_of_lt hr)
nlinarith
· -- Long trip: the strategy rents `a` days then buys, and the optimum buys.
have haT : a < T := Nat.lt_of_not_ge hT
have hcost : rentThenBuyCost r p a T = (a : ℝ) * r + p :=
rentThenBuyCost_long r p a T hT
have hTge : ((a + 1 : ℕ) : ℝ) ≤ (T : ℝ) := by exact_mod_cast (Nat.succ_le_of_lt haT)
have hTrp : p ≤ (T : ℝ) * r :=
le_trans hge (mul_le_mul_of_nonneg_right hTge (le_of_lt hr))
have hopt : optCost r p T = p := optCost_eq_buy r p T hTrp
rw [hcost, hopt]
have hle : (a : ℝ) * r ≤ p := le_of_lt hlt
nlinarithDeterministic lower bound
A deterministic online strategy for ski rental is its first buy day: it
rents days 0 .. a-1 and buys on day a if the trip lasts that long, or
none if it never buys. This is the general causal model — an online strategy
must decide rent-or-buy day by day without seeing the future, and buying is
irreversible, so the whole strategy is determined by this one threshold.
abbrev Strategy := Option ℕ
The cost of running a deterministic strategy s on a trip of T days: the
threshold strategy rents then buys, and the never-buy strategy rents every day.
def onlineCost (r p : ℝ) (s : Strategy) (T : ℕ) : ℝ :=
match s with
| none => (T : ℝ) * r
| some a => rentThenBuyCost r p a T
Theorem 27.1 (deterministic lower bound), threshold form. For r > 0 and
p > 0, no rent-a-then-buy threshold strategy beats competitive ratio
2 - r/p: for every a there is an input T on which it pays at least
(2 - r/p) times the optimal offline cost.
theorem rentThenBuy_lower_bound (r p : ℝ) (hr : 0 < r) (hp : 0 < p) (a : ℕ) :
∃ T : ℕ, 0 < T ∧ 0 < optCost r p T ∧
(2 - r / p) * optCost r p T ≤ rentThenBuyCost r p a T := by
refine ⟨a + 1, by omega, ?_, ?_⟩
· exact lt_min (mul_pos (by positivity) hr) hp
have ha0 : 0 ≤ (a : ℝ) := Nat.cast_nonneg a
have hcost : rentThenBuyCost r p a (a + 1) = (a : ℝ) * r + p := by
apply rentThenBuyCost_long
intro h
omega
rw [hcost]
have hcast : ((a + 1 : ℕ) : ℝ) = (a : ℝ) + 1 := by norm_num
by_cases hle : ((a + 1 : ℕ) : ℝ) * r ≤ p
· -- Buy early: the optimum is to rent every day; the strategy buys too soon.
have hopt : optCost r p (a + 1) = ((a + 1 : ℕ) : ℝ) * r := optCost_eq_rent r p (a + 1 : ℕ) hle
rw [hopt]
rw [hcast]
have hrp : r ≤ p := by
have h1 : r ≤ ((a : ℝ) + 1) * r := by nlinarith [ha0]
exact le_trans h1 (by rw [← hcast]; exact hle)
have hprod : 0 ≤ (p - r) * (p - ((a : ℝ) + 1) * r) := by
exact mul_nonneg (sub_nonneg.mpr hrp) (sub_nonneg.mpr (by rw [← hcast]; exact hle))
have hid : p * ((a : ℝ) * r + p - (2 - r / p) * ((a : ℝ) + 1) * r) =
(p - r) * (p - ((a : ℝ) + 1) * r) := by
field_simp [ne_of_gt hp]
ring
have hnn : 0 ≤ p * ((a : ℝ) * r + p - (2 - r / p) * ((a : ℝ) + 1) * r) := by
rw [hid]
exact hprod
nlinarith
· -- Buy at or after break-even: the optimum is to buy; the strategy rents first.
have hopt : optCost r p (a + 1) = p := optCost_eq_buy r p (a + 1 : ℕ) (le_of_not_ge hle)
rw [hopt]
have hge : p ≤ ((a : ℝ) + 1) * r := by rw [← hcast]; exact le_of_not_ge hle
field_simp [ne_of_gt hp]
nlinarith [hr, hp, hge, ha0]
Theorem 27.1 (deterministic lower bound). For r > 0 and p > 0, no
deterministic online strategy beats competitive ratio 2 - r/p: for every
strategy there is an input T on which it pays at least (2 - r/p) times the
optimal offline cost.
theorem skiRental_lower_bound (r p : ℝ) (hr : 0 < r) (hp : 0 < p) (s : Strategy) :
∃ T : ℕ, 0 < T ∧ 0 < optCost r p T ∧
(2 - r / p) * optCost r p T ≤ onlineCost r p s T := by
cases s with
| none =>
-- The strategy never buys, so on a long trip it pays `T * r` while the
-- optimum is `p`; pick `T` large enough that `2 * p - r ≤ T * r`.
obtain ⟨T, hT⟩ := exists_nat_gt (2 * p / r : ℝ)
have hTpos : 0 < T := by
have hq : 0 < 2 * p / r := div_pos (by positivity) hr
have ht : (0 : ℝ) < T := hq.trans hT
exact_mod_cast ht
refine ⟨T, hTpos, lt_min (mul_pos (by exact_mod_cast hTpos) hr) hp, ?_⟩
simp [onlineCost]
have hmul : (2 * p / r) * r < (T : ℝ) * r := mul_lt_mul_of_pos_right hT hr
have h2p : (2 * p / r) * r = 2 * p := by field_simp [ne_of_gt hr]
have hTr : 2 * p - r ≤ (T : ℝ) * r := by nlinarith
have hp_opt : p ≤ (T : ℝ) * r := by nlinarith
have hopt : optCost r p T = p := optCost_eq_buy r p T hp_opt
rw [hopt]
have hgoal : (2 - r / p) * p = 2 * p - r := by field_simp [ne_of_gt hp]
rw [hgoal]
exact hTr
| some a =>
simp [onlineCost]
exact rentThenBuy_lower_bound r p hr hp aStrictly smaller ratios fail on a positive-cost finite input.
theorem skiRental_not_competitive_below (r p c : ℝ) (hr : 0 < r) (hp : 0 < p)
(s : Strategy) (hc : c < 2 - r / p) :
∃ T : ℕ, 0 < T ∧ c * optCost r p T < onlineCost r p s T := by
obtain ⟨T,hT,hpos,hbound⟩ := skiRental_lower_bound r p hr hp s
exact ⟨T,hT,(mul_lt_mul_of_pos_right hc hpos).trans_le hbound⟩end SkiRentalnamespace Elevator
The cost of the online strategy that waits w seconds and then, if the
elevator has not arrived, takes the stairs, on the input where the elevator
arrives after t seconds. If it arrives in time (t ≤ w) the cost is
t + E; otherwise the elevator is treated as having never arrived and the cost
is w + S. E is the elevator ride time and S the stairs time (CLRS §27.1).
def cost (E S w t : ℝ) : ℝ :=
if t ≤ w then t + E else w + S
The optimal offline cost when the elevator arrives after t seconds: take
the elevator (waiting t and riding E) or take the stairs immediately
(cost S), whichever is cheaper.
def optCost (E S t : ℝ) : ℝ :=
min (t + E) S
Elevator corollary of Theorem 27.1. With elevator ride time E ≥ 0 and
stairs time S > 0, the strategy that waits S - E seconds and then takes the
stairs is 2-competitive: whatever the arrival time t ≥ 0, it pays at most
twice the optimal offline cost.
set_option linter.unusedVariables false in
theorem elevator_two_competitive (E S : ℝ) (hE : 0 ≤ E) (hS : 0 < S) :
∀ t : ℝ, 0 ≤ t → cost E S (S - E) t ≤ 2 * optCost E S t := by
intro t ht0
by_cases ht : t ≤ S - E
· -- The elevator comes in time: the strategy is optimal.
have hcost : cost E S (S - E) t = t + E := by
simp [cost, ht]
have hle : t + E ≤ S := by
nlinarith
have hopt : optCost E S t = t + E := by
rw [optCost]
exact min_eq_left hle
rw [hcost, hopt]
have hnn : 0 ≤ t + E := by
nlinarith [ht0, hE]
nlinarith
· -- The elevator comes late (or never): the strategy takes the stairs.
have hcost : cost E S (S - E) t = (S - E) + S := by
simp [cost, ht]
have hgt : S < t + E := by
nlinarith
have hopt : optCost E S t = S := by
rw [optCost]
exact min_eq_right (le_of_lt hgt)
rw [hcost, hopt]
nlinarith [hS, hE]
Worst-case cost ratio. When the elevator comes after the wait time, the
"wait S - E then take the stairs" strategy pays exactly (2 - E/S) * S, so
its competitive ratio on that input is 2 - E/S, matching the value stated in
CLRS §27.1. For 0 < E < S the ratio lies strictly between 1 and 2 (the
second conjunct).
lemma elevator_worst_case_ratio (E S : ℝ) (hE : 0 < E) (hS : 0 < S) :
(S - E) + S = (2 - E / S) * S ∧ (2 - E / S) < 2 := by
constructor
· field_simp [ne_of_gt hS]
ring
· have hpos : 0 < E / S := div_pos hE hS
linarith
Elevator lower bound (corollary of the deterministic bound). For
0 < E < S, no deterministic wait threshold w beats competitive ratio
2 - E/S: for every w ≥ 0 there is an arrival time t ≥ 0 on which the
strategy pays at least (2 - E/S) times the optimal offline cost. Combined
with elevator_two_competitive, the "wait S - E then stairs" strategy is
exactly optimal.
theorem elevator_lower_bound (E S : ℝ) (hE : 0 < E) (hS : 0 < S) (hES : E < S)
(w : ℝ) (hw0 : 0 ≤ w) :
∃ t : ℝ, 0 ≤ t ∧ (2 - E / S) * optCost E S t ≤ cost E S w t := by
have hdiv : E / S < 1 := (div_lt_one hS).mpr hES
have hden : 0 < 2 - E / S := by nlinarith
have hden2 : 2 - E / S ≠ 0 := ne_of_gt hden
have hpos : 0 < S * 2 - E := by nlinarith [hES, hS]
have hpos2 : S * 2 - E ≠ 0 := ne_of_gt hpos
by_cases hw : w < S - E
· -- Too eager: the elevator arrives exactly at the adversarial boundary.
let t : ℝ := (w + S) / (2 - E / S) - E
refine ⟨t, ?_, ?_⟩
· dsimp [t]
field_simp [hden2, hpos2, ne_of_gt hS]
nlinarith [hE, hS, hw0, sq_nonneg (S - E)]
· have hgt : w < t := by
dsimp [t]
field_simp [hden2, hpos2, ne_of_gt hS]
nlinarith [hw, hES, hS]
have hcost : cost E S w t = w + S := by
rw [cost, if_neg (show ¬ t ≤ w by intro htle; nlinarith [hgt])]
rw [hcost]
have hleS : t + E ≤ S := by
dsimp [t]
field_simp [hden2, hpos2, ne_of_gt hS]
nlinarith [hw, hES, hS]
have hopt : optCost E S t = t + E := by
rw [optCost]
exact min_eq_left hleS
rw [hopt]
have hmain : (2 - E / S) * (t + E) = w + S := by
dsimp [t]
rw [sub_add_cancel]
exact mul_div_cancel₀ (w + S) hden2
rw [hmain]
· -- Patient enough: the elevator arrives after the wait is abandoned.
refine ⟨w + 1, ?_, ?_⟩
· nlinarith [hw0]
· have hle : S - E ≤ w := le_of_not_gt hw
have hcost : cost E S w (w + 1) = w + S := by
rw [cost, if_neg (show ¬ w + 1 ≤ w by nlinarith)]
rw [hcost]
have hopt : optCost E S (w + 1) = S := by
rw [optCost]
apply min_eq_right
nlinarith [hle]
rw [hopt]
have hmain : (2 - E / S) * S = 2 * S - E := by
field_simp [ne_of_gt hS]
rw [hmain]
exact (by nlinarith [hle] : 2 * S - E ≤ w + S)end Elevatorend CLRSImports
import Mathlib.Data.List.Basic
import Mathlib.Tactic27.2. Maintaining a Search List
This section formalizes the list-update problem that CLRS §27.2 uses to
introduce amortized competitive analysis of online algorithms. A list of
distinct keys must serve a sequence of requests; servicing a request costs the
position of the requested key (scanning from the front). An online list-update
strategy may rearrange its list after each request, paying for the
rearrangement, while the offline optimum knows the whole request sequence in
advance. MOVE-TO-FRONT — after each request, move the requested key to the
front — is the classic deterministic strategy, and CLRS §27.2 proves it is
4-competitive by the potential method.
Main results:
-
Definition
SearchList.before: strict order (abeforeb) in a list. -
Definition
SearchList.position: the 0-based position of a key in a list. -
Definition
SearchList.invDist: the inversion distance between two lists. -
Definition
SearchList.moveToFront: move a key to the front of a list. -
Definition
SearchList.potential: the potential function2 * invDist. -
Lemma
SearchList.invDist_moveToFront_add_pos: the phase-1 potential change. -
Lemma
SearchList.invDist_triangle: the triangle inequality forinvDist. -
Theorem
SearchList.mtf_step_four_competitive: the per-request amortized bound that drives the potential argument. -
Theorem
SearchList.mtf_four_competitive(Theorem 27.2): MOVE-TO-FRONT is4-competitive against any list-update strategy.
The core theorem uses equality of resident key sets via toFinset,
not List.Perm or a no-duplicates invariant. Its counters remain defined
on lists with duplicate keys, but then distinct preceding keys need not equal
physical indices, and inversion counts are not minimum adjacent-swap costs.
The textbook list interpretation requires distinct-key lists preserved as
permutations. Requests must lie in the list. The competing strategy is a
function List α → α → List α; arbitrary offline traces or auxiliary
strategy state are not represented by this interface. Initial potential is
zero when both strategies start from the same list.
Notation conventions used in this section:
-
L: the list maintained by MOVE-TO-FRONT -
M: the list maintained by the competing strategy -
σ: the request sequence -
x: the current request -
A: a list-update strategy
namespace CLRSnamespace SearchListvariable {α : Type} [DecidableEq α]
a is strictly before b in the list L (both elements present).
def before (a b : α) (L : List α) : Prop :=
b ∈ L ∧ a ∈ L.takeWhile (fun c => decide (c ≠ b))
Membership in before is decidable for DecidableEq α.
instance before_decidable (a b : α) (L : List α) : Decidable (before a b L) :=
inferInstanceAs (Decidable (b ∈ L ∧ a ∈ L.takeWhile (fun c => decide (c ≠ b))))
Number of distinct keys strictly before the first occurrence of x.
This is its 0-based index only for a duplicate-free list containing the key.
def position (x : α) (L : List α) : ℕ :=
(L.toFinset.filter (fun y => before y x L)).cardInversion distance: the number of unordered element pairs whose relative order differs between the two lists. The minimum adjacent-swap interpretation requires duplicate-free permutations; equal key sets alone do not suffice.
def invDist (L₁ L₂ : List α) : ℕ :=
(L₁.toFinset.sum fun a =>
(L₁.toFinset.filter (fun b => before a b L₁ ∧ before b a L₂)).card)
The cost of servicing one request x by scanning from the front: the
1-based position.
def scanCost (x : α) (L : List α) : ℕ := position x L + 1
The cost of servicing one request x with MOVE-TO-FRONT: scanning to the
position and then swapping x to the front (2·position + 1).
def mtfCost (x : α) (L : List α) : ℕ := 2 * position x L + 1
Move-to-front: after accessing x, bring it to the front of the list.
def moveToFront (x : α) (L : List α) : List α := x :: L.erase xA list-update strategy: given the current list and the request, the next list.
abbrev Strategy (α : Type) [DecidableEq α] := List α → α → List αPotential function: twice the inversion distance between the two lists.
def potential (L₁ L₂ : List α) : ℕ := 2 * invDist L₁ L₂
Cost of a general strategy processing request x from list L to list L':
the scan cost plus the inversion distance of the rearrangement.
Total cost of MOVE-TO-FRONT over a request sequence.
def mtfTotalCost : List α → List α → ℕ
| [], _ => 0
| x :: σ, L => mtfCost x L + mtfTotalCost σ (moveToFront x L)Total cost of a strategy over a request sequence.
def strategyTotalCost (A : Strategy α) : List α → List α → ℕ
| [], _ => 0
| x :: σ, L => strategyCost x L (A L x) + strategyTotalCost A σ (A L x)Final list after running MOVE-TO-FRONT over a request sequence.
def mtfRun : List α → List α → List α
| [], L => L
| x :: σ, L => mtfRun σ (moveToFront x L)Final list after running a strategy over a request sequence.
def strategyRun (A : Strategy α) : List α → List α → List α
| [], L => L
| x :: σ, L => strategyRun A σ (A L x)
Recursion rule for before.
-- ========== basic lemmas about `before` ==========
lemma before_cons (a b x : α) (L : List α) :
before a b (x :: L) ↔ x ≠ b ∧ b ∈ L ∧ (a = x ∨ before a b L) := by
constructor
· intro h
rcases h with ⟨hb, ha⟩
have hxb : x ≠ b := by
intro hx
subst x
simp at ha
have hbL : b ∈ L := by
rcases (List.mem_cons.mp hb) with hb' | hb''
· exact (hxb hb'.symm).elim
· exact hb''
have hxneq : decide (x ≠ b) = true := decide_eq_true hxb
refine ⟨hxb, hbL, ?_⟩
· unfold List.takeWhile at ha
rw [hxneq] at ha
rw [List.mem_cons] at ha
rcases ha with ha | hat
· exact Or.inl ha
· exact Or.inr ⟨hbL, hat⟩
· intro h
rcases h with ⟨hxb, hbL, hdisj⟩
have hxneq : decide (x ≠ b) = true := decide_eq_true hxb
have hbcons : b ∈ x :: L := by
rw [List.mem_cons]
exact Or.inr hbL
rcases hdisj with ha | hab
· subst a
unfold before
constructor
· exact hbcons
· unfold List.takeWhile
rw [hxneq]
exact (List.mem_cons_self : x ∈ x :: (L.takeWhile (fun c => decide (c ≠ b))))
· unfold before
constructor
· exact hbcons
· unfold List.takeWhile
rw [hxneq]
rw [List.mem_cons]
exact Or.inr hab.2
before is irreflexive.
lemma before_irrefl (a : α) (L : List α) : ¬ before a a L := by
induction L with
| nil => simp [before]
| cons x rest ih =>
intro h
rw [before_cons] at h
rcases h with ⟨hxa, ha, hdisj⟩
rcases hdisj with heq | hrest
· exact hxa heq.symm
· exact ih hrest
before implies both elements are present.
lemma before_mem_left {a b : α} {L : List α} (h : before a b L) : a ∈ L := by
induction L with
| nil => simp [before] at h
| cons x rest ih =>
rw [before_cons] at h
rcases h with ⟨hxb, hb, hdisj⟩
rcases hdisj with heq | hrest
· simp [heq]
· exact List.mem_cons_of_mem x (ih hrest)
before is asymmetric.
lemma before_asymm {a b : α} {L : List α} (h : before a b L) : ¬ before b a L := by
induction L with
| nil => simp [before] at h
| cons x rest ih =>
intro hba
rw [before_cons] at h hba
rcases h with ⟨hxb, hb, hdisj⟩
rcases hba with ⟨hxa, ha, hdisj'⟩
rcases hdisj with heq | hab
· subst a
exact hxa rfl
· rcases hdisj' with heq' | hba'
· subst b
exact hxb rfl
· exact ih hab hba'
before implies the second element is present.
lemma before_mem_right {a b : α} {L : List α} (h : before a b L) : b ∈ L := h.1
For distinct elements of a list, exactly one of before a b L or before b a L
holds (trichotomy): scanning from the front, whichever of a, b comes first is
the one that is before the other.
lemma before_or_before {a b : α} {L : List α} (ha : a ∈ L) (hb : b ∈ L) (hab : a ≠ b) :
before a b L ∨ before b a L := by
revert a b ha hb hab
induction L with
| nil =>
intro a b ha hb hab
simp at ha
| cons x rest ih =>
intro a b ha hb hab
by_cases hxa : x = a
· subst a
left
rw [before_cons]
have hbrest : b ∈ rest := by
rcases (List.mem_cons.mp hb) with hb1 | hb2
· exact (hab hb1.symm).elim
· exact hb2
exact ⟨hab, hbrest, Or.inl rfl⟩
· by_cases hxb : x = b
· subst b
right
rw [before_cons]
have harest : a ∈ rest := by
rcases (List.mem_cons.mp ha) with ha1 | ha2
· exact (hxa ha1.symm).elim
· exact ha2
exact ⟨hxa, harest, Or.inl rfl⟩
· have ha' : a ∈ rest := by
rcases (List.mem_cons.mp ha) with ha1 | ha2
· exact (hxa ha1.symm).elim
· exact ha2
have hb' : b ∈ rest := by
rcases (List.mem_cons.mp hb) with hb1 | hb2
· exact (hxb hb1.symm).elim
· exact hb2
rcases ih ha' hb' hab with hab' | hba'
· left
rw [before_cons]
exact ⟨hxb, hb', Or.inr hab'⟩
· right
rw [before_cons]
exact ⟨hxa, ha', Or.inr hba'⟩
Consequence of trichotomy: if b is not before a, then a is before b.
lemma before_of_not_before {a b : α} {L : List α} (ha : a ∈ L) (hb : b ∈ L) (hab : a ≠ b)
(hn : ¬ before b a L) : before a b L := by
rcases before_or_before ha hb hab with h | h
· exact h
· exact (hn h).elim
a ∈ L.takeWhile (· ≠ b) is unchanged by erasing an element x that is neither
a nor b.
lemma mem_takeWhile_ne_erase (b x : α) (L : List α) {a : α} (hax : a ≠ x) (hbx : b ≠ x) :
a ∈ (L.erase x).takeWhile (fun c => decide (c ≠ b)) ↔
a ∈ L.takeWhile (fun c => decide (c ≠ b)) := by
induction L with
| nil => simp
| cons y rest ih =>
by_cases hya : a = y
· -- `a` is the head: it survives on both sides unless `y = b` (takeWhile stops
-- before the first `b`), and erasing `x` removes it only when `y = x`.
subst a
by_cases hyb : y = b
· subst b
by_cases hyx : y = x
· subst x
exfalso
exact hbx rfl
· simp [hyx]
· by_cases hyx : y = x
· subst x
exfalso
exact hax rfl
· simp [hyb, hyx]
· -- `a` is not the head: it lives in the tail, where erasing `x` (a ≠ x) and the
-- `takeWhile` prefix (b ≠ x) are both unchanged.
by_cases hyb : y = b
· subst b
by_cases hyx : y = x
· subst x
exfalso
exact hbx rfl
· simp [hyx]
· by_cases hyx : y = x
· subst x
simp [hya, hyb]
· simpa [hya, hyb, hyx] using ih
Erasing an element b does not change the membership of an element a ≠ b
(it removes only the first occurrence of b).
lemma mem_erase_of_ne {a b : α} {L : List α} (h : a ≠ b) : a ∈ L.erase b ↔ a ∈ L := by
induction L with
| nil => simp
| cons y rest ih =>
by_cases hyb : y = b
· subst b
simp [h]
· simp [hyb, ih]
before is invariant under move-to-front of an element distinct from both.
set_option linter.unusedVariables false in
lemma before_moveToFront_iff {a x b : α} {L : List α} (hax : a ≠ x) (hbx : b ≠ x) (hx : x ∈ L) :
before a b (moveToFront x L) ↔ before a b L := by
unfold moveToFront
-- `before a b (x :: L.erase x)` vs `before a b L`: `x ≠ b` keeps the head of the
-- takeWhile prefix, `a ≠ x` drops it from the front, and the erase is invisible
-- to `b` (b ≠ x) and to the prefix (mem_takeWhile_ne_erase).
rw [before]
rw [before]
constructor
· intro h
rcases h with ⟨hbcons, htw⟩
have hbL : b ∈ L := by
rcases (List.mem_cons.mp hbcons) with hbx' | hbEr
· exact (hbx hbx').elim
· exact (mem_erase_of_ne hbx).mp hbEr
have htwL : a ∈ L.takeWhile (fun c => decide (c ≠ b)) := by
have hhead : a ∈ (x :: L.erase x).takeWhile (fun c => decide (c ≠ b)) ↔
a ∈ (L.erase x).takeWhile (fun c => decide (c ≠ b)) := by
have hxnotb : x ≠ b := hbx.symm
simp [hxnotb, hax]
have hEr : a ∈ (L.erase x).takeWhile (fun c => decide (c ≠ b)) := (hhead.mp htw)
exact (mem_takeWhile_ne_erase b x L hax hbx).mp hEr
exact ⟨hbL, htwL⟩
· intro h
rcases h with ⟨hbL, htwL⟩
have hbEr : b ∈ L.erase x := (mem_erase_of_ne hbx).mpr hbL
have hbcons : b ∈ x :: L.erase x := by
simp [hbEr]
have htw : a ∈ (x :: L.erase x).takeWhile (fun c => decide (c ≠ b)) := by
have hhead : a ∈ (x :: L.erase x).takeWhile (fun c => decide (c ≠ b)) ↔
a ∈ (L.erase x).takeWhile (fun c => decide (c ≠ b)) := by
have hxnotb : x ≠ b := hbx.symm
simp [hxnotb, hax]
have hEr : a ∈ (L.erase x).takeWhile (fun c => decide (c ≠ b)) :=
(mem_takeWhile_ne_erase b x L hax hbx).mpr htwL
exact (hhead.mpr hEr)
exact ⟨hbcons, htw⟩
After moving x to the front, x is before every other element of the list.
lemma before_x_front {x b : α} {L : List α} (hb : b ∈ L) (hxb : x ≠ b) :
before x b (moveToFront x L) := by
unfold moveToFront
rw [before]
constructor
· have hbEr : b ∈ L.erase x := (mem_erase_of_ne hxb.symm).mpr hb
simp [hbEr]
· simp [hxb]Nothing is before the element moved to the front.
lemma not_before_x_front {x b : α} (L : List α) : ¬ before b x (moveToFront x L) := by
unfold moveToFront
simp [before]The element set of a list is unchanged by moving an element of the list to the front.
lemma toFinset_moveToFront {x : α} {L : List α} (hx : x ∈ L) :
(moveToFront x L).toFinset = L.toFinset := by
unfold moveToFront
ext y
by_cases hyx : y = x
· subst y
simp [hx]
· simp [hyx, mem_erase_of_ne hyx]The moved element is at the front after move-to-front.
lemma mem_moveToFront (x : α) (L : List α) : x ∈ moveToFront x L := by
unfold moveToFront
simpMove-to-front of an element already in the list preserves membership of every other element.
lemma mem_moveToFront_of_ne {x y : α} {L : List α} (hxy : x ≠ y) (hy : y ∈ L) :
y ∈ moveToFront x L := by
unfold moveToFront
have hyx : y ≠ x := hxy.symm
have hyEr : y ∈ L.erase x := (mem_erase_of_ne hyx).mpr hy
simp [hyx, hyEr]The position of the moved element after move-to-front is zero.
lemma position_moveToFront (x : α) (L : List α) : position x (moveToFront x L) = 0 := by
unfold position
simp [not_before_x_front]
position counts distinct keys preceding x.
lemma position_eq_card {x : α} {L : List α} :
position x L = (L.toFinset.filter (fun y => before y x L)).card := rfl
Elements before x in both lists: the inversion pairs newly created or destroyed
when move-to-front brings x forward.
def commonBefore (x : α) (L M : List α) : ℕ :=
(L.toFinset.filter (fun y => before y x L ∧ before y x M)).card
The number of elements before x in both lists is at most the number before it in
the second.
lemma commonBefore_le_position {x : α} {L M : List α} (hperm : L.toFinset = M.toFinset) :
commonBefore x L M ≤ position x M := by
unfold commonBefore position
rw [← hperm]
apply Finset.card_le_card
intro y hy
simp at hy ⊢
exact ⟨hy.1, hy.2.2⟩
Number of pairs (a, b) from L₁ (with b ≠ a) such that a is before b in
L₁ and b before a in L₂: the row of a in the inversion matrix.
def invFrom (a : α) (L₁ L₂ : List α) : ℕ :=
(L₁.toFinset.filter (fun b => before a b L₁ ∧ before b a L₂)).card
invDist is the sum of its rows.
lemma invDist_eq_sum_invFrom (L₁ L₂ : List α) :
invDist L₁ L₂ = ∑ a ∈ L₁.toFinset, invFrom a L₁ L₂ := rfl
The row of a does not change under move-to-front of x ≠ a, except that the
b = x entry disappears (as x is now at the front).
set_option linter.unusedVariables false in
lemma invFrom_ne_moveToFront {a x : α} {L M : List α} (hax : a ≠ x) (hx : x ∈ L)
(hperm : L.toFinset = M.toFinset) :
invFrom a (moveToFront x L) M + (if before a x L ∧ before x a M then 1 else 0) =
invFrom a L M := by
unfold invFrom
-- Rows a (≠ x): the `b ≠ x` columns are invariant (before_moveToFront_iff), and the
-- `b = x` column is dropped because nothing precedes the moved front element.
have hfilter :
(moveToFront x L).toFinset.filter
(fun b => before a b (moveToFront x L) ∧ before b a M) =
(L.toFinset.erase x).filter (fun b => before a b L ∧ before b a M) := by
rw [toFinset_moveToFront hx]
ext b
simp only [Finset.mem_filter]
by_cases hbx : b = x
· subst b
simp [not_before_x_front]
· simp [hbx, before_moveToFront_iff hax hbx hx]
rw [hfilter]
-- The erased column contributes exactly the `(a, x)` entry back.
have hxS : x ∈ L.toFinset := by simpa using hx
have hsplit :
(L.toFinset.filter (fun b => before a b L ∧ before b a M)).card =
((L.toFinset.erase x).filter (fun b => before a b L ∧ before b a M)).card +
(if before a x L ∧ before x a M then 1 else 0) := by
by_cases hP : before a x L ∧ before x a M
· have hxfilter : x ∈ L.toFinset.filter (fun b => before a b L ∧ before b a M) := by
simp [hP, hxS]
have hsplit' :
L.toFinset.filter (fun b => before a b L ∧ before b a M) =
insert x ((L.toFinset.erase x).filter (fun b => before a b L ∧ before b a M)) := by
ext b
by_cases hbx : b = x
· subst b
simp [hP, hxS]
· simp [hbx]
rw [hsplit']
rw [Finset.card_insert_of_notMem]
· simp [hP]
· intro hmem
rw [Finset.mem_filter, Finset.mem_erase] at hmem
exact hmem.1.1 rfl
· have hsplit' :
L.toFinset.filter (fun b => before a b L ∧ before b a M) =
(L.toFinset.erase x).filter (fun b => before a b L ∧ before b a M) := by
ext b
by_cases hbx : b = x
· subst b
simp [hP]
· simp [hbx]
rw [hsplit']
simp [hP]
rw [hsplit]
The row of x after move-to-front counts exactly the elements before x in M.
lemma invFrom_x_moveToFront {x : α} {L M : List α} (hperm : L.toFinset = M.toFinset) :
invFrom x (moveToFront x L) M = position x M := by
unfold invFrom position
apply congrArg Finset.card
ext b
simp only [Finset.mem_filter]
constructor
· intro hb
rcases hb with ⟨hb1, hb2, hb3⟩
-- `before b x M` already forces `b ∈ M`.
exact ⟨by simpa using before_mem_left hb3, hb3⟩
· intro hb
rcases hb with ⟨hb1, hb3⟩
have hbx : x ≠ b := by
intro hxeq
subst b
exact before_irrefl x M hb3
have hbL : b ∈ L := by
have hbMto : b ∈ M.toFinset := by simpa using (before_mem_left hb3 : b ∈ M)
have hbLto : b ∈ L.toFinset := by simpa [hperm] using hbMto
simpa using hbLto
exact ⟨by simpa using mem_moveToFront_of_ne hbx hbL,
before_x_front hbL hbx, hb3⟩
The row of x before move-to-front misses exactly the commonBefore pairs: the
elements before x in M are split by trichotomy into those also before x in L
(the commonBefore pairs) and those that x precedes in L (the invFrom row).
set_option linter.unusedVariables false in
lemma invFrom_x_eq {x : α} {L M : List α} (hperm : L.toFinset = M.toFinset)
(hxL : x ∈ L) (hxM : x ∈ M) :
invFrom x L M + commonBefore x L M = position x M := by
unfold invFrom commonBefore position
rw [← hperm]
-- Split `position x M` over `M.toFinset` by whether each element precedes `x` in `L`.
have hsplit :
(L.toFinset.filter (fun b => before b x M)).card =
(L.toFinset.filter (fun b => before b x L ∧ before b x M)).card +
(L.toFinset.filter (fun b => ¬ before b x L ∧ before b x M)).card := by
have hdisj : Disjoint (L.toFinset.filter (fun b => before b x L ∧ before b x M))
(L.toFinset.filter (fun b => ¬ before b x L ∧ before b x M)) := by
rw [Finset.disjoint_left]
intro b hb1 hb2
simp at hb1 hb2
exact hb2.2.1 hb1.2.1
have hunion : (L.toFinset.filter (fun b => before b x L ∧ before b x M)) ∪
(L.toFinset.filter (fun b => ¬ before b x L ∧ before b x M)) =
L.toFinset.filter (fun b => before b x M) := by
ext b
simp
by_cases h : before b x L
· simp [h]
· simp [h]
rw [← Finset.card_union_of_disjoint hdisj, hunion]
-- `¬ before b x L` is `before x b L` for the elements before `x` in `M` (trichotomy).
have hnot :
(L.toFinset.filter (fun b => ¬ before b x L ∧ before b x M)) =
(L.toFinset.filter (fun b => before x b L ∧ before b x M)) := by
ext b
simp only [Finset.mem_filter]
by_cases hbL : b ∈ L.toFinset
· by_cases hbM : before b x M
· have hb : b ∈ L := by simpa using hbL
have hbne : b ≠ x := by
intro hb
subst b
exact before_irrefl x M hbM
-- Trichotomy in `L` (both `b` and `x` are in `L`) decides `¬ before b x L ↔ before x b L`.
rcases before_or_before hb hxL hbne with hl | hxl
· simp [hl, before_asymm hl]
· simp [hxl, before_asymm hxl]
· simp [hbM]
· simp [hbL]
rw [hsplit, hnot]
rw [Nat.add_comm]
The position of x splits into the elements before x in both lists and those that
x precedes in M — the pairs whose inversion is destroyed by move-to-front.
lemma position_eq_common_add_removed {x : α} {L M : List α} (hxL : x ∈ L)
(hperm : L.toFinset = M.toFinset) :
position x L =
commonBefore x L M +
(L.toFinset.filter (fun a => before a x L ∧ before x a M)).card := by
unfold position commonBefore
have hsplit :
(L.toFinset.filter (fun a => before a x L)).card =
(L.toFinset.filter (fun a => before a x L ∧ before a x M)).card +
(L.toFinset.filter (fun a => before a x L ∧ ¬ before a x M)).card := by
have hdisj : Disjoint (L.toFinset.filter (fun a => before a x L ∧ before a x M))
(L.toFinset.filter (fun a => before a x L ∧ ¬ before a x M)) := by
rw [Finset.disjoint_left]
intro a ha1 ha2
simp at ha1 ha2
exact ha2.2.2 ha1.2.2
have hunion : (L.toFinset.filter (fun a => before a x L ∧ before a x M)) ∪
(L.toFinset.filter (fun a => before a x L ∧ ¬ before a x M)) =
L.toFinset.filter (fun a => before a x L) := by
ext a
simp
by_cases h : before a x M
· simp [h]
· simp [h]
rw [← Finset.card_union_of_disjoint hdisj, hunion]
have hnot :
(L.toFinset.filter (fun a => before a x L ∧ ¬ before a x M)) =
(L.toFinset.filter (fun a => before a x L ∧ before x a M)) := by
ext a
simp only [Finset.mem_filter]
by_cases haL : a ∈ L.toFinset
· by_cases hax : before a x L
· have ha : a ∈ L := by simpa using haL
have hane : a ≠ x := by
intro hxeq
subst a
exact before_irrefl x L hax
have hxM' : x ∈ M := by
have : x ∈ L.toFinset := by simpa using hxL
simpa [hperm] using this
have haM : a ∈ M := by
have : a ∈ L.toFinset := by simpa using ha
simpa [hperm] using this
rcases before_or_before haM hxM' hane with hm | hxm
· simp [hm, before_asymm hm]
· simp [hxm, before_asymm hxm]
· simp [hax]
· simp [haL]
rw [hsplit, hnot]
Phase-1 potential change. Moving x to the front of L, with the other list M
held fixed, changes the inversion distance by 2·commonBefore x L M − position x L
(stated in the truncation-free form). This is the engine of the move-to-front
amortized analysis: the amortized cost of the move is roughly 1 + 4·commonBefore.
lemma invDist_moveToFront_add_pos {x : α} {L M : List α} (hxL : x ∈ L) (hxM : x ∈ M)
(hperm : L.toFinset = M.toFinset) :
invDist (moveToFront x L) M + position x L = invDist L M + 2 * commonBefore x L M := by
let S : Finset α := L.toFinset
let L' : List α := moveToFront x L
let ind : α → ℕ := fun a => (if before a x L ∧ before x a M then 1 else 0)
let removed : ℕ := (S.filter (fun a => before a x L ∧ before x a M)).card
let pos : ℕ := (S.filter (fun a => before a x L)).card
have hxS : x ∈ S := by simpa [S] using hxL
have hL'toS : L'.toFinset = S := by
dsimp [L', S]
exact toFinset_moveToFront hxL
-- Row relations of the inversion matrix under move-to-front.
have hrow_ne : ∀ a, a ≠ x →
(S.filter (fun b => before a b L' ∧ before b a M)).card + ind a =
(S.filter (fun b => before a b L ∧ before b a M)).card := by
intro a ha
have h := invFrom_ne_moveToFront (a := a) ha hxL hperm
dsimp [ind, L', S] at h ⊢
unfold invFrom at h
rw [toFinset_moveToFront hxL] at h
exact h
have hrow_x :
(S.filter (fun b => before x b L' ∧ before b x M)).card =
(S.filter (fun b => before x b L ∧ before b x M)).card + commonBefore x L M := by
have h1 := invFrom_x_moveToFront (x := x) hperm
have h2 := invFrom_x_eq hperm hxL hxM
have h1' : (S.filter (fun b => before x b L' ∧ before b x M)).card = position x M := by
dsimp [L', S] at h1 ⊢
unfold invFrom at h1
rw [toFinset_moveToFront hxL] at h1
exact h1
have h2' : (S.filter (fun b => before x b L ∧ before b x M)).card + commonBefore x L M =
position x M := by
dsimp [S] at h2 ⊢
exact h2
rw [h1']
exact h2'.symm
-- `removed` collects the destroyed inversions, summed over the rows `a ≠ x`.
have hrem : removed = (S.erase x).sum ind := by
have hindx : ind x = 0 := by
dsimp [ind]
simp [before_irrefl x L]
calc
removed = S.sum ind := by
simp [removed, ind, Finset.sum_boole]
_ = (S.erase x).sum ind + ind x := by
rw [← Finset.sum_erase_add S ind hxS]
_ = (S.erase x).sum ind := by
rw [hindx, Nat.add_zero]
-- `position x L` splits into the destroyed inversions and the common-before pairs.
have hpos : pos = commonBefore x L M + removed := by
have h := position_eq_common_add_removed hxL hperm
dsimp [pos, removed, S] at h ⊢
exact h
-- The row-sum identity: moving `x` to the front removes the `removed` inversions and
-- adds `position x M` of them back (the elements before `x` in `M`).
have hmid : invDist L' M + removed = invDist L M + commonBefore x L M := by
let row' : α → ℕ := fun a => (S.filter (fun b => before a b L' ∧ before b a M)).card
let row : α → ℕ := fun a => (S.filter (fun b => before a b L ∧ before b a M)).card
calc
invDist L' M + removed = (S.sum row') + removed := by
congr 1
unfold invDist
rw [hL'toS]
_ = ((S.erase x).sum row' + row' x) + (S.erase x).sum ind := by
rw [Finset.sum_erase_add S row' hxS]
rw [hrem]
_ = ((S.erase x).sum row' + (S.erase x).sum ind) + row' x := by
omega
_ = (S.erase x).sum (fun a => row' a + ind a) + row' x := by
rw [← Finset.sum_add_distrib]
_ = (S.erase x).sum row + (row x + commonBefore x L M) := by
have hcong : (S.erase x).sum (fun a => row' a + ind a) = (S.erase x).sum row := by
apply Finset.sum_congr rfl
intro a ha
have ha' : a ∈ S.erase x := ha
have hane : a ≠ x := by
intro hxeq
subst a
exact (Finset.mem_erase.mp ha').1 rfl
exact hrow_ne a hane
rw [hcong]
dsimp [row', row]
rw [hrow_x]
_ = (S.sum row) + commonBefore x L M := by
rw [← Finset.sum_erase_add S row hxS]
ac_rfl
_ = invDist L M + commonBefore x L M := by
congr 1
-- Put the pieces together.
have htarget : invDist L' M + pos = invDist L M + 2 * commonBefore x L M := by
omega
dsimp [pos, S] at htarget
unfold position at ⊢
exact htarget
Triangle inequality for the inversion distance. For three lists over the same
element set, the inversion distance between the first and the third is at most the
sum of the two intermediate distances. Phase 2 of the amortized analysis uses the
case L₁ = L' (MTF's new list), L₂ = M, L₃ = M' (OPT's list before and after):
OPT's rearrangement of M can increase the potential by at most 2 · invDist M M'.
lemma invDist_triangle {L₁ L₂ L₃ : List α} (h12 : L₁.toFinset = L₂.toFinset)
(h23 : L₂.toFinset = L₃.toFinset) :
invDist L₁ L₃ ≤ invDist L₁ L₂ + invDist L₂ L₃ := by
let S : Finset α := L₁.toFinset
let r13 : α → ℕ := fun a => (S.filter (fun b => before a b L₁ ∧ before b a L₃)).card
let r12 : α → ℕ := fun a => (S.filter (fun b => before a b L₁ ∧ before b a L₂)).card
let r23 : α → ℕ := fun a => (S.filter (fun b => before a b L₂ ∧ before b a L₃)).card
have hrow_le : ∀ a ∈ S, r13 a ≤ r12 a + r23 a := by
intro a haS
have hsplit : r13 a =
(S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ before b a L₂)).card +
(S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ ¬ before b a L₂)).card := by
have hdisj : Disjoint (S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ before b a L₂))
(S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ ¬ before b a L₂)) := by
rw [Finset.disjoint_left]
intro b hb1 hb2
simp at hb1 hb2
exact hb2.2.2.2 hb1.2.2.2
have hunion : (S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ before b a L₂)) ∪
(S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ ¬ before b a L₂)) =
S.filter (fun b => before a b L₁ ∧ before b a L₃) := by
ext b
simp
by_cases h : before b a L₂
· simp [h]
· simp [h]
rw [← Finset.card_union_of_disjoint hdisj, hunion]
have h1 : (S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ before b a L₂)).card ≤ r12 a := by
apply Finset.card_le_card
intro b hb
simp at hb ⊢
exact ⟨hb.1, hb.2.1, hb.2.2.2⟩
have h2 : (S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ ¬ before b a L₂)).card ≤ r23 a := by
apply Finset.card_le_card
intro b hb
simp at hb ⊢
rcases hb with ⟨hbS, hbal1, hba3, hnb2⟩
have hb2 : b ∈ L₂ := by
have : b ∈ L₃ := before_mem_left hba3
have : b ∈ L₃.toFinset := by simpa using this
have : b ∈ L₂.toFinset := by simpa [h23] using this
simpa using this
have ha2 : a ∈ L₂ := by
have ha1 : a ∈ L₁ := before_mem_left hbal1
have : a ∈ L₁.toFinset := by simpa using ha1
have : a ∈ L₂.toFinset := by simpa [h12] using this
simpa using this
have hbne : b ≠ a := by
intro hba
subst b
exact before_irrefl a L₃ hba3
rcases before_or_before hb2 ha2 hbne with hba2 | hab2
· exact (hnb2 hba2).elim
· exact ⟨hbS, hab2, hba3⟩
calc
r13 a = (S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ before b a L₂)).card +
(S.filter (fun b => before a b L₁ ∧ before b a L₃ ∧ ¬ before b a L₂)).card := hsplit
_ ≤ r12 a + r23 a := Nat.add_le_add h1 h2
calc
invDist L₁ L₃ = S.sum r13 := by
unfold invDist
dsimp [S]
_ ≤ S.sum (fun a => r12 a + r23 a) := by
exact Finset.sum_le_sum hrow_le
_ = S.sum r12 + S.sum r23 := by
rw [Finset.sum_add_distrib]
_ = invDist L₁ L₂ + invDist L₂ L₃ := by
congr 1
unfold invDist
dsimp [r23, S]
rw [h12]
MOVE-TO-FRONT is 4-competitive, per request (amortized). For a request x, with
MTF's list L and OPT's list M permutations of the same set, after MTF moves x
to the front (list moveToFront x L) and OPT moves to M', the amortized cost of
MTF's move — its actual cost plus the change in the potential 2 · invDist — is at
most 4 times OPT's cost for the request, plus the previous potential.
theorem mtf_step_four_competitive (x : α) (L M M' : List α)
(hxL : x ∈ L) (hxM : x ∈ M) (hperm : L.toFinset = M.toFinset)
(hperm' : (moveToFront x L).toFinset = M'.toFinset) :
mtfCost x L + 2 * invDist (moveToFront x L) M' ≤
4 * strategyCost x M M' + 2 * invDist L M := by
have hL' : (moveToFront x L).toFinset = L.toFinset := toFinset_moveToFront hxL
have hMM' : M.toFinset = M'.toFinset := (hL'.trans hperm).symm.trans hperm'
have htri := invDist_triangle (L₁ := moveToFront x L) (L₂ := M) (L₃ := M')
(hL'.trans hperm) hMM'
calc
mtfCost x L + 2 * invDist (moveToFront x L) M' ≤
2 * position x L + 1 + 2 * invDist (moveToFront x L) M + 2 * invDist M M' := by
unfold mtfCost
nlinarith [htri]
_ = 1 + 2 * invDist L M + 4 * commonBefore x L M + 2 * invDist M M' := by
have h1 := invDist_moveToFront_add_pos hxL hxM hperm
nlinarith [h1]
_ ≤ 4 * (position x M + 1 + invDist M M') + 2 * invDist L M := by
have hC := commonBefore_le_position (x := x) (L := L) (M := M) hperm
nlinarith [hC, Nat.zero_le (invDist M M')]
_ = 4 * strategyCost x M M' + 2 * invDist L M := by
unfold strategyCost scanCost
ring
Move-to-front of an element of L preserves membership of every element of L.
lemma mem_moveToFront_all {x : α} {L : List α} : ∀ y ∈ L, y ∈ moveToFront x L := by
intro y hy
by_cases hxy : x = y
· subst y
exact mem_moveToFront x L
· exact mem_moveToFront_of_ne hxy hy
MOVE-TO-FRONT is 4-competitive over a request sequence. Against any list-update
strategy A that keeps its list a permutation of the initial set, running MOVE-TO-FRONT
from the same initial list L costs at most 4 times the strategy's cost plus the
initial potential 2 · invDist L L = 0 (the additive term is zero when both start
from the same list). This is Theorem 27.2 in CLRS §27.2.
theorem mtf_four_competitive (A : Strategy α) (σ L M : List α)
(hperm : L.toFinset = M.toFinset)
(hreq : ∀ x ∈ σ, x ∈ L)
(hA : ∀ L x, x ∈ L → (A L x).toFinset = L.toFinset) :
mtfTotalCost σ L ≤ 4 * strategyTotalCost A σ M + 2 * invDist L M := by
induction σ generalizing L M with
| nil =>
simp [mtfTotalCost, strategyTotalCost]
| cons x σ' ih =>
have hxL : x ∈ L := hreq x (by simp)
have hxM : x ∈ M := by
have : x ∈ L.toFinset := by simpa using hxL
have : x ∈ M.toFinset := by simpa [hperm] using this
simpa using this
let L' : List α := moveToFront x L
let M' : List α := A M x
have hperm' : L'.toFinset = M'.toFinset := by
dsimp [L', M']
rw [toFinset_moveToFront hxL, hA M x hxM]
exact hperm
have hreq' : ∀ y ∈ σ', y ∈ L' := by
intro y hy
have hyL : y ∈ L := hreq y (by simp [hy])
simpa [L'] using mem_moveToFront_all y hyL
have hstep := mtf_step_four_competitive x L M M' hxL hxM hperm (by simpa [L'] using hperm')
have hrec := ih L' M' hperm' hreq'
have hcombo : mtfCost x L + mtfTotalCost σ' L' ≤
4 * strategyCost x M M' + 4 * strategyTotalCost A σ' M' + 2 * invDist L M := by
nlinarith [hstep, hrec]
unfold mtfTotalCost strategyTotalCost
simp [L', M'] at hcombo ⊢
nlinarith [hcombo]Inversion distance between a list and itself is zero.
lemma invDist_self_zero (L : List α) : invDist L L = 0 := by
unfold invDist
simp
intro i hi x hx hix
exact before_asymm hixend SearchListend CLRSImports
import Mathlib.Data.Finset.Basic
import Mathlib.Data.List.Basic
import Mathlib.Tactic27.3. Online Caching
This section formalizes the online caching (paging) problem of CLRS §27.3
and the classical competitive-analysis bound that the LRU (least-recently-used)
eviction policy is k-competitive. A cache holds at most k pages. A request
for a page already in the cache is a hit (free); a request for an absent page
is a miss, and the page is loaded, evicting some resident page when the cache
is full. An online policy must decide evictions without knowing future
requests; the offline optimum knows the whole request sequence.
Main results:
-
Definition
lruStep/lruRun/lruMisses: the LRU policy (cache kept as a most-recent-first list) and its miss count. -
Structure
Algorithm: a legacy memoryless eviction policy over a finite-set cache, bundled with its validity laws (it loads the request, only adds the request, preserves capacity and hit residency on legal states), andmisses. ThePoliciescompanion adds history-dependent policies and an actual inhabited LRU instance for positive capacities. -
Lemma
distinct_fault: a segment requestingk + 1distinct pages forces a miss for any size-kalgorithm. -
Lemma
resident_fault: a page resident at the start of a segment that then requestskother distinct pages forces a miss. -
Lemma
lru_head_evict/lru_miss_le_distinct: LRU makes at most one fault per distinct page within a segment of at mostkdistinct pages. -
Definitions
phaseGo/firstPhase/phases: the phase partition of a request sequence into maximal segments of at mostkdistinct pages. -
Lemma
lru_miss_le_phases: LRU makes at mostkmisses per phase, so its total miss count is bounded byktimes the number of phases. -
Lemma
phases_le_misses: the number of phases is at most one more than the miss count of any size-kalgorithm. -
Theorem
lru_k_competitive: LRU isk-competitive. -
Lemma
advSeq_misses: the Sleator-Tarjan adversary (always request the page absent from the online algorithm's cache, over thek + 1-page universeFin (k + 1)) faults the online algorithm on every request. -
Lemma
offMisses_bound/offMisses_bound_from_full: the phase-based offline schedule faults at most(phases k σ).length + ktimes from an empty cache, and at most once per phase from a full cache. -
Theorem
caching_lower_bound(Theorem 27.4): for any deterministic online algorithm and any request countNthere is a request sequence of lengthNon which the algorithm faults every request while some offline schedule faults at mostN / k + k + 1times — so no deterministic online algorithm is natural-ratio multiplicative bound belowk. The strongerPolicy.no_real_competitivecompanion includes real ratios, arbitrary additive constants and auxiliary state;Schedule.lru_k_competitiveproves the upper bound against every legal offline trace.
Notation conventions used in this section:
-
k: the cache size -
L: LRU's cache, aList Pageordered most-recent-first -
C: an algorithm's cache, aFinset Page -
σ: the request sequence -
q: a page -
A: an arbitrary eviction algorithm
namespace CLRSnamespace OnlineCachingvariable {Page : Type} [DecidableEq Page]variable {k : ℕ}
The cache size bound: a cache holds at most k pages.
def AtMost (k : ℕ) (C : Finset Page) : Prop := C.card ≤ k
The LRU step: request p against the most-recent-first cache L. A hit
moves p to the front; a miss prepends p, evicting the least-recently-used
tail when the cache already has k pages.
-- ---------------------------------------------------------------------------
-- LRU: the cache is a most-recent-first list.
def lruStep (k : ℕ) (L : List Page) (p : Page) : List Page :=
if p ∈ L then p :: L.erase p
else if L.length < k then p :: L
else p :: L.dropLast
The LRU cache after processing the request list σ.
def lruRun (k : ℕ) : List Page → List Page → List Page
| L, [] => L
| L, p :: σ => lruRun k (lruStep k L p) σ
The number of LRU misses over σ, threading the cache.
def lruMissesGo (k : ℕ) : List Page → List Page → ℕ
| _, [] => 0
| L, p :: σ => (if p ∈ L then 0 else 1) + lruMissesGo k (lruStep k L p) σ
The number of LRU misses over σ from the initial cache L₀.
def lruMisses (k : ℕ) (L₀ : List Page) (σ : List Page) : ℕ := lruMissesGo k L₀ σ
The 0-indexed position of q in a most-recent-first list: the number of
pages strictly more recent than q (junk when q ∉ L).
def lruPos (q : Page) (L : List Page) : ℕ := (L.takeWhile (fun x => x ≠ q)).length
A deterministic paging algorithm with cache size bound k. step C p is the
cache after serving request p from cache C; the bundled laws say it loads
p, only ever adds p, and keeps at most k resident pages. This is the
legacy memoryless policy interface. Size preservation and hit behavior are
required only for legal capacity states; invalid oversized caches cannot force
a contradiction. Positive capacity is necessary when pages can be requested.
History-dependent online policies and offline schedules are distinct interfaces
in the companion development.
-- ---------------------------------------------------------------------------
-- An arbitrary eviction algorithm (the "adversary" / offline optimum).
structure Algorithm (Page : Type) [DecidableEq Page] (k : ℕ) where
step : Finset Page → Page → Finset Page
step_loads : ∀ C p, p ∈ step C p
step_subset : ∀ C p, step C p ⊆ insert p C
step_size : ∀ C p, C.card ≤ k → (step C p).card ≤ k
step_hit : ∀ C p, C.card ≤ k → p ∈ C → step C p = C
The algorithm's cache after processing σ.
def runGo (A : Algorithm Page k) : Finset Page → List Page → Finset Page
| C, [] => C
| C, p :: σ => runGo A (A.step C p) σ
The number of misses of A over σ, threading the cache.
def missesGo (A : Algorithm Page k) : Finset Page → List Page → ℕ
| _, [] => 0
| C, p :: σ => (if p ∈ C then 0 else 1) + missesGo A (A.step C p) σ
The number of misses of A over σ from the initial cache C₀.
The algorithm's cache never shrinks the set of requested pages beyond the
initial cache: runGo A C σ ⊆ C ∪ σ.toFinset.
-- ---------------------------------------------------------------------------
-- Basic facts about the algorithm's cache.
lemma runGo_subset (A : Algorithm Page k) (C : Finset Page) (σ : List Page) :
runGo A C σ ⊆ C ∪ σ.toFinset := by
induction σ generalizing C with
| nil => simp [runGo]
| cons p τ ih =>
rw [runGo]
refine subset_trans (ih (A.step C p)) ?_
rw [List.toFinset_cons]
intro x hx
rw [Finset.mem_union] at hx ⊢
rcases hx with hx | hx
· rw [Finset.mem_insert]
have hx' := A.step_subset C p hx
rw [Finset.mem_insert] at hx'
rcases hx' with rfl | hx'
· right; simp
· left; exact hx'
· right
rw [Finset.mem_insert]
exact Or.inr hx
The algorithm's cache has at most k pages throughout (given it starts
within the bound).
lemma runGo_size (A : Algorithm Page k) (C : Finset Page) (σ : List Page)
(hC : C.card ≤ k) : (runGo A C σ).card ≤ k := by
induction σ generalizing C with
| nil => simpa [runGo] using hC
| cons p τ ih =>
rw [runGo]
exact ih (A.step C p) (A.step_size C p hC)If a run has no misses, every requested page was already resident: the requested pages are a subset of the initial cache.
lemma missesGo_eq_zero_subset (A : Algorithm Page k) (C : Finset Page) (σ : List Page)
(h : missesGo A C σ = 0) : σ.toFinset ⊆ C := by
induction σ generalizing C with
| nil => simp
| cons p τ ih =>
rw [missesGo] at h
have hp : p ∈ C := by
by_cases hp : p ∈ C
· exact hp
· simp [hp] at h
have hτ : missesGo A (A.step C p) τ = 0 := by
by_cases hp' : p ∈ C
· simpa [hp'] using h
· simp [hp'] at h
rw [List.toFinset_cons]
intro x hx
rw [Finset.mem_insert] at hx
rcases hx with rfl | hx
· exact hp
· have hx' := ih (A.step C p) hτ hx
have hx'' := A.step_subset C p hx'
rw [Finset.mem_insert] at hx''
rcases hx'' with rfl | hxC
· exact hp
· exact hxC
A segment that requests k + 1 distinct pages forces a miss for any
size-k algorithm.
lemma distinct_fault (A : Algorithm Page k) (C : Finset Page) (ρ : List Page)
(hC : C.card ≤ k) (hk : k < ρ.toFinset.card) : 1 ≤ missesGo A C ρ := by
by_contra hzero
have h0 : missesGo A C ρ = 0 := by omega
have hsub := missesGo_eq_zero_subset A C ρ h0
have hcard : ρ.toFinset.card ≤ C.card := Finset.card_le_card hsub
omega
A page resident at the start of a segment that then requests k other
distinct pages forces a miss for any size-k algorithm.
lemma resident_fault (A : Algorithm Page k) (C : Finset Page) (p : Page) (ρ : List Page)
(hC : C.card ≤ k) (hp : p ∈ C) (hk : k ≤ (ρ.toFinset.erase p).card) :
1 ≤ missesGo A C ρ := by
by_contra hzero
have h0 : missesGo A C ρ = 0 := by omega
have hsub := missesGo_eq_zero_subset A C ρ h0
-- Every distinct page of ρ lies in C; since ρ also requests k pages other than
-- p, those k pages live in C.erase p, but C has at most k pages and p already
-- occupies one of them -- a contradiction.
have hle : (ρ.toFinset.erase p).card ≤ (C.erase p).card := by
apply Finset.card_le_card
intro x hx
rw [Finset.mem_erase] at hx ⊢
exact ⟨hx.1, hsub hx.2⟩
have hCardErase : (C.erase p).card = C.card - 1 := Finset.card_erase_of_mem hp
have hCp : 0 < C.card := Finset.card_pos.mpr ⟨p, hp⟩
omega
The pages strictly more recent than p in a most-recent-first cache L
(the takeWhile prefix of L up to p), as a Finset.
-- ---------------------------------------------------------------------------
-- Structural facts about the LRU list cache.
def front (p : Page) (L : List Page) : Finset Page :=
(L.takeWhile (fun x => x ≠ p)).toFinset
The Finset of a no-duplicate list has its length as cardinality.
lemma toFinset_card_of_nodup {L : List Page} (h : L.Nodup) : L.toFinset.card = L.length := by
rw [List.toFinset, Multiset.card_toFinset, Multiset.coe_dedup, List.dedup_eq_self.mpr h]
rfl
lruStep preserves the no-duplicate invariant of the cache list.
lemma lruStep_nodup (k : ℕ) (L : List Page) (p : Page) (h : L.Nodup) :
(lruStep k L p).Nodup := by
unfold lruStep
split_ifs with hp hl
· exact (List.perm_cons_erase hp).nodup_iff.mp h
· exact List.nodup_cons.mpr ⟨hp, h⟩
· refine List.nodup_cons.mpr ⟨?_, (List.dropLast_sublist L).nodup h⟩
intro hpd
exact hp (List.mem_of_mem_dropLast hpd)
lruRun preserves the no-duplicate invariant of the cache list.
lemma lruRun_nodup (k : ℕ) (L : List Page) (ρ : List Page) (h : L.Nodup) :
(lruRun k L ρ).Nodup := by
induction ρ generalizing L with
| nil => simpa [lruRun] using h
| cons p ρ' ih => simp [lruRun]; exact ih (lruStep k L p) (lruStep_nodup k L p h)The cache after a concatenation of request lists is the sequential run.
lemma lruRun_append (k : ℕ) (L : List Page) (σ τ : List Page) :
lruRun k L (σ ++ τ) = lruRun k (lruRun k L σ) τ := by
induction σ generalizing L with
| nil => rfl
| cons p σ' ih => simp [lruRun, ih]The miss count over a concatenation splits additively.
lemma lruMissesGo_append (k : ℕ) (L : List Page) (σ τ : List Page) :
lruMissesGo k L (σ ++ τ) = lruMissesGo k L σ + lruMissesGo k (lruRun k L σ) τ := by
induction σ generalizing L with
| nil => simp [lruMissesGo, lruRun]
| cons p σ' ih =>
simp [lruMissesGo, lruRun]
rw [ih (lruStep k L p)]
omega
Membership after a single lruStep, unfolded into the hit / not-full miss /
full miss cases.
lemma mem_lruStep (k : ℕ) (L : List Page) (p q : Page) :
q ∈ lruStep k L p ↔
(if p ∈ L then q ∈ L
else if L.length < k then q = p ∨ q ∈ L
else q = p ∨ q ∈ L.dropLast) := by
unfold lruStep
split_ifs with hp hl
· -- hit: q ∈ p :: L.erase p ↔ q ∈ L
rw [List.mem_cons]
constructor
· intro h
rcases h with h | h
· exact h ▸ hp
· exact List.mem_of_mem_erase h
· intro h
by_cases hqp : q = p
· exact Or.inl hqp
· exact Or.inr ((List.mem_erase_of_ne hqp).mpr h)
· -- miss, not full: q ∈ p :: L ↔ q = p ∨ q ∈ L
rw [List.mem_cons]
· -- miss, full: q ∈ p :: L.dropLast ↔ q = p ∨ q ∈ L.dropLast
rw [List.mem_cons]
Erasing an element that satisfies f cannot enlarge the takeWhile f prefix.
lemma takeWhile_erase_subset {f : Page → Bool} {q : Page} {L : List Page} (hfq : f q = true) :
(L.erase q).takeWhile f ⊆ L.takeWhile f := by
induction L with
| nil => simp
| cons x rest ih =>
by_cases hxq : x = q
· subst x; simp [hfq]
· by_cases hx : f x = true
· simp [hxq, hx]
intro a ha
exact List.Mem.tail x (ih ha)
· simp [hxq, hx]
Dropping the last element cannot enlarge the takeWhile f prefix.
lemma takeWhile_dropLast_subset {f : Page → Bool} {L : List Page} :
L.dropLast.takeWhile f ⊆ L.takeWhile f := by
induction L with
| nil => simp
| cons x rest ih =>
cases rest with
| nil => simp
| cons y ys =>
rw [List.dropLast_cons_of_ne_nil (show y :: ys ≠ [] by simp)]
by_cases hx : f x = true
· simp [hx]
intro a ha
exact List.Mem.tail x (ih ha)
· simp [hx]One step grows the front of a page by at most the requested page.
lemma lruStep_front_subset (k : ℕ) (L : List Page) (p q : Page) :
front p (lruStep k L q) ⊆ {q} ∪ front p L := by
by_cases hqp : q = p
· subst q
unfold front lruStep
split_ifs <;> simp [front]
· unfold front lruStep
by_cases hq : q ∈ L
· simp [hq, hqp]
intro x hx
rw [Finset.mem_insert] at hx
rw [Finset.mem_insert]
rcases hx with hxq | hx
· exact Or.inl hxq
· exact Or.inr (List.mem_toFinset.mpr
(takeWhile_erase_subset (f := fun y => !decide (y = p)) (q := q) (by simp [hqp])
(List.mem_toFinset.mp hx)))
· by_cases hl : L.length < k
· simp [hq, hl, hqp]
· simp [hq, hl, hqp]
intro x hx
rw [Finset.mem_insert] at hx
rw [Finset.mem_insert]
rcases hx with hxq | hx
· exact Or.inl hxq
· exact Or.inr (List.mem_toFinset.mpr
(takeWhile_dropLast_subset (f := fun y => !decide (y = p))
(List.mem_toFinset.mp hx)))
The front of p after a run is a subset of the requested pages together with
the pages that were already in front of p initially.
lemma lru_front_bound (k : ℕ) (L : List Page) (p : Page) (ρ : List Page) :
front p (lruRun k L ρ) ⊆ ρ.toFinset ∪ front p L := by
induction ρ generalizing L with
| nil => simp [lruRun]
| cons q ρ' ih =>
have hstep := lruStep_front_subset k L p q
have hih := ih (L := lruStep k L q)
intro x hx
have hx' := hih hx
rw [Finset.mem_union] at hx' ⊢
rcases hx' with hxρ | hxf
· left
rw [List.toFinset_cons, Finset.mem_insert]
exact Or.inr hxρ
· have hxf' := hstep hxf
rw [Finset.mem_union, Finset.mem_singleton] at hxf'
rcases hxf' with hxq | hxfL
· left
rw [List.toFinset_cons, Finset.mem_insert]
exact Or.inl hxq
· right; exact hxfL
When h is the last element of a list, the takeWhile (· ≠ h) prefix is
exactly the drop-last tail.
lemma takeWhile_ne_dropLast {h : Page} {L : List Page} (hL : h ∈ L)
(hlast : h ∉ L.dropLast) :
L.takeWhile (fun x => x ≠ h) = L.dropLast := by
have hnil : L ≠ [] := by intro h0; subst h0; simp at hL
have hget : h = L.getLast hnil := by
have hmem : h ∈ L.dropLast ++ [L.getLast hnil] := by
rw [List.dropLast_append_getLast hnil]
exact hL
rw [List.mem_append] at hmem
rcases hmem with hmem | hmem
· exact False.elim (hlast hmem)
· simpa using hmem
rw [← List.dropLast_append_getLast hnil]
rw [← hget]
rw [List.takeWhile_append_of_pos (p := fun x => x ≠ h)]
· simp
· intro x hx
apply decide_eq_true
intro hxeq
exact hlast (by simpa [hxeq] using hx)
When a page h is evicted in a single step, the front of h (which then has
k - 1 distinct pages) together with the evicting request has at least k pages.
lemma lruStep_evict_card (k : ℕ) (L : List Page) (h q : Page)
(hNodup : L.Nodup) (hL : h ∈ L) (hev : h ∉ lruStep k L q) :
k ≤ (front h L ∪ {q}).card := by
have hnot : ¬ (if q ∈ L then h ∈ L
else if L.length < k then h = q ∨ h ∈ L
else h = q ∨ h ∈ L.dropLast) := by
intro h'
exact hev ((mem_lruStep k L q h).mpr h')
have hqL : q ∉ L := by intro hq; apply hnot; simp [hq, hL]
have hfull : k ≤ L.length := by
by_contra hn
have hl : L.length < k := by omega
apply hnot; simp [hqL, hl, hL]
have hlast : h ∉ L.dropLast := by intro hd; apply hnot; simp [hqL, hfull, hd]
have hfront : front h L = L.dropLast.toFinset := by
unfold front
congr 1
exact takeWhile_ne_dropLast hL hlast
have hfront_card : k - 1 ≤ (front h L).card := by
rw [hfront]
rw [toFinset_card_of_nodup ((List.dropLast_sublist L).nodup hNodup)]
rw [List.dropLast_eq_take, List.length_take]
omega
have hq_front : q ∉ front h L := by
intro hq'
rw [hfront] at hq'
exact hqL (List.mem_of_mem_dropLast (by simpa using hq'))
have hcard : (front h L ∪ {q}).card = (front h L).card + 1 := by
rw [Finset.card_union_of_disjoint]
· simp
· exact Finset.disjoint_singleton_right.mpr hq_front
omega
If a page h ∈ L is not in the cache after a run, then the distinct pages of
ρ other than h, together with the pages initially in front of h, number at
least k. For h at the front this is the "head eviction" lemma: evicting the
most-recently-used page needs k other distinct requests.
lemma lru_evict_ge (k : ℕ) (L : List Page) (h : Page) (ρ : List Page)
(hNodup : L.Nodup) (hL : h ∈ L) (hev : h ∉ lruRun k L ρ) :
k ≤ (front h L ∪ ρ.toFinset.erase h).card := by
induction ρ generalizing L with
| nil => simp [lruRun] at hev; exact (hev hL).elim
| cons q ρ' ih =>
let L' := lruStep k L q
by_cases hL' : h ∈ L'
· have hih := ih (L := L') (lruStep_nodup k L q hNodup) hL' (by simpa [lruRun, L'] using hev)
have hle : (front h L' ∪ ρ'.toFinset.erase h).card ≤
(front h L ∪ (q :: ρ').toFinset.erase h).card := by
apply Finset.card_le_card
intro x hx
rw [Finset.mem_union] at hx ⊢
rcases hx with hx | hx
· have hx' := lruStep_front_subset k L h q hx
rw [Finset.mem_union, Finset.mem_singleton] at hx'
rcases hx' with hxq | hxf
· right
rw [List.toFinset_cons, Finset.mem_erase, Finset.mem_insert]
refine ⟨?_, Or.inl hxq⟩
intro hqh
subst q
subst h
have : x ∉ front x (lruStep k L x) := by simp [front, lruStep, hL]
exact this hx
· left; exact hxf
· right
rw [List.toFinset_cons, Finset.mem_erase, Finset.mem_insert]
exact ⟨(Finset.mem_erase.mp hx).1, Or.inr (Finset.mem_erase.mp hx).2⟩
omega
· have hcard := lruStep_evict_card k L h q hNodup hL hL'
have hqh : q ≠ h := by
intro hqh
subst q
exact hL' (by change h ∈ lruStep k L h; simp [lruStep, hL])
have hle : (front h L ∪ {q}).card ≤ (front h L ∪ (q :: ρ').toFinset.erase h).card := by
apply Finset.card_le_card
intro x hx
rw [Finset.mem_union] at hx ⊢
rcases hx with hxf | hxq
· left; exact hxf
· right
rw [Finset.mem_singleton] at hxq
rw [List.toFinset_cons, Finset.mem_erase, Finset.mem_insert]
refine ⟨by simpa [hxq] using hqh, Or.inl hxq⟩
omega
LRU head eviction. If h is the most-recently-used page of a full cache at
the start of a segment, and h is evicted during the segment, then the segment
requests at least k other distinct pages. This is the key fact that a single
page can only be faulted once within any segment of at most k distinct pages.
lemma lru_head_evict (k : ℕ) (h : Page) (rest ρ : List Page) (hNodup : (h :: rest).Nodup) :
h ∉ lruRun k (h :: rest) ρ → k ≤ (ρ.toFinset.erase h).card := by
intro hev
have hge := lru_evict_ge k (h :: rest) h ρ hNodup (by simp) hev
simpa [front] using hge
Erasing p before or after dropping the last element commutes, when p is
not the last element.
lemma erase_dropLast {p : Page} {L : List Page} (hp : p ∈ L.dropLast) :
(L.dropLast).erase p = (L.erase p).dropLast := by
have hnil : L ≠ [] := by intro h0; subst h0; simp at hp
calc
(L.dropLast).erase p = ((L.dropLast).erase p ++ [L.getLast hnil]).dropLast := by
rw [List.dropLast_append_of_ne_nil (show [L.getLast hnil] ≠ [] by simp)]
simp
_ = ((L.dropLast ++ [L.getLast hnil]).erase p).dropLast := by
rw [List.erase_append_left [L.getLast hnil] hp]
_ = (L.erase p).dropLast := by
rw [List.dropLast_append_getLast hnil]
When p survives a step q ≠ p, erasing p from the resulting cache equals
running the step on the size-(k - 1) cache with p already erased.
lemma lruStep_erase (k : ℕ) (L : List Page) (q p : Page)
(hNodup : L.Nodup) (hp : p ∈ L) (hqp : q ≠ p) (hkeep : p ∈ lruStep k L q) :
(lruStep k L q).erase p = lruStep (k - 1) (L.erase p) q := by
unfold lruStep
by_cases hq : q ∈ L
· have hq_erase : q ∈ L.erase p := (List.mem_erase_of_ne hqp).mpr hq
simp [hq, hqp, hq_erase]
rw [List.erase_comm q p]
· by_cases hl : L.length < k
· have hq_erase : q ∉ L.erase p := by
intro h; exact hq (List.mem_of_mem_erase h)
have hlen : (L.erase p).length < k - 1 := by
rw [List.length_erase_of_mem hp]
have hpos : 0 < L.length := List.length_pos_of_mem hp
omega
simp [hq, hl, hqp, hq_erase, hlen]
· have hq_erase : q ∉ L.erase p := by
intro h; exact hq (List.mem_of_mem_erase h)
have hlen : ¬ (L.erase p).length < k - 1 := by
rw [List.length_erase_of_mem hp]
omega
have hp_dropLast : p ∈ L.dropLast := by
have : p ∈ q :: L.dropLast := by simpa [lruStep, hq, hl] using hkeep
rw [List.mem_cons] at this
rcases this with this | this
· exact (hqp this.symm).elim
· exact this
simp [hq, hl, hqp, hq_erase, hlen]
exact erase_dropLast hp_dropLast
The miss count commutes with erasing a page that is never evicted: with the
page p pinned in the cache, LRU over a size-k cache from L has the same
misses as a size-(k - 1) cache from L.erase p over ρ.filter (· ≠ p).
lemma lru_miss_shrink (k : ℕ) (L : List Page) (ρ : List Page) (p : Page)
(hNodup : L.Nodup) (hp : p ∈ L) (hkeep : ∀ τ, List.IsPrefix τ ρ → p ∈ lruRun k L τ) :
lruMissesGo k L ρ = lruMissesGo (k - 1) (L.erase p) (ρ.filter (fun x => x ≠ p)) := by
induction ρ generalizing L with
| nil => simp [lruMissesGo]
| cons q rest ih =>
have hq : p ∈ lruStep k L q := by
have h := hkeep [q] (⟨rest, by simp⟩ : List.IsPrefix [q] (q :: rest))
simpa [lruRun] using h
have hkeep' : ∀ τ, List.IsPrefix τ rest → p ∈ lruRun k (lruStep k L q) τ := by
intro τ hτ
rcases hτ with ⟨t, ht⟩
have hprefix : List.IsPrefix ([q] ++ τ) (q :: rest) := ⟨t, by simp [ht]⟩
have h := hkeep ([q] ++ τ) hprefix
rwa [lruRun_append] at h
have hih := ih (L := lruStep k L q) (lruStep_nodup k L q hNodup) hq hkeep'
by_cases hqp : q = p
· subst q
rw [lruMissesGo]
simp [hp]
have hstep : (lruStep k L p).erase p = L.erase p := by
unfold lruStep
simp [hp]
rw [hih, hstep]
simp
· have hq_in : q ∈ L ↔ q ∈ L.erase p := (List.mem_erase_of_ne hqp).symm
have hstep_erase := lruStep_erase k L q p hNodup hp hqp hq
rw [lruMissesGo]
rw [hih]
rw [hstep_erase]
have hfilter : (q :: rest).filter (fun x => x ≠ p) = q :: rest.filter (fun x => x ≠ p) := by
simp [hqp]
rw [hfilter]
rw [lruMissesGo]
simp [hq_in]
LRU makes at most one fault per distinct requested page, whenever at most
k distinct pages are requested.
lemma lru_miss_le_distinct (k : ℕ) (L : List Page) (ρ : List Page)
(hNodup : L.Nodup) (hcard : ρ.toFinset.card ≤ k) :
lruMissesGo k L ρ ≤ ρ.toFinset.card := by
classical
have go : ∀ (n : ℕ) (k : ℕ) (L : List Page) (ρ : List Page), ρ.length = n → L.Nodup →
ρ.toFinset.card ≤ k → lruMissesGo k L ρ ≤ ρ.toFinset.card := by
intro n
induction n using Nat.strong_induction_on with
| h n ih =>
intro k L ρ hlen hNodup hcard
cases ρ with
| nil => simp [lruMissesGo]
| cons p ρ' =>
let L' : List Page := lruStep k L p
have hNodup' : L'.Nodup := lruStep_nodup k L p hNodup
have hlt : ρ'.length < n := by
rw [← hlen]
exact Nat.lt_succ_self ρ'.length
rw [lruMissesGo, List.toFinset_cons]
by_cases hpL : p ∈ L
· have hrec : lruMissesGo k L' ρ' ≤ ρ'.toFinset.card := by
apply ih ρ'.length hlt k L' ρ' rfl hNodup'
have : ρ'.toFinset.card ≤ k := by
have hins : (insert p ρ'.toFinset).card ≤ k := by
simpa [List.toFinset_cons] using hcard
have hle : ρ'.toFinset.card ≤ (insert p ρ'.toFinset).card :=
Finset.card_le_card (Finset.subset_insert _ _)
omega
exact this
simp [hpL]
exact le_trans hrec (Finset.card_le_card (Finset.subset_insert _ _))
· by_cases hpρ : p ∈ ρ'.toFinset
· -- p is re-requested: it is pinned, so shrink to a size-(k-1) cache
have hk : 0 < k := by
have hpos : 0 < ρ'.toFinset.card := Finset.card_pos.mpr ⟨p, hpρ⟩
have hle : ρ'.toFinset.card ≤ k := by
have hins : (insert p ρ'.toFinset).card ≤ k := by
simpa [List.toFinset_cons] using hcard
have hle2 : ρ'.toFinset.card ≤ (insert p ρ'.toFinset).card :=
Finset.card_le_card (Finset.subset_insert _ _)
omega
omega
have hcard' : (ρ'.toFinset.erase p).card ≤ k - 1 := by
have hρ : ρ'.toFinset.card ≤ k := by
have hins : (insert p ρ'.toFinset).card ≤ k := by
simpa [List.toFinset_cons] using hcard
have : (insert p ρ'.toFinset).card = ρ'.toFinset.card :=
Finset.card_insert_of_mem hpρ
omega
rw [Finset.card_erase_of_mem hpρ]
have hpos : 0 < ρ'.toFinset.card := Finset.card_pos.mpr ⟨p, hpρ⟩
omega
have hL'head : ∃ t, L' = p :: t := by
change ∃ t, lruStep k L p = p :: t
unfold lruStep
by_cases hl : L.length < k
· exact ⟨L, by simp [hpL, hl]⟩
· exact ⟨L.dropLast, by simp [hpL, hl]⟩
rcases hL'head with ⟨t, hEq⟩
have hNodup_t : (p :: t).Nodup := by simpa [hEq] using hNodup'
have hkeep : ∀ τ, List.IsPrefix τ ρ' → p ∈ lruRun k (p :: t) τ := by
intro τ hτ
have hcardτ : (τ.toFinset.erase p).card ≤ k - 1 := by
have hsub : τ.toFinset.erase p ⊆ ρ'.toFinset.erase p := by
intro x hx
rw [Finset.mem_erase] at hx ⊢
refine ⟨hx.1, ?_⟩
rcases hτ with ⟨t', ht'⟩
have hsub' : τ.toFinset ⊆ ρ'.toFinset := by
rw [← ht', List.toFinset_append]
exact Finset.subset_union_left
exact hsub' hx.2
exact le_trans (Finset.card_le_card hsub) hcard'
by_contra hnot
have hge := lru_head_evict k p t τ hNodup_t hnot
omega
have hshrink := lru_miss_shrink k (p :: t) ρ' p hNodup_t (by simp) hkeep
have hrec : lruMissesGo (k - 1) t (ρ'.filter (fun x => x ≠ p)) ≤
(ρ'.filter (fun x => x ≠ p)).toFinset.card := by
have hlenF : (ρ'.filter (fun x => x ≠ p)).length < n := by
have hle : (ρ'.filter (fun x => x ≠ p)).length ≤ ρ'.length :=
List.length_filter_le _ _
omega
apply ih (ρ'.filter (fun x => x ≠ p)).length hlenF (k - 1) t
(ρ'.filter (fun x => x ≠ p)) rfl
· exact (List.sublist_cons_self p t).nodup hNodup_t
· have hto : (ρ'.filter (fun x => x ≠ p)).toFinset = ρ'.toFinset.erase p := by
ext x
by_cases hxp : x = p <;> simp [List.mem_filter, List.mem_toFinset, hxp]
rw [hto]
exact hcard'
have hpos : 0 < ρ'.toFinset.card := Finset.card_pos.mpr ⟨p, hpρ⟩
simp [hpL]
calc
1 + lruMissesGo k L' ρ' =
1 + lruMissesGo (k - 1) t (ρ'.filter (fun x => x ≠ p)) := by
simpa [hEq, hshrink]
_ ≤ 1 + (ρ'.toFinset.erase p).card := by
have hto : (ρ'.filter (fun x => x ≠ p)).toFinset = ρ'.toFinset.erase p := by
ext x
by_cases hxp : x = p <;> simp [List.mem_filter, List.mem_toFinset, hxp]
rw [hto] at hrec
exact Nat.add_le_add_left hrec 1
_ = ρ'.toFinset.card := by
rw [Finset.card_erase_of_mem hpρ]
omega
_ ≤ (insert p ρ'.toFinset).card := by
rw [Finset.card_insert_of_mem hpρ]
· have hrec : lruMissesGo k L' ρ' ≤ ρ'.toFinset.card := by
apply ih ρ'.length hlt k L' ρ' rfl hNodup'
have : ρ'.toFinset.card ≤ k := by
have hins : (insert p ρ'.toFinset).card ≤ k := by
simpa [List.toFinset_cons] using hcard
have hle : ρ'.toFinset.card ≤ (insert p ρ'.toFinset).card :=
Finset.card_le_card (Finset.subset_insert _ _)
omega
exact this
simp [hpL]
have hins : (insert p ρ'.toFinset).card = ρ'.toFinset.card + 1 := by
rw [Finset.card_insert_of_notMem hpρ]
rw [hins]
rw [Nat.add_comm]
exact Nat.add_le_add_right hrec 1
exact go ρ.length k L ρ rfl hNodup hcard
Auxiliary: with seen the pages already accumulated in the current phase,
extend it by the longest prefix of σ whose distinct pages (together with
seen) stay within k. Returns the extension (forward order) and the leftover
suffix.
-- ---------------------------------------------------------------------------
-- The phase partition.
def phaseGo (k : ℕ) (seen : Finset Page) : List Page → List Page × List Page
| [] => ([], [])
| q :: τ =>
let seen' := insert q seen
if seen'.card ≤ k then
let (pre, rem) := phaseGo k seen' τ
(q :: pre, rem)
else
([], q :: τ)
termination_by σ => σ.length
phaseGo splits its input: the phase prefix and the suffix concatenate back
to the original list.
lemma phaseGo_split (k : ℕ) (seen : Finset Page) (σ : List Page) :
(phaseGo k seen σ).1 ++ (phaseGo k seen σ).2 = σ := by
induction σ generalizing seen with
| nil => simp [phaseGo]
| cons q τ ih =>
by_cases h : (insert q seen).card ≤ k
· have hi := ih (insert q seen)
simp [phaseGo, h, hi]
· simp [phaseGo, h]
The first phase of a nonempty request list: the longest prefix with at most
k distinct pages, paired with the remaining suffix.
def firstPhase (k : ℕ) : List Page → List Page × List Page
| [] => ([], [])
| p :: rest =>
let (pre, rem) := phaseGo k {p} rest
(p :: pre, rem)The first phase of a nonempty list is nonempty.
lemma firstPhase_nonempty (k : ℕ) (σ : List Page) (h : σ ≠ []) :
(firstPhase k σ).1 ≠ [] := by
cases σ with
| nil => contradiction
| cons p rest => simp [firstPhase]The first phase and its suffix split the list.
lemma firstPhase_split (k : ℕ) (σ : List Page) :
(firstPhase k σ).1 ++ (firstPhase k σ).2 = σ := by
cases σ with
| nil => simp [firstPhase]
| cons p rest =>
have hs := phaseGo_split k {p} rest
simp [firstPhase, hs]
The phase partition of σ: maximal segments, each (except possibly the
last) containing exactly k distinct pages.
def phases (k : ℕ) (σ : List Page) : List (List Page) :=
WellFounded.fix (hwf := (measure (fun σ : List Page => σ.length)).wf) (fun σ rec =>
match σ with
| [] => []
| p :: rest =>
let fp := firstPhase k (p :: rest)
let pre := fp.1
let rem := fp.2
pre :: rec rem (by
have hsplit := firstPhase_split k (p :: rest)
have hnonempty := firstPhase_nonempty k (p :: rest) (by simp)
have hlen : pre.length + rem.length = (p :: rest).length := by
rw [← hsplit, List.length_append]
have hpos : 0 < pre.length := List.length_pos_of_ne_nil hnonempty
change rem.length < (p :: rest).length
rw [← hlen]
exact Nat.lt_add_of_pos_left hpos)
) σ
The pages of a phaseGo prefix, together with the already-seen pages, stay
within the cache size k.
lemma phaseGo_distinct_le (k : ℕ) (seen : Finset Page) (σ : List Page) (hseen : seen.card ≤ k) :
((phaseGo k seen σ).1.toFinset ∪ seen).card ≤ k := by
induction σ generalizing seen with
| nil => simpa [phaseGo] using hseen
| cons q τ ih =>
by_cases h : (insert q seen).card ≤ k
· have hi := ih (insert q seen) (by simpa using h)
simp [phaseGo, h]
rw [← Finset.union_insert]
exact hi
· simp [phaseGo, h, hseen]
A phase has at most k distinct pages (requires 0 < k).
lemma firstPhase_distinct_le (k : ℕ) (σ : List Page) (hk : 0 < k) :
(firstPhase k σ).1.toFinset.card ≤ k := by
cases σ with
| nil => simp [firstPhase]
| cons p rest =>
have hd := phaseGo_distinct_le k {p} rest (by simpa using (Nat.succ_le_of_lt hk))
simp [firstPhase]
rw [Finset.insert_eq, Finset.union_comm]
exact hdThe phase partition recurses: for a nonempty list, the phases are the first phase followed by the phases of the suffix.
lemma phases_cons_eq (k : ℕ) (σ : List Page) (h : σ ≠ []) :
phases k σ = (firstPhase k σ).1 :: phases k (firstPhase k σ).2 := by
cases σ with
| nil => contradiction
| cons p rest => simp [phases]; rw [WellFounded.fix_eq]The phase partition concatenates back to the original list.
lemma phases_join (k : ℕ) (σ : List Page) : (phases k σ).flatten = σ := by
classical
have hmain : ∀ (n : ℕ) (σ : List Page), σ.length = n → (phases k σ).flatten = σ := by
intro n
induction n using Nat.strong_induction_on with
| h n ih =>
intro σ hn
cases σ with
| nil => simp [phases, WellFounded.fix_eq]
| cons p rest =>
let fp := firstPhase k (p :: rest)
have hsplit := firstPhase_split k (p :: rest)
have hnonempty : fp.1 ≠ [] := by
dsimp [fp]
exact firstPhase_nonempty k (p :: rest) (by simp)
have hremlen : fp.2.length < (p :: rest).length := by
have hlen : fp.1.length + fp.2.length = (p :: rest).length := by
rw [← hsplit, List.length_append]
have hpos : 0 < fp.1.length := List.length_pos_of_ne_nil hnonempty
omega
have hremlen' : fp.2.length < n := by omega
have hi := ih fp.2.length hremlen' fp.2 rfl
rw [phases_cons_eq k (p :: rest) (by simp)]
simp [fp, List.flatten, hi, hsplit]
exact hmain σ.length σ rfl
LRU makes at most k misses per phase: its total misses are bounded by k
times the number of phases.
lemma lru_miss_le_phases (k : ℕ) (L : List Page) (σ : List Page) (hk : 0 < k) (hNodup : L.Nodup) :
lruMissesGo k L σ ≤ k * (phases k σ).length := by
classical
have go : ∀ (n : ℕ) (L : List Page) (σ : List Page), σ.length = n → L.Nodup →
lruMissesGo k L σ ≤ k * (phases k σ).length := by
intro n
induction n using Nat.strong_induction_on with
| h n ih =>
intro L σ hlen hNodup
cases σ with
| nil => simp [lruMissesGo, phases]
| cons p rest =>
let fp := firstPhase k (p :: rest)
have hsplit := firstPhase_split k (p :: rest)
have hcard : (firstPhase k (p :: rest)).1.toFinset.card ≤ k :=
firstPhase_distinct_le k (p :: rest) hk
have hnonempty : fp.1 ≠ [] := by
dsimp [fp]
exact firstPhase_nonempty k (p :: rest) (by simp)
have hremlen : fp.2.length < (p :: rest).length := by
have hlen : fp.1.length + fp.2.length = (p :: rest).length := by
rw [← hsplit, List.length_append]
have hpos : 0 < fp.1.length := List.length_pos_of_ne_nil hnonempty
omega
have hremlen' : fp.2.length < n := by omega
have hphase : lruMissesGo k L fp.1 ≤ fp.1.toFinset.card :=
lru_miss_le_distinct k L fp.1 hNodup (by simpa [fp] using hcard)
have hrec : lruMissesGo k (lruRun k L fp.1) fp.2 ≤ k * (phases k fp.2).length :=
ih fp.2.length hremlen' (lruRun k L fp.1) fp.2 rfl (lruRun_nodup k L fp.1 hNodup)
rw [phases_cons_eq k (p :: rest) (by simp)]
conv_lhs => rw [← hsplit, lruMissesGo_append]
calc
lruMissesGo k L fp.1 + lruMissesGo k (lruRun k L fp.1) fp.2
≤ fp.1.toFinset.card + k * (phases k fp.2).length := Nat.add_le_add hphase hrec
_ ≤ k + k * (phases k fp.2).length := by
exact Nat.add_le_add_right (by simpa [fp] using hcard) _
_ = k * (fp.1 :: phases k fp.2).length := by
simp [List.length_cons, Nat.mul_add, Nat.add_comm]
exact go σ.length L σ rfl hNodupThe miss count over a concatenation splits additively for an algorithm.
lemma missesGo_append (A : Algorithm Page k) (C : Finset Page) (σ τ : List Page) :
missesGo A C (σ ++ τ) = missesGo A C σ + missesGo A (runGo A C σ) τ := by
induction σ generalizing C with
| nil => simp [missesGo, runGo]
| cons p σ' ih =>
simp [missesGo, runGo, ih]
omegaA run with no misses leaves the cache unchanged.
lemma runGo_eq_self_of_misses_eq_zero (A : Algorithm Page k) (C : Finset Page) (ρ : List Page)
(h : missesGo A C ρ = 0) (hC : C.card ≤ k) : runGo A C ρ = C := by
induction ρ generalizing C with
| nil => simp [runGo]
| cons p τ ih =>
have hp : p ∈ C := by
have h' : (if p ∈ C then 0 else 1) + missesGo A (A.step C p) τ = 0 := by
simpa [missesGo] using h
by_contra hnot
simp [hnot] at h'
have hτ : missesGo A (A.step C p) τ = 0 := by
have h' : (if p ∈ C then 0 else 1) + missesGo A (A.step C p) τ = 0 := by
simpa [missesGo] using h
simpa [hp] using h'
rw [runGo]
simpa [A.step_hit C p hC hp] using ih (A.step C p) hτ (A.step_size C p hC)
When a phaseGo prefix ends because the next request would exceed k, the
accumulated pages have cardinality exactly k and the next request is fresh.
lemma phaseGo_maximal (k : ℕ) (seen : Finset Page) (σ : List Page) (hseen : seen.card ≤ k)
(h : (phaseGo k seen σ).2 ≠ []) :
((phaseGo k seen σ).1.toFinset ∪ seen).card = k ∧
List.head (phaseGo k seen σ).2 h ∉ (phaseGo k seen σ).1.toFinset ∪ seen := by
induction σ generalizing seen with
| nil => simp [phaseGo] at h
| cons q τ ih =>
by_cases hq : (insert q seen).card ≤ k
· have hrem : (phaseGo k (insert q seen) τ).2 ≠ [] := by
simpa [phaseGo, hq] using h
have hi := ih (insert q seen) (by simpa using hq) hrem
simpa [phaseGo, hq, Finset.insert_union, Finset.union_insert, Finset.union_assoc,
Finset.union_comm, Finset.union_left_comm] using hi
· have hcard : seen.card = k := by
have hqnotin : q ∉ seen := by
intro hqin
have : (insert q seen).card ≤ k := by simpa [hqin] using hseen
exact hq this
have hinsert : (insert q seen).card = seen.card + 1 :=
Finset.card_insert_of_notMem hqnotin
have hgt : k < (insert q seen).card := by omega
rw [hinsert] at hgt
omega
constructor
· simp [phaseGo, hq, hcard]
· simp [phaseGo, hq]
intro hqin
have : (insert q seen).card ≤ k := by simpa [hqin] using hseen
exact hq this
A nonempty first phase is maximal: if its suffix is nonempty, the phase has
exactly k distinct pages and the first page of the suffix is fresh.
lemma firstPhase_maximal (k : ℕ) (σ : List Page) (hk : 0 < k) (h : (firstPhase k σ).2 ≠ []) :
(firstPhase k σ).1.toFinset.card = k ∧
List.head (firstPhase k σ).2 h ∉ (firstPhase k σ).1.toFinset := by
cases σ with
| nil => simp [firstPhase] at h
| cons p rest =>
have hseen : ({p} : Finset Page).card ≤ k := by
simpa using (Nat.succ_le_of_lt hk)
have hrem : (phaseGo k {p} rest).2 ≠ [] := by
simpa [firstPhase] using h
have hmax := phaseGo_maximal k {p} rest hseen hrem
constructor
· simp [firstPhase]
rw [Finset.insert_eq, Finset.union_comm]
exact hmax.1
· simp [firstPhase]
simpa [Finset.mem_union, Finset.mem_singleton, and_comm] using hmax.2
If phaseGo stops at q (its leftover suffix begins with q), then q is
fresh with respect to the accumulated seen and the returned prefix.
lemma phaseGo_fresh (k : ℕ) (seen : Finset Page) (σ : List Page) (hseen : seen.card ≤ k)
(q : Page) (rest' : List Page) (h : (phaseGo k seen σ).2 = q :: rest') :
q ∉ (phaseGo k seen σ).1.toFinset ∪ seen := by
induction σ generalizing seen with
| nil => simp [phaseGo] at h
| cons x τ ih =>
by_cases hx : (insert x seen).card ≤ k
· have h' : (phaseGo k (insert x seen) τ).2 = q :: rest' := by
simpa [phaseGo, hx] using h
have hi := ih (insert x seen) (by simpa using hx) h'
simpa [phaseGo, hx, Finset.insert_union, Finset.union_insert, Finset.union_assoc,
Finset.union_comm, Finset.union_left_comm] using hi
· have hxτ : x :: τ = q :: rest' := by simpa [phaseGo, hx] using h
have hxq : x = q := (List.cons.inj hxτ).1
subst x
simp [phaseGo, hx]
intro hqin
have hcard : (insert q seen).card ≤ k := by simpa [hqin] using hseen
exact hx hcardThe first page of the suffix after a maximal first phase is fresh with respect to the pages of the phase.
lemma firstPhase_fresh (k : ℕ) (σ : List Page) (hk : 0 < k) (q : Page) (rest' : List Page)
(h : (firstPhase k σ).2 = q :: rest') :
q ∉ (firstPhase k σ).1.toFinset := by
cases σ with
| nil => simp [firstPhase] at h
| cons p rest =>
have h' : (phaseGo k ({p} : Finset Page) rest).2 = q :: rest' := by
simpa [firstPhase] using h
have hseen : ({p} : Finset Page).card ≤ k := by
simpa using (Nat.succ_le_of_lt hk)
have hfresh := phaseGo_fresh k {p} rest hseen q rest' h'
simp [firstPhase]
simpa [Finset.mem_union, Finset.mem_singleton, and_comm] using hfresh
The number of phases is at most one more than the miss count of any size-k
algorithm: (phases k σ).length ≤ missesGo A C σ + 1. This is the lower-bound
half of the competitive analysis: the transition between consecutive phases
requests k + 1 distinct pages (the k pages of one phase plus the first fresh
page of the next), forcing at least one miss (CLRS §27.3).
lemma phases_le_misses (k : ℕ) (σ : List Page) (hk : 0 < k)
(A : Algorithm Page k) (C : Finset Page) (hC : C.card ≤ k) :
(phases k σ).length ≤ missesGo A C σ + 1 := by
classical
have go : ∀ (n : ℕ) (σ : List Page), σ.length = n → ∀ (A : Algorithm Page k)
(C : Finset Page), C.card ≤ k → (phases k σ).length ≤ missesGo A C σ + 1 := by
intro n
induction n using Nat.strong_induction_on with
| h n ih =>
intro σ hlen A C hC
cases σ with
| nil => simp [phases, missesGo, WellFounded.fix_eq]
| cons p rest =>
let fp := firstPhase k (p :: rest)
have hsplit := firstPhase_split k (p :: rest)
have hnonempty : fp.1 ≠ [] := by
dsimp [fp]
exact firstPhase_nonempty k (p :: rest) (by simp)
cases hrem : fp.2 with
| nil =>
rw [phases_cons_eq k (p :: rest) (by simp)]
rw [hrem]
simp [phases, WellFounded.fix_eq]
| cons q rest' =>
have hrem_ne : fp.2 ≠ [] := by simp [hrem]
have hmax := firstPhase_maximal k (p :: rest) hk hrem_ne
have hcard : fp.1.toFinset.card = k := by simpa [fp] using hmax.1
have hqfresh : q ∉ fp.1.toFinset := by
exact firstPhase_fresh k (p :: rest) hk q rest' (by simpa [fp] using hrem)
have hremlen : fp.2.length < (p :: rest).length := by
have hlen : fp.1.length + fp.2.length = (p :: rest).length := by
rw [← hsplit, List.length_append]
have hpos : 0 < fp.1.length := List.length_pos_of_ne_nil hnonempty
omega
have hremlen' : fp.2.length < n := by omega
-- The transition segment fp.1 ++ [q] requests k + 1 distinct pages.
have htrans : k < (fp.1 ++ [q]).toFinset.card := by
have hto : (fp.1 ++ [q]).toFinset = insert q fp.1.toFinset := by
ext x
simp [List.toFinset_append]
rw [hto, Finset.card_insert_of_notMem hqfresh, hcard]
omega
have hforce : 1 ≤ missesGo A C (fp.1 ++ [q]) :=
distinct_fault A C (fp.1 ++ [q]) hC htrans
have hforce' : 1 ≤ missesGo A C fp.1 + (if q ∈ runGo A C fp.1 then 0 else 1) := by
have h := missesGo_append A C fp.1 [q]
rw [h] at hforce
simp [missesGo] at hforce ⊢
exact hforce
have hih := ih fp.2.length hremlen' fp.2 rfl A (A.step (runGo A C fp.1) q)
(A.step_size (runGo A C fp.1) q (runGo_size A C fp.1 hC))
have hqstep : q ∈ A.step (runGo A C fp.1) q := A.step_loads (runGo A C fp.1) q
have hhit : A.step (A.step (runGo A C fp.1) q) q = A.step (runGo A C fp.1) q :=
A.step_hit (A.step (runGo A C fp.1) q) q
(A.step_size (runGo A C fp.1) q (runGo_size A C fp.1 hC)) hqstep
have hih' : (phases k fp.2).length ≤
missesGo A (A.step (runGo A C fp.1) q) rest' + 1 := by
have hmiss : missesGo A (A.step (runGo A C fp.1) q) fp.2 =
missesGo A (A.step (runGo A C fp.1) q) rest' := by
rw [hrem]
simp [missesGo, hqstep, hhit]
rw [hmiss] at hih
exact hih
have hmisses : missesGo A C (p :: rest) =
missesGo A C fp.1 + (if q ∈ runGo A C fp.1 then 0 else 1) +
missesGo A (A.step (runGo A C fp.1) q) rest' := by
rw [← hsplit, missesGo_append A C fp.1 fp.2, hrem]
simp only [missesGo]
ac_rfl
rw [phases_cons_eq k (p :: rest) (by simp)]
change (phases k fp.2).length + 1 ≤ missesGo A C (p :: rest) + 1
rw [hmisses]
omega
exact go σ.length σ rfl A C hC
Theorem 27.3 (LRU is k-competitive). For any deterministic paging
algorithm A with cache size k, LRU's miss count over a request sequence σ
is at most k times A's miss count plus k, both starting from an empty
cache. This is the classical competitive-analysis bound of CLRS §27.3.
theorem lru_k_competitive (k : ℕ) (σ : List Page) (hk : 0 < k) (A : Algorithm Page k) :
lruMissesGo k [] σ ≤ k * missesGo A ∅ σ + k := by
have hub := lru_miss_le_phases k [] σ hk (by simp)
have hlb := phases_le_misses k σ hk A ∅ (by simp)
have hmul : k * (phases k σ).length ≤ k * (missesGo A ∅ σ + 1) :=
Nat.mul_le_mul_left k hlb
calc
lruMissesGo k [] σ ≤ k * (phases k σ).length := hub
_ ≤ k * (missesGo A ∅ σ + 1) := hmul
_ = k * missesGo A ∅ σ + k := by rw [Nat.mul_add, Nat.mul_one]Deterministic lower bound
The matching lower bound of CLRS §27.3: no deterministic online paging
algorithm is better than k-competitive. The adversary works over the
k + 1-page universe Fin (k + 1) and always requests a page absent from the
algorithm's cache, so the algorithm faults on every request. An offline
schedule that sees the whole sequence serves the same requests with at most
N / k + k + 1 faults, which is the comparison cost of the lower bound.
A cache of at most k pages omits some page of the k + 1-page universe.
lemma exists_page_not_mem (C : Finset (Fin (k + 1))) (hC : C.card ≤ k) :
∃ p : Fin (k + 1), p ∉ C := by
classical
have huniv : (Finset.univ : Finset (Fin (k + 1))).card = k + 1 := by
rw [Finset.card_univ, Fintype.card_fin]
have hsub : ¬ (Finset.univ : Finset (Fin (k + 1))) ⊆ C := by
intro h
have hle : (Finset.univ : Finset (Fin (k + 1))).card ≤ C.card := Finset.card_le_card h
omega
rw [← Finset.sdiff_nonempty] at hsub
rcases hsub with ⟨p, hp⟩
exact ⟨p, (Finset.mem_sdiff.mp hp).2⟩
The adversary's fresh page: a page outside the cache C (junk when C is
already the whole universe). The lower-bound adversary always requests this
absent page, forcing a fault.
noncomputable def freshPage (C : Finset (Fin (k + 1))) : Fin (k + 1) :=
if h : ∃ p : Fin (k + 1), p ∉ C then Classical.choose h else 0
freshPage C lies outside C whenever some page does.
lemma freshPage_spec (C : Finset (Fin (k + 1))) (h : ∃ p : Fin (k + 1), p ∉ C) :
freshPage C ∉ C := by
unfold freshPage
rw [dif_pos h]
exact Classical.choose_spec h
The cache of A after n adversarial requests.
noncomputable def advCache (A : Algorithm (Fin (k + 1)) k) : ℕ → Finset (Fin (k + 1))
| 0 => ∅
| n + 1 => A.step (advCache A n) (freshPage (advCache A n))The adversary's cache always respects the size bound.
lemma advCache_card_le (A : Algorithm (Fin (k + 1)) k) (n : ℕ) :
(advCache A n).card ≤ k := by
induction n with
| zero => simp [advCache]
| succ n ih =>
simp [advCache]
exact A.step_size (advCache A n) (freshPage (advCache A n)) ih
The adversarial request sequence of length n: always request the page
absent from the algorithm's current cache.
noncomputable def advSeq (A : Algorithm (Fin (k + 1)) k) (n : ℕ) : List (Fin (k + 1)) :=
(List.range n).map (fun i => freshPage (advCache A i))
The adversarial sequence of length n + 1 extends the length-n one by the
next fresh page.
lemma advSeq_snoc (A : Algorithm (Fin (k + 1)) k) (n : ℕ) :
advSeq A (n + 1) = advSeq A n ++ [freshPage (advCache A n)] := by
unfold advSeq
rw [List.range_succ, List.map_append]
simp
The length of the adversarial sequence is n.
lemma advSeq_length (A : Algorithm (Fin (k + 1)) k) (n : ℕ) :
(advSeq A n).length = n := by
unfold advSeq
simp
runGo distributes over list concatenation.
lemma runGo_append (A : Algorithm Page k) (C : Finset Page) (σ τ : List Page) :
runGo A C (σ ++ τ) = runGo A (runGo A C σ) τ := by
induction σ generalizing C with
| nil => simp [runGo]
| cons p σ' ih => simp [runGo, ih]
The cache of A after running the adversarial sequence of length n is
exactly the adversary's tracked cache.
lemma runGo_advSeq (A : Algorithm (Fin (k + 1)) k) (n : ℕ) :
runGo A ∅ (advSeq A n) = advCache A n := by
induction n with
| zero => simp [advSeq, advCache, runGo]
| succ n ih =>
rw [advSeq_snoc, runGo_append]
simp [runGo, ih, advCache]The next adversarial request is always absent from the algorithm's cache.
lemma freshPage_not_mem (A : Algorithm (Fin (k + 1)) k) (n : ℕ) :
freshPage (advCache A n) ∉ advCache A n := by
exact freshPage_spec (advCache A n) (exists_page_not_mem (advCache A n) (advCache_card_le A n))
The algorithm faults on every adversarial request: over n requests it
misses exactly n times.
lemma advSeq_misses (A : Algorithm (Fin (k + 1)) k) (n : ℕ) :
misses A ∅ (advSeq A n) = n := by
induction n with
| zero => simp [advSeq, misses, missesGo]
| succ n ih =>
rw [advSeq_snoc]
change missesGo A ∅ (advSeq A n ++ [freshPage (advCache A n)]) = n + 1
rw [missesGo_append, runGo_advSeq]
simp only [missesGo]
rw [show missesGo A ∅ (advSeq A n) = n from ih]
simp [freshPage_not_mem A n]
The offline eviction step: on a fault for p from cache C, evict a page
not belonging to the current phase phase (a "stale" page) if one exists, and
otherwise just load p. The offline schedule knows the whole sequence, so it
knows the phase.
-- ---------------------------------------------------------------------------
-- The offline schedule: a phase-aware policy that realizes the comparison cost.
noncomputable def offlineStep (phase : Finset Page) (C : Finset Page) (p : Page) :
Finset Page :=
if p ∈ C then C
else if h : ∃ q ∈ C, q ∉ phase then insert p (C.erase (Classical.choose h))
else insert p CThe offline step loads the request.
lemma offlineStep_loads (phase C : Finset Page) (p : Page) :
p ∈ offlineStep phase C p := by
unfold offlineStep
split_ifs with hpC hq
· exact hpC
· simp
· simpThe offline step only ever adds the request.
lemma offlineStep_subset (phase C : Finset Page) (p : Page) :
offlineStep phase C p ⊆ insert p C := by
unfold offlineStep
split_ifs with hp hq <;> intro x hx
· exact Finset.mem_insert.mpr (Or.inr hx)
· rw [Finset.mem_insert] at hx ⊢
rcases hx with hx | hx
· exact Or.inl hx
· rw [Finset.mem_erase] at hx
exact Or.inr hx.2
· exact hx
The offline step keeps the cache within the size bound when the request is
in the phase and the phase has at most k pages.
lemma offlineStep_size (phase C : Finset Page) (p : Page) (hC : C.card ≤ k)
(hp : p ∈ phase) (hphase : phase.card ≤ k) : (offlineStep phase C p).card ≤ k := by
unfold offlineStep
split_ifs with hpC hq
· exact hC
· have hqmem : (Classical.choose hq) ∈ C := (Classical.choose_spec hq).1
have hpos : 0 < C.card := Finset.card_pos.mpr ⟨Classical.choose hq, hqmem⟩
rw [Finset.card_insert_of_notMem]
· rw [Finset.card_erase_of_mem hqmem]
omega
· intro hp'
exact hpC (Finset.mem_of_mem_erase hp')
· have hsub : C ⊆ phase := by
intro x hx
by_contra hnot
exact hq ⟨x, hx, hnot⟩
have hins : insert p C ⊆ phase := by
rw [Finset.insert_subset_iff]
exact ⟨hp, hsub⟩
exact le_trans (Finset.card_le_card hins) hphaseA hit leaves the offline cache unchanged.
lemma offlineStep_hit (phase C : Finset Page) (p : Page) (hp : p ∈ C) :
offlineStep phase C p = C := by
unfold offlineStep
simp [hp]The offline step preserves an exactly-full cache.
lemma offlineStep_card_eq (phase C : Finset Page) (p : Page) (hC : C.card = k)
(hp : p ∈ phase) (hphase : phase.card ≤ k) : (offlineStep phase C p).card = k := by
unfold offlineStep
split_ifs with hpC hq
· exact hC
· have hqmem : (Classical.choose hq) ∈ C := (Classical.choose_spec hq).1
have hpos : 0 < C.card := Finset.card_pos.mpr ⟨Classical.choose hq, hqmem⟩
rw [Finset.card_insert_of_notMem]
· rw [Finset.card_erase_of_mem hqmem]
omega
· intro hp'
exact hpC (Finset.mem_of_mem_erase hp')
· exfalso
have hsub : C ⊆ phase := by
intro x hx
by_contra hnot
exact hq ⟨x, hx, hnot⟩
have hle : C.card ≤ phase.card := Finset.card_le_card hsub
have hk_le : k ≤ phase.card := by omega
have hfull : C = phase := Finset.eq_of_subset_of_card_le hsub (by omega)
exact hpC (by rw [hfull]; exact hp)
Serve one phase ρ (all requests lie in the phase phase) from cache C,
evicting stale pages.
noncomputable def servePhase (phase : Finset Page) (C : Finset Page) : List Page → Finset Page
| [] => C
| p :: ρ => servePhase phase (offlineStep phase C p) ρ
The number of misses when serving one phase ρ from cache C.
noncomputable def servePhaseMisses (phase : Finset Page) (C : Finset Page) : List Page → ℕ
| [] => 0
| p :: ρ => (if p ∈ C then 0 else 1) + servePhaseMisses phase (offlineStep phase C p) ρ
The offline cache after serving a list of phases from cache C.
noncomputable def offCache (C : Finset Page) : List (List Page) → Finset Page
| [] => C
| ρ :: ps => offCache (servePhase ρ.toFinset C ρ) ps
The offline miss count over a list of phases from cache C.
noncomputable def offMisses (C : Finset Page) : List (List Page) → ℕ
| [] => 0
| ρ :: ps => servePhaseMisses ρ.toFinset C ρ + offMisses (servePhase ρ.toFinset C ρ) ps
A single offline step removes p from the phase pages S (and nothing
else): S \ offlineStep phase C p = (S \ C) \ {p}.
lemma offlineStep_sdiff (phase C : Finset Page) (p : Page) (S : Finset Page) (hS : S ⊆ phase) :
S \ offlineStep phase C p = (S \ C) \ {p} := by
unfold offlineStep
split_ifs with hpC hq
· ext x
simp only [Finset.mem_sdiff, Finset.mem_erase, Finset.mem_singleton]
have hnp : x ∉ C → x ≠ p := by intro hx hxp; subst x; exact hx hpC
tauto
· have hqmem : (Classical.choose hq) ∈ C ∧ (Classical.choose hq) ∉ phase := Classical.choose_spec hq
have hqS : Classical.choose hq ∉ S := by intro h; exact hqmem.2 (hS h)
ext x
simp only [Finset.mem_sdiff, Finset.mem_insert, Finset.mem_erase, Finset.mem_singleton]
have hxq : x ∈ S → x ≠ Classical.choose hq := by intro hx hxq; exact hqS (hxq ▸ hx)
tauto
· ext x
simp only [Finset.mem_sdiff, Finset.mem_insert, Finset.mem_erase, Finset.mem_singleton]
tautoThe key one-step cardinal inequality driving the phase miss bound.
lemma card_step_ineq (S C : Finset Page) (p : Page) :
(if p ∈ C then 0 else 1) + ((S \ C) \ {p}).card ≤ (insert p S \ C).card := by
by_cases hpC : p ∈ C
· simp [hpC]
rw [Finset.insert_sdiff_of_mem S hpC]
exact Finset.card_le_card (by intro x hx; exact (Finset.mem_sdiff.mp hx).1)
· simp [hpC]
by_cases hpS : p ∈ S
· have hmem : p ∈ S \ C := Finset.mem_sdiff.mpr ⟨hpS, hpC⟩
have hcard : ((S \ C) \ {p}).card + 1 = (S \ C).card := by
rw [show (S \ C) \ {p} = (S \ C).erase p by ext x; simp only [Finset.mem_sdiff, Finset.mem_erase, Finset.mem_singleton]; tauto]
exact Finset.card_erase_add_one hmem
have hinsert : insert p S \ C = S \ C := by simp [hpS]
rw [hinsert]
omega
· have hnot : p ∉ S \ C := by intro h; exact hpS (Finset.mem_sdiff.mp h).1
have hins : insert p S \ C = insert p (S \ C) := by
ext x
by_cases hxp : x = p <;> simp [Finset.mem_sdiff, Finset.mem_insert, hxp, hpC, hpS]
rw [hins, Finset.card_insert_of_notMem hnot]
have heq : (S \ C) \ {p} = S \ C := by
ext x
simp only [Finset.mem_sdiff, Finset.mem_erase, Finset.mem_singleton]
have hnp : x ∈ S → x ≠ p := by intro hx hxp; exact hpS (hxp ▸ hx)
tauto
rw [heq]
omega
Serving one phase faults at most once per distinct requested page not
already cached: servePhaseMisses phase C ρ ≤ (ρ.toFinset \ C).card.
lemma servePhaseMisses_le (phase C : Finset Page) (ρ : List Page) (hρ : ρ.toFinset ⊆ phase) :
servePhaseMisses phase C ρ ≤ (ρ.toFinset \ C).card := by
induction ρ generalizing C with
| nil => simp [servePhaseMisses]
| cons p ρ' ih =>
have hρ' : ρ'.toFinset ⊆ phase := by
intro x hx
exact hρ (by rw [List.toFinset_cons]; exact Finset.mem_insert.mpr (Or.inr hx))
have hsd := offlineStep_sdiff phase C p ρ'.toFinset hρ'
have hih := ih (offlineStep phase C p) hρ'
change (if p ∈ C then 0 else 1) + servePhaseMisses phase (offlineStep phase C p) ρ'
≤ ((p :: ρ').toFinset \ C).card
rw [List.toFinset_cons]
calc
(if p ∈ C then 0 else 1) + servePhaseMisses phase (offlineStep phase C p) ρ'
≤ (if p ∈ C then 0 else 1) + (ρ'.toFinset \ offlineStep phase C p).card :=
Nat.add_le_add_left hih _
_ = (if p ∈ C then 0 else 1) + ((ρ'.toFinset \ C) \ {p}).card := by rw [hsd]
_ ≤ (insert p ρ'.toFinset \ C).card := card_step_ineq ρ'.toFinset C p
Serving a phase whose requests all lie in phase from a cache within the
size bound stays within the size bound.
lemma servePhase_card_le (phase C : Finset Page) (ρ : List Page) (hC : C.card ≤ k)
(hρ : ρ.toFinset ⊆ phase) (hphase : phase.card ≤ k) :
(servePhase phase C ρ).card ≤ k := by
induction ρ generalizing C with
| nil => simpa [servePhase] using hC
| cons p ρ' ih =>
have hp_phase : p ∈ phase := hρ (by simp)
have hρ' : ρ'.toFinset ⊆ phase := by
intro x hx
exact hρ (by rw [List.toFinset_cons]; exact Finset.mem_insert.mpr (Or.inr hx))
simp [servePhase]
exact ih (offlineStep phase C p) (offlineStep_size phase C p hC hp_phase hphase) hρ'Serving a phase preserves an exactly-full cache.
lemma servePhase_card_eq (phase C : Finset Page) (ρ : List Page) (hC : C.card = k)
(hρ : ρ.toFinset ⊆ phase) (hphase : phase.card ≤ k) :
(servePhase phase C ρ).card = k := by
induction ρ generalizing C with
| nil => simpa [servePhase] using hC
| cons p ρ' ih =>
have hp_phase : p ∈ phase := hρ (by simp)
have hρ' : ρ'.toFinset ⊆ phase := by
intro x hx
exact hρ (by rw [List.toFinset_cons]; exact Finset.mem_insert.mpr (Or.inr hx))
simp [servePhase]
exact ih (offlineStep phase C p) (offlineStep_card_eq phase C p hC hp_phase hphase) hρ'
Serving one phase from a full cache over the k + 1-page universe faults at
most once.
lemma servePhaseMisses_le_one (C : Finset (Fin (k + 1))) (ρ : List (Fin (k + 1)))
(hC : C.card = k) (hρ : ρ.toFinset.card ≤ k) :
servePhaseMisses ρ.toFinset C ρ ≤ 1 := by
have hle := servePhaseMisses_le ρ.toFinset C ρ (by intro x hx; exact hx)
have hcard : (ρ.toFinset \ C).card ≤ 1 := by
have huniv : (Finset.univ : Finset (Fin (k + 1))).card = k + 1 := by
rw [Finset.card_univ, Fintype.card_fin]
have hcup : (C ∪ ρ.toFinset).card ≤ (Finset.univ : Finset (Fin (k + 1))).card :=
Finset.card_le_card (Finset.subset_univ _)
have hsd : (ρ.toFinset \ C).card = (C ∪ ρ.toFinset).card - C.card := by
rw [Finset.card_sdiff]
have hunion := Finset.card_union_add_card_inter C ρ.toFinset
omega
rw [hsd, hC]
omega
exact le_trans hle hcard
The number of phases is at most one more than σ.length / k.
lemma phases_length_le (k : ℕ) (σ : List Page) (hk : 0 < k) :
(phases k σ).length ≤ σ.length / k + 1 := by
classical
have go : ∀ (n : ℕ) (σ : List Page), σ.length = n →
(phases k σ).length ≤ σ.length / k + 1 := by
intro n
induction n using Nat.strong_induction_on with
| h n ih =>
intro σ hlen
cases σ with
| nil => simp [phases, WellFounded.fix_eq]
| cons p rest =>
let fp := firstPhase k (p :: rest)
have hsplit := firstPhase_split k (p :: rest)
have hnonempty : fp.1 ≠ [] := by
dsimp [fp]
exact firstPhase_nonempty k (p :: rest) (by simp)
by_cases hrem : fp.2 = []
· rw [phases_cons_eq k (p :: rest) (by simp)]
rw [hrem]
simp [phases, WellFounded.fix_eq]
· have hremlen : fp.2.length < (p :: rest).length := by
have hlen : fp.1.length + fp.2.length = (p :: rest).length := by
rw [← hsplit, List.length_append]
have hpos : 0 < fp.1.length := List.length_pos_of_ne_nil hnonempty
omega
have hremlen' : fp.2.length < n := by omega
have hih := ih fp.2.length hremlen' fp.2 rfl
have hcard : fp.1.toFinset.card = k := by
have hmax := firstPhase_maximal k (p :: rest) hk (by intro h; exact hrem (by simpa [fp, h] using h))
simpa [fp] using hmax.1
have hlen1 : k ≤ fp.1.length := by
have hle : fp.1.toFinset.card ≤ fp.1.length := by
rw [List.toFinset, Multiset.card_toFinset]
exact Multiset.card_le_card (Multiset.dedup_le _)
omega
have hdiv : 1 + fp.2.length / k ≤ (k + fp.2.length) / k := by
rw [Nat.le_div_iff_mul_le hk]
rw [Nat.add_mul]
simp only [one_mul]
have hle : fp.2.length / k * k ≤ fp.2.length := Nat.div_mul_le_self fp.2.length k
omega
have hlenσ : k + fp.2.length ≤ (p :: rest).length := by
rw [← hsplit, List.length_append]
exact Nat.add_le_add_right hlen1 fp.2.length
rw [phases_cons_eq k (p :: rest) (by simp)]
calc
(fp.1 :: phases k fp.2).length = 1 + (phases k fp.2).length := by simp only [List.length_cons]; omega
_ ≤ 1 + (fp.2.length / k + 1) := Nat.add_le_add_left hih 1
_ = 1 + fp.2.length / k + 1 := by omega
_ ≤ (k + fp.2.length) / k + 1 := Nat.add_le_add_right hdiv 1
_ ≤ (p :: rest).length / k + 1 := by
exact Nat.add_le_add_right (Nat.div_le_div_right hlenσ) 1
exact go σ.length σ rfl
From a full cache (exactly k pages) over the k + 1-page universe, the
offline schedule faults at most once per phase.
theorem offMisses_bound_from_full (k : ℕ) (σ : List (Fin (k + 1))) (hk : 0 < k)
(C : Finset (Fin (k + 1))) (hC : C.card = k) :
offMisses C (phases k σ) ≤ (phases k σ).length := by
classical
have go : ∀ (n : ℕ) (σ : List (Fin (k + 1))), σ.length = n →
∀ (C : Finset (Fin (k + 1))), C.card = k → offMisses C (phases k σ) ≤ (phases k σ).length := by
intro n
induction n using Nat.strong_induction_on with
| h n ih =>
intro σ hlen C hC
cases σ with
| nil => simp [offMisses, phases, WellFounded.fix_eq]
| cons p rest =>
let fp := firstPhase k (p :: rest)
have hsplit := firstPhase_split k (p :: rest)
have hnonempty : fp.1 ≠ [] := by
dsimp [fp]
exact firstPhase_nonempty k (p :: rest) (by simp)
have hcard_fp : fp.1.toFinset.card ≤ k := by
exact firstPhase_distinct_le k (p :: rest) hk
have hmisses : servePhaseMisses fp.1.toFinset C fp.1 ≤ 1 := by
exact servePhaseMisses_le_one C fp.1 hC (by simpa [fp] using hcard_fp)
have hcache : (servePhase fp.1.toFinset C fp.1).card = k := by
exact servePhase_card_eq fp.1.toFinset C fp.1 hC (by intro x hx; exact hx) (by simpa [fp] using hcard_fp)
have hremlen : fp.2.length < (p :: rest).length := by
have hlen : fp.1.length + fp.2.length = (p :: rest).length := by
rw [← hsplit, List.length_append]
have hpos : 0 < fp.1.length := List.length_pos_of_ne_nil hnonempty
omega
have hremlen' : fp.2.length < n := by omega
have hih := ih fp.2.length hremlen' fp.2 rfl (servePhase fp.1.toFinset C fp.1) hcache
rw [phases_cons_eq k (p :: rest) (by simp)]
simp [offMisses]
calc
servePhaseMisses fp.1.toFinset C fp.1 + offMisses (servePhase fp.1.toFinset C fp.1) (phases k fp.2)
≤ 1 + offMisses (servePhase fp.1.toFinset C fp.1) (phases k fp.2) := Nat.add_le_add_right hmisses _
_ ≤ 1 + (phases k fp.2).length := Nat.add_le_add_left hih 1
_ = (fp.1 :: phases k fp.2).length := by simp only [List.length_cons]; omega
exact go σ.length σ rfl C hCServing one phase from an empty cache yields exactly the phase's distinct pages as the new cache.
lemma servePhase_eq_union (phase C : Finset Page) (ρ : List Page) (hC : C ⊆ phase)
(hρ : ρ.toFinset ⊆ phase) : servePhase phase C ρ = C ∪ ρ.toFinset := by
induction ρ generalizing C with
| nil => simp [servePhase]
| cons p ρ' ih =>
have hp_phase : p ∈ phase := hρ (by simp)
have hρ' : ρ'.toFinset ⊆ phase := by
intro x hx
exact hρ (by rw [List.toFinset_cons]; exact Finset.mem_insert.mpr (Or.inr hx))
have hstep : offlineStep phase C p = insert p C := by
unfold offlineStep
by_cases hpC : p ∈ C
· simp [hpC]
· have hnone : ¬ ∃ q ∈ C, q ∉ phase := by
rintro ⟨q, hqC, hqnot⟩
exact hqnot (hC hqC)
simp [hpC, hnone]
simp [servePhase, hstep]
have hC' : insert p C ⊆ phase := by
rw [Finset.insert_subset_iff]
exact ⟨hp_phase, hC⟩
rw [ih (insert p C) hC' hρ']
ext x
simp only [Finset.mem_insert, Finset.mem_union]
tauto
Offline upper bound. The offline schedule faults at most
(phases k σ).length + k times over any request sequence on the k + 1-page
universe, starting from an empty cache.
theorem offMisses_bound (k : ℕ) (σ : List (Fin (k + 1))) (hk : 0 < k) :
offMisses ∅ (phases k σ) ≤ (phases k σ).length + k := by
classical
have go : ∀ (n : ℕ) (σ : List (Fin (k + 1))), σ.length = n →
offMisses ∅ (phases k σ) ≤ (phases k σ).length + k := by
intro n
induction n using Nat.strong_induction_on with
| h n ih =>
intro σ hlen
cases σ with
| nil => simp [offMisses, phases, WellFounded.fix_eq]
| cons p rest =>
let fp := firstPhase k (p :: rest)
have hsplit := firstPhase_split k (p :: rest)
have hnonempty : fp.1 ≠ [] := by
dsimp [fp]
exact firstPhase_nonempty k (p :: rest) (by simp)
have hcard_fp : fp.1.toFinset.card ≤ k := by
exact firstPhase_distinct_le k (p :: rest) hk
have hmisses : servePhaseMisses fp.1.toFinset ∅ fp.1 ≤ fp.1.toFinset.card := by
exact servePhaseMisses_le fp.1.toFinset ∅ fp.1 (by intro x hx; exact hx)
have hremlen : fp.2.length < (p :: rest).length := by
have hlen : fp.1.length + fp.2.length = (p :: rest).length := by
rw [← hsplit, List.length_append]
have hpos : 0 < fp.1.length := List.length_pos_of_ne_nil hnonempty
omega
have hremlen' : fp.2.length < n := by omega
by_cases hrem : fp.2 = []
· rw [phases_cons_eq k (p :: rest) (by simp), hrem]
simp [offMisses, phases, WellFounded.fix_eq]
exact le_trans (le_trans hmisses hcard_fp) (by omega)
· have hcard_fp_eq : fp.1.toFinset.card = k := by
have hmax := firstPhase_maximal k (p :: rest) hk (by intro h; exact hrem (by simpa [fp, h] using h))
simpa [fp] using hmax.1
have hcache_eq : servePhase fp.1.toFinset ∅ fp.1 = fp.1.toFinset := by
exact servePhase_eq_union fp.1.toFinset ∅ fp.1 (by simp) (by intro x hx; exact hx)
have hcache : (servePhase fp.1.toFinset ∅ fp.1).card = k := by
rw [hcache_eq, hcard_fp_eq]
have hih := offMisses_bound_from_full k fp.2 hk (servePhase fp.1.toFinset ∅ fp.1) hcache
rw [phases_cons_eq k (p :: rest) (by simp)]
simp [offMisses]
calc
servePhaseMisses fp.1.toFinset ∅ fp.1 + offMisses (servePhase fp.1.toFinset ∅ fp.1) (phases k fp.2)
≤ fp.1.toFinset.card + offMisses (servePhase fp.1.toFinset ∅ fp.1) (phases k fp.2) :=
Nat.add_le_add_right hmisses _
_ ≤ k + (phases k fp.2).length := by
rw [hcard_fp_eq]
exact Nat.add_le_add_left hih k
_ ≤ (fp.1 :: phases k fp.2).length + k := by
simp only [List.length_cons]
omega
exact go σ.length σ rfl
Theorem 27.4 (Sleator-Tarjan lower bound). For any deterministic online
paging algorithm A with cache size k ≥ 1, and any request count N, there
is a request sequence σ of length N (over k + 1 pages) on which A faults
on every request while some offline schedule faults at most N / k + k + 1
times. Hence no deterministic online algorithm is c-competitive for any
c < k.
theorem caching_lower_bound (k N : ℕ) (hk : 0 < k) (A : Algorithm (Fin (k + 1)) k) :
∃ σ : List (Fin (k + 1)),
σ.length = N ∧
misses A ∅ σ = N ∧
offMisses ∅ (phases k σ) ≤ N / k + k + 1 := by
refine ⟨advSeq A N, ?_, advSeq_misses A N, ?_⟩
· exact advSeq_length A N
· calc
offMisses ∅ (phases k (advSeq A N)) ≤ (phases k (advSeq A N)).length + k :=
offMisses_bound k (advSeq A N) hk
_ ≤ (advSeq A N).length / k + 1 + k := by
exact Nat.add_le_add_right (phases_length_le k (advSeq A N) hk) k
_ = N / k + k + 1 := by
rw [advSeq_length A]
omega
No c-competitive ratio below k. For any c < k, the adversarial
sequence of length k^3 witnesses that A's miss count strictly exceeds c
times the offline cost.
theorem caching_no_c_competitive (k : ℕ) (hk : 0 < k) (A : Algorithm (Fin (k + 1)) k) :
∀ c : ℕ, c < k → c * offMisses ∅ (phases k (advSeq A (k * k * k))) < misses A ∅ (advSeq A (k * k * k)) := by
intro c hc
have hbound : offMisses ∅ (phases k (advSeq A (k * k * k))) ≤ (k * k * k) / k + k + 1 := by
calc
offMisses ∅ (phases k (advSeq A (k * k * k))) ≤ (phases k (advSeq A (k * k * k))).length + k :=
offMisses_bound k (advSeq A (k * k * k)) hk
_ ≤ (advSeq A (k * k * k)).length / k + 1 + k := by
exact Nat.add_le_add_right (phases_length_le k (advSeq A (k * k * k)) hk) k
_ = (k * k * k) / k + k + 1 := by
rw [advSeq_length A]
omega
have hmiss : misses A ∅ (advSeq A (k * k * k)) = k * k * k := advSeq_misses A (k * k * k)
have hdiv : (k * k * k) / k = k * k := by
rw [Nat.mul_assoc]
exact Nat.mul_div_right (k * k) hk
calc
c * offMisses ∅ (phases k (advSeq A (k * k * k)))
≤ c * ((k * k * k) / k + k + 1) := Nat.mul_le_mul_left c hbound
_ ≤ (k - 1) * ((k * k * k) / k + k + 1) := by
exact Nat.mul_le_mul_right _ (by omega : c ≤ k - 1)
_ = (k - 1) * (k * k + k + 1) := by rw [hdiv]
_ < k * k * k := by
obtain ⟨d, rfl⟩ := Nat.exists_eq_succ_of_ne_zero (by omega : k ≠ 0)
simp only [Nat.succ_eq_add_one]
have hd : d + 1 - 1 = d := by omega
rw [hd]
have h : d * ((d + 1) * (d + 1) + (d + 1) + 1) + 1 = (d + 1) * (d + 1) * (d + 1) := by ring
rw [← h]
exact Nat.lt_succ_self _
_ = misses A ∅ (advSeq A (k * k * k)) := by rw [hmiss]end OnlineCachingend CLRSDefinitions and proofs
CLRSLean.FourthEdition.Chapter_27.Section_27_3_Online_Caching.Competitive
LRU against policies with arbitrary auxiliary state
Actual policy runs produce legal schedules. The offline trace theorem therefore also proves the online result without replaying any requests or assuming that hits leave auxiliary history unchanged.
namespace CLRS.OnlineCaching.Policyvariable {Page : Type} [DecidableEq Page] {k : Nat}theorem phases_from_resident (A : Policy Page k) (hk : 0 < k)
(p : Page) (rest : List Page) (s : A.State) (hp : p ∈ A.cache s) :
(phases k (p :: rest)).length ≤ A.misses s rest + 1 :=
Schedule.phases_from_resident hk p rest (A.schedule s rest) hptheorem phases_le_misses (A : Policy Page k) (hk : 0 < k) (s : A.State) (xs : List Page) :
(phases k xs).length ≤ A.misses s xs + 1 := (A.schedule s xs).phases_le_cost hkLRU's actual list execution is k-competitive against every history-dependent policy.
theorem lru_k_competitive (A : Policy Page k) (hk : 0 < k) (xs : List Page) :
(lruPolicy hk).misses (lruPolicy hk).initial xs ≤ k * A.misses A.initial xs + k := by
rw [lruPolicy_misses]
have hs := A.schedule A.initial xs
rw [A.initial_empty] at hs
exact hs.lru_k_competitive hkend CLRS.OnlineCaching.PolicyCLRSLean.FourthEdition.Chapter_27.Section_27_3_Online_Caching.LowerBound
Deterministic paging lower bound with real ratios and additive constants
The adversary observes only a policy's current resident set. Arbitrary hidden history is permitted in the online policy. The offline comparator is the existing trace-dependent phase schedule; it is not claimed to be an online finite-set transition. Witness length grows with the additive constant.
noncomputable sectionnamespace CLRS.OnlineCaching.Policyvariable {k : Nat} (A : Policy (Fin (k + 1)) k)def adversary (s : A.State) : Nat → List (Fin (k + 1))
| 0 => []
| n + 1 =>
let p := freshPage (A.cache s)
p :: adversary (A.step s p) n@[simp] theorem adversary_length (s : A.State) (n : Nat) :
(A.adversary s n).length = n := by
induction n generalizing s with
| zero => rfl
| succ n ih => simp [adversary, ih]@[simp] theorem adversary_misses (s : A.State) (n : Nat) :
A.misses s (A.adversary s n) = n := by
induction n generalizing s with
| zero => rfl
| succ n ih =>
have hp := freshPage_spec (A.cache s) (exists_page_not_mem _ (A.size s))
simp [adversary, misses, hp, ih, Nat.add_comm]A history-dependent online policy misses every request of an arbitrary-length witness.
theorem caching_lower_bound (hk : 0 < k) (N : Nat) :
∃ xs : List (Fin (k + 1)), xs.length = N ∧ A.misses A.initial xs = N ∧
offMisses ∅ (phases k xs) ≤ N / k + k + 1 := by
refine ⟨A.adversary A.initial N, by simp, by simp, ?_⟩
have h := offMisses_bound k (A.adversary A.initial N) hk
have hp := phases_length_le k (A.adversary A.initial N) hk
simp only [adversary_length] at hp
omegaEvery real ratio below k fails, for every real additive constant. The witness is nonempty and both runs begin with empty caches.
theorem no_real_competitive (hk : 0 < k) (c b : ℝ) (hc0 : 0 ≤ c) (hc : c < k) :
∃ xs : List (Fin (k + 1)), 0 < xs.length ∧
c * (offMisses ∅ (phases k xs) : ℝ) + b < (A.misses A.initial xs : ℝ) := by
have hgap : 0 < (k : ℝ) - c := sub_pos.mpr hc
obtain ⟨m,hm⟩ := exists_nat_gt (max 0 ((c * ((k : ℝ) + 1) + b) / ((k : ℝ) - c)))
have hmpos : 0 < m := by
have : (0 : ℝ) < m := (le_max_left _ _).trans_lt hm
exact_mod_cast this
have hratio : (c * ((k : ℝ) + 1) + b) / ((k : ℝ) - c) < m :=
(le_max_right _ _).trans_lt hm
have hlarge : c * ((k : ℝ) + 1) + b < (m : ℝ) * ((k : ℝ) - c) :=
(div_lt_iff₀ hgap).mp hratio
obtain ⟨xs,hlen,hmiss,hoff⟩ := A.caching_lower_bound hk (k * m)
have hdiv : k * m / k = m := Nat.mul_div_cancel_left m hk
rw [hdiv] at hoff
refine ⟨xs, by rw [hlen]; exact Nat.mul_pos hk hmpos, ?_⟩
have hoffR : (offMisses ∅ (phases k xs) : ℝ) ≤ (m : ℝ) + k + 1 := by exact_mod_cast hoff
have hscaled := mul_le_mul_of_nonneg_left hoffR hc0
rw [hmiss]
push_cast
nlinarithend CLRS.OnlineCaching.PolicyCLRSLean.FourthEdition.Chapter_27.Section_27_3_Online_Caching.OfflineCompetitive
LRU competitiveness against every legal offline trace
namespace CLRS.OnlineCaching.Schedulevariable {Page : Type} [DecidableEq Page] {k : Nat}theorem start_size {C F : Finset Page} {xs : List Page} {cost : Nat}
(h : Schedule k C xs F cost) : C.card ≤ k := by cases h <;> assumptiontheorem split {C F : Finset Page} (xs ys : List Page) {cost : Nat}
(h : Schedule k C (xs ++ ys) F cost) :
∃ D a b, Schedule k C xs D a ∧ Schedule k D ys F b ∧ cost = a + b := by
induction xs generalizing C cost with
| nil => exact ⟨C,0,cost,.nil C h.start_size,h,by omega⟩
| cons p xs ih =>
cases h with
| cons C C' p _ F cost hs hl hsub hh ht =>
obtain ⟨D,a,b,ha,hb,heq⟩ := ih ht
exact ⟨D,_,b,.cons C C' p xs D a hs hl hsub hh ha,hb,by omega⟩
theorem zero_subset {C F : Finset Page} {xs : List Page} {cost : Nat}
(h : Schedule k C xs F cost) (hc : cost = 0) : xs.toFinset ⊆ C := by
induction h with
| nil => simp
| cons C C' p xs F cost hs hl hsub hh ht ih =>
have hp : p ∈ C := by by_contra hn; simp [hn] at hc
have htail : cost = 0 := by simp [hp] at hc; exact hc
have hxs := ih htail
rw [hh hp] at hxs
simpa using Finset.insert_subset_iff.mpr ⟨hp,hxs⟩
theorem resident_fault {C F : Finset Page} {xs : List Page} {cost : Nat}
(h : Schedule k C xs F cost) (p : Page) (hp : p ∈ C)
(hx : k ≤ (xs.toFinset.erase p).card) : 1 ≤ cost := by
by_contra hn
have hs := h.zero_subset (by omega)
have hc := Finset.card_le_card (Finset.erase_subset_erase p hs)
rw [Finset.card_erase_of_mem hp] at hc
have hpos := Finset.card_pos.mpr ⟨p,hp⟩
have hb := h.start_size
omegatheorem singleton_loads {C F : Finset Page} {p : Page} {cost : Nat}
(h : Schedule k C [p] F cost) : p ∈ F := by
cases h with
| cons C C' p xs F cost hs hl hsub hh ht => cases ht; exact hl
theorem phases_from_resident (hk : 0 < k) (p : Page) (rest : List Page)
{C F : Finset Page} {cost : Nat} (h : Schedule k C rest F cost) (hp : p ∈ C) :
(phases k (p :: rest)).length ≤ cost + 1 := by
classical
have go : ∀ n (rest : List Page), rest.length = n → ∀ p C F cost,
Schedule k C rest F cost → p ∈ C → (phases k (p :: rest)).length ≤ cost + 1 := by
intro n
induction n using Nat.strong_induction_on with
| h n ih =>
intro rest hn p C F cost h hp
let chunk := phaseGo k ({p} : Finset Page) rest
have hsplit : chunk.1 ++ chunk.2 = rest := phaseGo_split k {p} rest
cases ht : chunk.2 with
| nil =>
rw [phases_cons_eq k (p :: rest) (by simp)]
change ((p :: chunk.1) :: phases k chunk.2).length ≤ _
rw [ht]
simp [phases, WellFounded.fix_eq]
| cons q tail =>
have hfp : firstPhase k (p :: rest) = (p :: chunk.1, chunk.2) := rfl
have hm := firstPhase_maximal k (p :: rest) hk (by rw [hfp, ht]; simp)
have hcard : (p :: chunk.1).toFinset.card = k := by simpa [hfp] using hm.1
have hq : q ∉ (p :: chunk.1).toFinset :=
firstPhase_fresh k (p :: rest) hk q tail (by rw [hfp]; exact ht)
have hset : (chunk.1 ++ [q]).toFinset.erase p =
(insert q (p :: chunk.1).toFinset).erase p := by
ext x
simp only [Finset.mem_erase, List.mem_toFinset, List.mem_append,
Finset.mem_insert, List.mem_cons]
tauto
have hcount : ((chunk.1 ++ [q]).toFinset.erase p).card = k := by
rw [hset, Finset.card_erase_of_mem (by simp), Finset.card_insert_of_notMem hq, hcard]
omega
have hrest : rest = (chunk.1 ++ [q]) ++ tail := by
rw [ht] at hsplit
simpa [List.append_assoc] using hsplit.symm
rw [hrest] at h
obtain ⟨D,a,b,ha,hb,hcost⟩ := split (chunk.1 ++ [q]) tail h
have hfault := ha.resident_fault p hp hcount.ge
obtain ⟨M,aa,ab,_,hlast,_⟩ := split chunk.1 [q] ha
have hresident := hlast.singleton_loads
have hlen : tail.length < n := by
have := congrArg List.length hrest
simp at this
omega
have hnext := ih tail.length hlen tail rfl q D F b hb hresident
rw [phases_cons_eq k (p :: rest) (by simp)]
change ((p :: chunk.1) :: phases k chunk.2).length ≤ _
rw [ht]
simp only [List.length_cons]
omega
exact go rest.length rest rfl p C F cost h hpEvery legal trace, including a future-dependent offline schedule, obeys the phase lower bound.
theorem phases_le_cost (hk : 0 < k) {C F : Finset Page} {xs : List Page} {cost : Nat}
(h : Schedule k C xs F cost) : (phases k xs).length ≤ cost + 1 := by
cases h with
| nil => simp [phases, WellFounded.fix_eq]
| cons C C' p xs F cost hs hl hsub hh ht =>
have hb := phases_from_resident hk p xs ht hl
split <;> omegaLRU's actual misses are k-competitive against every legal empty-start offline schedule.
theorem lru_k_competitive (hk : 0 < k) {F : Finset Page} {xs : List Page} {cost : Nat}
(h : Schedule k ∅ xs F cost) : lruMissesGo k [] xs ≤ k * cost + k := by
have hu := lru_miss_le_phases k [] xs hk (by simp)
have hl := h.phases_le_cost hk
nlinarith [Nat.mul_le_mul_left k hl]end CLRS.OnlineCaching.ScheduleCLRSLean.FourthEdition.Chapter_27.Section_27_3_Online_Caching.Policies
Inhabited paging policies with auxiliary state
The legacy finite-set interface describes memoryless policies. The new Policy interface has arbitrary internal state and exposes only its resident set. A hit preserves residency, but may update history. The transition receives only the current state and request, so it cannot inspect future requests. Capacity is an invariant of every state in its state type. An offline schedule instead receives a request trace and may choose its evictions from that trace.
namespace CLRS.OnlineCachingvariable {Page : Type} [DecidableEq Page] {k : Nat}A concrete memoryless policy: retain a hit, flush to the request on a miss.
def flushAlgorithm (hk : 0 < k) : Algorithm Page k where
step C p := if p ∈ C then C else {p}
step_loads := by intro C p; split <;> simp_all
step_subset := by intro C p; split <;> simp_all
step_size := by intro C p hC; split <;> simp_all; omega
step_hit := by intro C p _ hp; simp [hp]Positive-capacity policy space is inhabited even on the k+1 page universe.
theorem algorithm_nonempty (hk : 0 < k) : Nonempty (Algorithm Page k) := ⟨flushAlgorithm hk⟩structure Policy (Page : Type) [DecidableEq Page] (k : Nat) where
State : Type
cache : State → Finset Page
initial : State
initial_empty : cache initial = ∅
step : State → Page → State
size : ∀ s, (cache s).card ≤ k
loads : ∀ s p, p ∈ cache (step s p)
subset : ∀ s p, cache (step s p) ⊆ insert p (cache s)
hit : ∀ s p, p ∈ cache s → cache (step s p) = cache snamespace Policyvariable (A : Policy Page k)def run : A.State → List Page → A.State
| s, [] => s
| s, p :: ps => run (A.step s p) psdef misses : A.State → List Page → Nat
| _, [] => 0
| s, p :: ps => (if p ∈ A.cache s then 0 else 1) + misses (A.step s p) ps@[simp] theorem run_append (s : A.State) (xs ys : List Page) :
A.run s (xs ++ ys) = A.run (A.run s xs) ys := by
induction xs generalizing s with
| nil => rfl
| cons p xs ih => simp [run, ih]@[simp] theorem misses_append (s : A.State) (xs ys : List Page) :
A.misses s (xs ++ ys) = A.misses s xs + A.misses (A.run s xs) ys := by
induction xs generalizing s with
| nil => simp [misses, run]
| cons p xs ih => simp [misses, run, ih, Nat.add_assoc]
theorem zero_subset (s : A.State) (xs : List Page) (h : A.misses s xs = 0) :
xs.toFinset ⊆ A.cache s := by
induction xs generalizing s with
| nil => simp
| cons p xs ih =>
have hp : p ∈ A.cache s := by
by_contra hn
simp [misses, hn] at h
have ht : A.misses (A.step s p) xs = 0 := by simpa [misses, hp] using h
have hs := ih (A.step s p) ht
rw [A.hit s p hp] at hs
simpa using Finset.insert_subset_iff.mpr ⟨hp, hs⟩
theorem resident_fault (s : A.State) (p : Page) (xs : List Page)
(hp : p ∈ A.cache s) (hx : k ≤ (xs.toFinset.erase p).card) :
1 ≤ A.misses s xs := by
by_contra hn
have hz : A.misses s xs = 0 := by omega
have hs := A.zero_subset s xs hz
have hc : (xs.toFinset.erase p).card ≤ ((A.cache s).erase p).card :=
Finset.card_le_card (Finset.erase_subset_erase p hs)
rw [Finset.card_erase_of_mem hp] at hc
have hpos := Finset.card_pos.mpr ⟨p,hp⟩
have hb := A.size s
omegaend PolicyLRU stores recency order as well as residency.
structure LRUState (Page : Type) (k : Nat) where
recency : List Page
nodup : recency.Nodup
size : recency.length ≤ ktheorem lruStep_length_le (hk : 0 < k) (L : List Page) (p : Page) (hL : L.length ≤ k) :
(lruStep k L p).length ≤ k := by
unfold lruStep
split_ifs with hp hl
· have heq := (List.perm_cons_erase hp).length_eq
simpa only [← heq] using hL
· simp only [List.length_cons]; omega
· simp only [List.length_cons, List.length_dropLast]; omegadef lruPolicy (hk : 0 < k) : Policy Page k where
State := LRUState Page k
cache s := s.recency.toFinset
initial := ⟨[], by simp, by simp⟩
initial_empty := rfl
step s p := ⟨lruStep k s.recency p, lruStep_nodup k _ p s.nodup,
lruStep_length_le hk _ p s.size⟩
size s := by simpa [toFinset_card_of_nodup s.nodup] using s.size
loads s p := by simp [lruStep]; split_ifs <;> simp
subset s p := by
intro q hq
simp only [List.mem_toFinset, mem_lruStep] at hq
split_ifs at hq with hp hl
· exact Finset.mem_insert_of_mem (List.mem_toFinset.mpr hq)
· simpa using hq
· rcases hq with rfl | hq
· simp
· exact Finset.mem_insert_of_mem (List.mem_toFinset.mpr (List.mem_of_mem_dropLast hq))
hit s p hp := by
ext q
simp only [List.mem_toFinset] at hp ⊢
simp [mem_lruStep, hp]The stateful LRU policy's misses are those of the original list execution.
theorem lruPolicy_misses (hk : 0 < k) (s : LRUState Page k) (xs : List Page) :
(lruPolicy hk).misses s xs = lruMissesGo k s.recency xs := by
induction xs generalizing s with
| nil => rfl
| cons p xs ih =>
change (if p ∈ s.recency.toFinset then 0 else 1) +
(lruPolicy hk).misses ((lruPolicy hk).step s p) xs = _
rw [ih]
simp [lruMissesGo, lruPolicy]end CLRS.OnlineCachingCLRSLean.FourthEdition.Chapter_27.Section_27_3_Online_Caching.Schedules
Valid offline paging schedules
A schedule records legal cache transitions for an entire request trace, with its actual miss count. It can depend on the future. The existing phase-based offline execution produces such a schedule, including every intermediate capacity invariant; its comparison cost is therefore attainable.
namespace CLRS.OnlineCachingvariable {Page : Type} [DecidableEq Page] {k : Nat}inductive Schedule (k : Nat) : Finset Page → List Page → Finset Page → Nat → Prop
| nil (C : Finset Page) (size : C.card ≤ k) : Schedule k C [] C 0
| cons (C C' : Finset Page) (p : Page) (xs : List Page) (final : Finset Page) (cost : Nat)
(size : C.card ≤ k) (loads : p ∈ C') (subset : C' ⊆ insert p C)
(hit : p ∈ C → C' = C) (tail : Schedule k C' xs final cost) :
Schedule k C (p :: xs) final ((if p ∈ C then 0 else 1) + cost)namespace Scheduletheorem append {C D F : Finset Page} {xs ys : List Page} {a b : Nat}
(hx : Schedule k C xs D a) (hy : Schedule k D ys F b) :
Schedule k C (xs ++ ys) F (a + b) := by
induction hx with
| nil _ _ => simpa using hy
| cons C C' p xs final cost hs hl hsub hh ht ih =>
simpa [Nat.add_assoc] using cons C C' p (xs ++ ys) F (cost+b) hs hl hsub hh (ih hy)end Schedule
theorem servePhase_schedule (phase C : Finset Page) (xs : List Page)
(hC : C.card ≤ k) (hxs : xs.toFinset ⊆ phase) (hphase : phase.card ≤ k) :
Schedule k C xs (servePhase phase C xs) (servePhaseMisses phase C xs) := by
induction xs generalizing C with
| nil => exact .nil C hC
| cons p xs ih =>
have hp := hxs (by simp : p ∈ (p::xs).toFinset)
have ht : xs.toFinset ⊆ phase := by intro x hx; exact hxs (by simp [hx])
exact .cons C (offlineStep phase C p) p xs _ _ hC
(offlineStep_loads _ _ _) (offlineStep_subset _ _ _) (offlineStep_hit _ _ _)
(ih _ (offlineStep_size _ _ _ hC hp hphase) ht)
theorem off_schedule (C : Finset Page) (ps : List (List Page)) (hC : C.card ≤ k)
(hps : ∀ xs ∈ ps, xs.toFinset.card ≤ k) :
Schedule k C ps.flatten (offCache C ps) (offMisses C ps) := by
induction ps generalizing C with
| nil => exact .nil C hC
| cons xs ps ih =>
have hx := hps xs (by simp)
have hs := servePhase_schedule xs.toFinset C xs hC (by intro x h; exact h) hx
have ht := ih (servePhase xs.toFinset C xs)
(servePhase_card_le _ _ _ hC (by intro x h; exact h) hx)
(by intro ys hy; exact hps ys (by simp [hy]))
exact hs.append ht
theorem phases_capacity (hk : 0 < k) (xs : List Page) :
∀ ys ∈ phases k xs, ys.toFinset.card ≤ k := by
classical
have go : ∀ n (xs : List Page), xs.length = n → ∀ ys ∈ phases k xs, ys.toFinset.card ≤ k := by
intro n
induction n using Nat.strong_induction_on with
| h n ih =>
intro xs hn ys hy
by_cases hempty : xs = []
· subst xs; simp [phases, WellFounded.fix_eq] at hy
· rw [phases_cons_eq k xs hempty] at hy
rcases List.mem_cons.mp hy with rfl | hy
· exact firstPhase_distinct_le k xs hk
· have hlen : (firstPhase k xs).2.length < n := by
have hsplit := congrArg List.length (firstPhase_split k xs)
have hp := List.length_pos_of_ne_nil (firstPhase_nonempty k xs hempty)
simp only [List.length_append] at hsplit
omega
exact ih _ hlen _ rfl ys hy
exact go xs.length xs rflThe phase comparator cost is realized by a legal empty-start offline schedule.
theorem offline_schedule_valid (hk : 0 < k) (xs : List Page) :
Schedule k ∅ xs (offCache ∅ (phases k xs)) (offMisses ∅ (phases k xs)) := by
simpa only [phases_join] using off_schedule (∅ : Finset Page) (phases k xs)
(by simp) (phases_capacity hk xs)A policy's actual run supplies a legal schedule, including hidden-state policies.
theorem Policy.schedule (A : Policy Page k) (s : A.State) (xs : List Page) :
Schedule k (A.cache s) xs (A.cache (A.run s xs)) (A.misses s xs) := by
induction xs generalizing s with
| nil => exact .nil _ (A.size s)
| cons p xs ih =>
exact .cons _ _ p xs _ _ (A.size s) (A.loads s p) (A.subset s p) (A.hit s p) (ih _)end CLRS.OnlineCachingScope and implementation notes
Imports
import CLRSLean.FourthEdition.Chapter_27.Section_27_1_Waiting_For_Elevator
import CLRSLean.FourthEdition.Chapter_27.Section_27_2_Maintaining_A_Search_List
import CLRSLean.FourthEdition.Chapter_27.Section_27_3_Online_Caching
import CLRSLean.FourthEdition.Chapter_27.Section_27_3_Online_Caching.Competitive
import CLRSLean.FourthEdition.Chapter_27.Section_27_3_Online_Caching.LowerBoundCurrent source
Section 27.1 (Waiting for an elevator) is formalized natively in
CLRSLean.FourthEdition.Chapter_27.Section_27_1_Waiting_For_Elevator: the
rent-or-buy (ski rental) problem — the cost of the deterministic
rent-a-days-then-buy strategy, the optimal offline cost, Theorem 27.1 (any
strategy with a * r < p ≤ (a + 1) * r is 2-competitive), and the elevator
corollary whose wait-S - E-then-take-the-stairs strategy is 2-competitive
with worst-case ratio 2 - E/S.
Section 27.2 (Maintaining a search list) is formalized natively in
CLRSLean.FourthEdition.Chapter_27.Section_27_2_Maintaining_A_Search_List:
the list-update problem, the MOVE-TO-FRONT strategy with its per-request cost,
the inversion-distance potential, and Theorem 27.2 (MOVE-TO-FRONT is
4-competitive against any list-update strategy that keeps its list a
permutation of the initial set).
Section 27.3 (Online caching) is formalized natively in
CLRSLean.FourthEdition.Chapter_27.Section_27_3_Online_Caching: the paging
model with the least-recently-used (LRU) policy as a most-recent-first list,
the legacy memoryless Algorithm with legal-state capacity/hit laws,
and the new Policy interface with arbitrary auxiliary state. An inhabited
LRU policy preserves recency order, so equal resident sets can produce different
evictions. Schedule describes future-dependent offline traces separately.
Schedule.lru_k_competitive proves the actual LRU miss bound against every
legal offline schedule. Policy.lru_k_competitive follows for actual online
policy runs. The actual phase-based offline execution is a valid schedule.
Policy.no_real_competitive proves that for every real ratio
0 ≤ c < k and every real additive constant there is a nonempty request
sequence defeating that ratio. The witness length grows with the constant;
this theorem permits arbitrary hidden history and requires positive capacity.
No legacy source is promoted into this chapter.
Coverage boundary
The represented rental lower bounds retain positive horizons and strictly
positive offline costs; skiRental_not_competitive_below rules out every
strictly smaller ratio. The list-update theorem uses equality of distinct-key
sets. Physical index and adjacent-swap interpretations require duplicate-free
permutations; arbitrary offline list traces are not covered by its strategy
interface. Paging uses explicit valid states, history-dependent online policies,
and separately validated offline schedules. These are abstract competitive
cost models, not machine-runtime bounds.
See docs/clrs-fourth-edition-map.csv for the section-level mapping and
docs/migrations/clrs4.md for compatibility and deprecation policy.
CLRS, fourth edition · Chapter 27 of 35