summaryrefslogtreecommitdiff
path: root/Day6.lean
blob: 9319a0e56c9b9f3e571890737487b05c56fadee9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import Common.Option
import Common.Helpers
import Common.DayPart

namespace Day6
structure Race where
  timeLimit : Nat
  recordDistance : Nat
  deriving Repr

private def parseLine (header : String) (input : String) : Except String (List Nat) := do
  if not $ input.startsWith header then
    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 (not  String.isEmpty)
  numbers.mapM $ Option.toExcept s!"Failed to parse input line: Not a number {input}"   String.toNat?

def parse (input : String) : Except String (List Race) := do
  let lines := input.splitOn "\n"
    |> List.map String.trim
    |> List.filter (not  String.isEmpty)
  let (times, distances)  match lines with
    | [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
    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