summaryrefslogtreecommitdiff
path: root/Day3.lean
diff options
context:
space:
mode:
authorAndreas Grois <andi@grois.info>2023-12-03 21:13:47 +0100
committerAndreas Grois <andi@grois.info>2023-12-03 21:13:47 +0100
commit0cdb5a2449c996e54a0c777dd807785d3016fd66 (patch)
tree40f1ffe9a46fe2d342c37204361ee1d2c897110a /Day3.lean
parentd09a6869fea6f0d79c57e4ff756e21e03d395d52 (diff)
Day 3
Diffstat (limited to 'Day3.lean')
-rw-r--r--Day3.lean143
1 files changed, 143 insertions, 0 deletions
diff --git a/Day3.lean b/Day3.lean
new file mode 100644
index 0000000..81eb5b8
--- /dev/null
+++ b/Day3.lean
@@ -0,0 +1,143 @@
+import «Common»
+import Lean.Data.HashSet
+import Lean.Data.HashMap
+
+namespace Day3
+structure Coordinate : Type 0 where
+ x : Nat
+ y : Nat
+ deriving Repr, BEq, Ord, Hashable
+
+def Coordinate.default : Coordinate := {x := 0, y := 0}
+
+/--Returns the adjacent fields, row-major order (this is important)-/
+def Coordinate.adjacents : Coordinate → List Coordinate
+| { x := 0, y := 0} => [
+ .mk 1 0,
+ .mk 1 1, .mk 0 1
+ ]
+| { x := 0, y := y} => [
+ .mk 0 y.pred, .mk 1 y.pred,
+ .mk 1 y,
+ .mk 0 y.succ, .mk 1 y.succ
+ ]
+| { x := x, y := 0} => [
+ .mk x.pred 0, .mk x.succ 0,
+ .mk x.pred 1, .mk x 1, .mk x.succ 1
+ ]
+| { x := x, y := y} => [
+ .mk x.pred y.pred, .mk x y.pred, .mk x.succ y.pred,
+ .mk x.pred y, .mk x.succ y,
+ .mk x.pred y.succ, .mk x y.succ, .mk x.succ y.succ
+ ]
+
+structure Part : Type 0 where
+ symbol : Char
+ position : Coordinate
+ deriving Repr
+
+structure PartNumber : Type 0 where
+ value : Nat
+ positions : List Coordinate
+ deriving Repr, BEq
+
+-- Schematic is just using lists, because at this point it's not clear what kind of lookup
+-- is needed in part 2... Probably some kind of HashMap Coordinate Whatever, but that's
+-- guesswork for now...
+-- Parts can refine the data further, anyhow.
+structure Schematic : Type 0 where
+ parts : List Part
+ numbers : List PartNumber
+ deriving Repr
+
+/-- nextByChar gives the coordinate of the **next** character. -/
+private def Coordinate.nextByChar : Coordinate → Char → Coordinate
+| {x := _, y := oldY}, '\n' => { x := 0, y := oldY + 1 }
+| {x := oldX, y := oldY}, _ => { x := oldX + 1, y := oldY }
+
+private def extractParts : List (Coordinate × Char) → List Part :=
+ (List.map (uncurry $ flip Part.mk)) ∘ (List.filter $ not ∘ λ (sc : Coordinate × Char) ↦ sc.snd.isWhitespace || sc.snd.isDigit || sc.snd == '.')
+
+private def extractPartNumbers (input : List (Coordinate × Char)) : List PartNumber :=
+ let rec helper : (List (Coordinate × Char)) → Option PartNumber → List PartNumber := λ
+ | [], none => []
+ | [], some acc => [acc] -- if we are still accumulating a number at the end, commit
+ | a :: as, none =>
+ if p: a.snd.isDigit then
+ --start accumulating a PartNumber
+ helper as $ some {value := a.snd.asDigit p, positions := [a.fst]}
+ else
+ --not accumulating, not a digit, not of interest.
+ helper as none
+ | a :: as, some acc =>
+ if p: a.snd.isDigit then
+ --keep accumulating
+ helper as $ some {value := acc.value * 10 + a.snd.asDigit p, positions := a.fst :: acc.positions }
+ else
+ -- we were accumulating, aren't on a number any more -> commit
+ acc :: helper as none
+ helper input none
+
+def parse (schematic : String) : Option Schematic := do
+ -- I think this one is easier if we don't split the input in lines. Because:
+ let charsWithCoordinates ← match schematic.toList with
+ | [] => none
+ | c :: cs => pure $ cs.scan (λ s c ↦ (uncurry Coordinate.nextByChar s, c)) (Coordinate.default, c)
+ -- Whitespaces are **intentionally** left in. This makes extracting the numbers easier.
+ pure $ {
+ parts := extractParts charsWithCoordinates
+ numbers := extractPartNumbers charsWithCoordinates
+ }
+
+def part1 (schematic : Schematic) : Nat :=
+ -- fast lookup: We need to know if a part is at a given coordinate
+ open Lean(HashSet) in
+ let partCoordinates : HashSet Coordinate :=
+ HashSet.insertMany HashSet.empty $ schematic.parts.map Part.position
+ let partNumbers := schematic.numbers.filter λ number ↦
+ number.positions.any λ position ↦
+ position.adjacents.any partCoordinates.contains
+ partNumbers.foldl (. + PartNumber.value .) 0
+
+def part2 (schematic : Schematic) : Nat :=
+ -- and now it is obvous that keeping the parsed input free of opinions was a good idea.
+ -- because here we need quick lookup for the numbers, not the parts.
+ open Lean(HashMap) in
+ let numberCoordinates : HashMap Coordinate PartNumber :=
+ Lean.HashMap.ofList $ schematic.numbers.bind $ λ pn ↦
+ pn.positions.map λ pos ↦
+ (pos, pn)
+ let gearSymbols := schematic.parts.filter (Part.symbol . == '*')
+ -- but the symbols aren't enough, they need to be adjacent to **exactly** 2 numbers
+ let numbersNextGearSymbols := List.dedup <$> gearSymbols.map λ gs ↦
+ gs.position.adjacents.filterMap numberCoordinates.find?
+ let gearSymbols := numbersNextGearSymbols.filter (List.length . == 2)
+ let gearRatios : List Nat := gearSymbols.map λ l ↦ l.map PartNumber.value |> List.foldl (.*.) 1
+ gearRatios.foldl (.+.) 0
+
+open DayPart
+
+instance : Parse ⟨3, by simp⟩ (ι := Schematic) where
+ parse := parse
+
+instance : DayPart.Part ⟨3,_⟩ Parts.One (ι := Schematic) (ρ := Nat) where
+ run := some ∘ part1
+
+instance : DayPart.Part ⟨3,_⟩ Parts.Two (ι := Schematic) (ρ := Nat) where
+ run := some ∘ part2
+
+private def testInput := "467..114..
+...*......
+..35..633.
+......#...
+617*......
+.....+.58.
+..592.....
+......755.
+...$.*....
+.664.598..
+"
+
+#eval parse testInput
+
+#eval part1 <$> parse testInput