summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndreas Grois <andi@grois.info>2023-12-07 21:53:19 +0100
committerAndreas Grois <andi@grois.info>2023-12-07 21:53:19 +0100
commite6f4111aa97f34383369577bff011d546d26d000 (patch)
treeb9990a0b7b1bc6c2b6a9684356656d5adb29f6b2
parent8d6efb1c1fdddc1fbad167510ce25e56be684130 (diff)
Day 6
-rw-r--r--Common/Option.lean4
-rw-r--r--Day6.lean84
-rw-r--r--Main.lean3
3 files changed, 82 insertions, 9 deletions
diff --git a/Common/Option.lean b/Common/Option.lean
index 23ea8d6..275f83f 100644
--- a/Common/Option.lean
+++ b/Common/Option.lean
@@ -5,3 +5,7 @@ def zip (a : Option α) (b : Option β) : Option (α×β) := a >>= λ a ↦ b >>
def unzip : (a : Option (α×β)) → (Option α) × (Option β)
| none => (none, none)
| some (a, b) => (some a, some b)
+
+def toExcept {α : Type u0} {ε : Type u1} { m : Type u0 → Type u2} [Pure m] [MonadExcept m (ε := ε)] (error : ε) : Option α → m α
+| none => throw error
+| some a => pure a
diff --git a/Day6.lean b/Day6.lean
index 54464ea..93f7dbb 100644
--- a/Day6.lean
+++ b/Day6.lean
@@ -1,27 +1,93 @@
import Common.Option
import Common.Helpers
+import Common.DayPart
structure Race where
timeLimit : Nat
recordDistance : Nat
+ deriving Repr
-private def parseLine (header : String) (input : String) : Option (List Nat) := do
+private def parseLine (header : String) (input : String) : Except String (List Nat) := do
if not $ input.startsWith header then
- failure
+ throw s!"Unexpected line header: {header}, {input}"
let input := input.drop header.length |> String.trim
let numbers := input.split Char.isWhitespace
|> List.map String.trim
- |> List.filter String.isEmpty
- numbers.mapM String.toNat?
+ |> List.filter (not ∘ String.isEmpty)
+ numbers.mapM $ Option.toExcept s!"Failed to parse input line: Not a number {input}" ∘ String.toNat?
-def parse (input : String) : Option (List Race) := do
+def parse (input : String) : Except String (List Race) := do
let lines := input.splitOn "\n"
|> List.map String.trim
- |> List.filter String.isEmpty
+ |> List.filter (not ∘ String.isEmpty)
let (times, distances) ← match lines with
- | [times, distances] => Option.zip (parseLine "Time:" times) (parseLine "Distance:" distances)
- | _ => none
+ | [times, distances] =>
+ let times ← parseLine "Time:" times
+ let distances ← parseLine "Distance:" distances
+ pure (times, distances)
+ | _ => throw "Failed to parse: there should be exactly 2 lines of input"
if times.length != distances.length then
- failure
+ throw "Input lines need to have the same number of, well, numbers."
let pairs := times.zip distances
+ if pairs = [] then
+ throw "Input does not have at least one race."
return pairs.map $ uncurry Race.mk
+
+-- okay, part 1 is a quadratic equation. Simple as can be
+-- s = v * tMoving
+-- s = tPressed * (tLimit - tPressed)
+-- (tPressed - tLimit) * tPressed + s = 0
+-- tPressed² - tPressed * tLimit + s = 0
+-- tPressed := tLimit / 2 ± √(tLimit² / 4 - s)
+-- beware: We need to _beat_ the record, so s here is the record + 1
+
+-- Inclusive! This is the smallest number that can win, and the largest number that can win
+private def Race.timeRangeToWin (input : Race) : (Nat × Nat) :=
+ let tLimit := input.timeLimit.toFloat
+ let sRecord := input.recordDistance.toFloat
+ let tlimitHalf := 0.5 * tLimit
+ let theRoot := (tlimitHalf^2 - sRecord - 1.0).sqrt
+ let lowerBound := tlimitHalf - theRoot
+ let upperBound := tlimitHalf + theRoot
+ let lowerBound := lowerBound.ceil.toUInt64.toNat
+ let upperBound := upperBound.floor.toUInt64.toNat
+ (lowerBound,upperBound)
+
+def part1 (input : List Race) : Nat :=
+ let limits := input.map Race.timeRangeToWin
+ let counts := limits.map $ λ p ↦ p.snd - p.fst + 1 -- inclusive range
+ counts.foldl (· * ·) 1
+
+-- part2 is the same thing, but here we need to be careful.
+-- namely, careful about the precision of Float. Which luckily is enough, as confirmed by pen&paper
+-- but _barely_ enough.
+-- If Lean's Float were an actual C float and not a C double, this would not work.
+
+-- we need to concatenate the numbers again (because I don't want to make a separate parse for part2)
+private def combineNumbers (left : Nat) (right : Nat) : Nat :=
+ let rec countDigits := λ (s : Nat) (n : Nat) ↦
+ if p : n > 0 then
+ have : n > n / 10 := by simp[p, Nat.div_lt_self]
+ countDigits (s+1) (n/10)
+ else
+ s
+ let d := if right = 0 then 1 else countDigits 0 right
+ left * (10^d) + right
+
+def part2 (input : List Race) : Nat :=
+ let timeLimits := input.map Race.timeLimit
+ let timeLimit := timeLimits.foldl combineNumbers 0
+ let records := input.map Race.recordDistance
+ let record := records.foldl combineNumbers 0
+ let limits := Race.timeRangeToWin $ {timeLimit := timeLimit, recordDistance := record}
+ limits.snd - limits.fst + 1 -- inclusive range
+
+open DayPart
+instance : Parse ⟨6, by simp⟩ (ι := List Race) where
+ parse := parse
+
+instance : Part ⟨6, _⟩ Parts.One (ι := List Race) (ρ := Nat) where
+ run := some ∘ part1
+
+instance : Part ⟨6, _⟩ Parts.Two (ι := List Race) (ρ := Nat) where
+ run := some ∘ part2
diff --git a/Main.lean b/Main.lean
index 1ad3933..7bfce37 100644
--- a/Main.lean
+++ b/Main.lean
@@ -4,6 +4,7 @@ import «Day2»
import «Day3»
import «Day4»
import «Day5»
+import «Day6»
open DayPart
@@ -26,6 +27,8 @@ def try_run_day_part (day : Days) (part : Parts) (data : String) : IO String :=
| ⟨4,_⟩, Parts.Two => impl ⟨4,_⟩ Parts.Two data
| ⟨5,_⟩, Parts.One => impl ⟨5,_⟩ Parts.One data
| ⟨5,_⟩, Parts.Two => impl ⟨5,_⟩ Parts.Two data
+ | ⟨6,_⟩, Parts.One => impl ⟨6,_⟩ Parts.One data
+ | ⟨6,_⟩, Parts.Two => impl ⟨6,_⟩ Parts.Two data
| _, _ => throw $ IO.userError "The requested combination of day/part has not been implemented yet."
def main (parameters : List String): IO Unit := do