summaryrefslogtreecommitdiff
path: root/Day14.lean
blob: d0acf6b703a037195b00c24991b3cd750cf1638b (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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import «Common»
import Lean.Data.HashMap
import Lean.Data.HashSet

namespace Day14

------------------------------------------------------------------------------------

inductive Tile
| Space
| Cube
| Round
deriving Repr, BEq

instance : LawfulBEq Tile where
  rfl := λ{a}  by cases a <;> decide
  eq_of_beq := by intros a b; cases a <;> cases b <;> simp <;> decide

instance: ToString Tile where
toString := λ
| Tile.Space => "."
| Tile.Cube => "#"
| Tile.Round => "O"

structure ParseCharError where
unexpectedCharacter : Char

instance : ToString ParseCharError where
toString := λc  s!"Unexpected Character '{c.unexpectedCharacter}', expected '.', '#', or 'O'."

private def Tile.ofChar? : Char  Except ParseCharError Tile
| '.' => pure Tile.Space
| '#' => pure Tile.Cube
| 'O' => pure Tile.Round
| c => throw {unexpectedCharacter := c}

abbrev ControlPanel := Parsing.RectangularGrid Tile

abbrev ParseInputError := Parsing.RectangularGrid.ParseError ParseCharError

def parse : String  Except ParseInputError ControlPanel :=
  Parsing.RectangularGrid.ofSubstring Tile.ofChar?  String.toSubstring

------------------------------------------------------------------------------------

/-- Finds the northmost free space reachable from start. Does not look at start. -/
private def findNorthmostFreeTile {controlPanel : ControlPanel} (start : controlPanel.Coordinate) : controlPanel.Coordinate :=
  if start.y > 0,controlPanel.not_empty.right then
    let northNeighbour := { start with y := start.y - 1,Nat.lt_imp_pred_lt start.y.isLt }
    match controlPanel[northNeighbour] with
    | Tile.Space => findNorthmostFreeTile northNeighbour
    | Tile.Round | Tile.Cube => start
  else
    start
termination_by start.y

private def moveTileNorth {controlPanel : ControlPanel} (position : controlPanel.Coordinate) : ControlPanel :=
  match controlPanel[position] with
  | Tile.Space | Tile.Cube => controlPanel
  | Tile.Round =>
    let intermediate := controlPanel.set position Tile.Space
    let positionInIntermediate : intermediate.Coordinate := {x := position.x, y := position.y} -- how? well, I won't complain
    let northMost := findNorthmostFreeTile positionInIntermediate
    intermediate.set northMost Tile.Round

private theorem moveTileNorth_same_size {controlPanel : ControlPanel} (position : controlPanel.Coordinate) : (moveTileNorth position).width = controlPanel.width  (moveTileNorth position).height = controlPanel.height := by
  unfold moveTileNorth
  constructor <;> split <;> rfl

private def moveAllTilesNorth (controlPanel : ControlPanel) : ControlPanel := Id.run do
  let mut result : ((c : ControlPanel) ×' (c.width = controlPanel.width  c.height = controlPanel.height)) := controlPanel, rfl,rfl⟩⟩
  for hr : row in [:controlPanel.height] do
    for hc : column in [:controlPanel.width] do
      let x : Fin result.fst.width := Fin.cast result.snd.left.symm column,hc.upper
      let y : Fin result.fst.height := Fin.cast result.snd.right.symm row,hr.upper
      let coordinate : result.fst.Coordinate := {x, y}
      have : (moveTileNorth coordinate).width = controlPanel.width  (moveTileNorth coordinate).height = controlPanel.height :=
        (moveTileNorth_same_size coordinate).right.substr $ (moveTileNorth_same_size coordinate).left.substr result.snd
      result := moveTileNorth coordinate, this
  result.fst

private def weightOnNorthSupport (controlPanel : ControlPanel) : Nat := Id.run do
  let mut score := 0
  for hr :row in [:controlPanel.height] do
    for hc : column in [:controlPanel.width] do
      let coordinate := {x := column,hc.upper, y := row,hr.upper : controlPanel.Coordinate}
      if controlPanel[coordinate] == Tile.Round then
        score := score + (controlPanel.height - row)
  score

def part1 : (input : ControlPanel)  Nat := weightOnNorthSupport  moveAllTilesNorth

------------------------------------------------------------------------------------

/-- Finds the westmost free space reachable from start. Does not look at start. -/
private def findWestmostFreeTile {controlPanel : ControlPanel} (start : controlPanel.Coordinate) : controlPanel.Coordinate :=
  if start.x > 0,controlPanel.not_empty.left then
    let westNeighbour := { start with x := start.x - 1,Nat.lt_imp_pred_lt start.x.isLt }
    match controlPanel[westNeighbour] with
    | Tile.Space => findWestmostFreeTile westNeighbour
    | Tile.Round | Tile.Cube => start
  else
    start
termination_by start.x

/-- Finds the southmost free space reachable from start. Does not look at start. -/
private def findSouthmostFreeTile {controlPanel : ControlPanel} (start : controlPanel.Coordinate) : controlPanel.Coordinate :=
  if h : start.y.val.succ < controlPanel.height then
    let southNeighbour := { start with y := start.y.val.succ,h }
    match controlPanel[southNeighbour] with
    | Tile.Space => findSouthmostFreeTile southNeighbour
    | Tile.Round | Tile.Cube => start
  else
    start

/-- Finds the eastmost free space reachable from start. Does not look at start. -/
private def findEastmostFreeTile {controlPanel : ControlPanel} (start : controlPanel.Coordinate) : controlPanel.Coordinate :=
  if h : start.x.val.succ < controlPanel.width then
    let eastNeighbour := { start with x := start.x.val.succ,h }
    match controlPanel[eastNeighbour] with
    | Tile.Space => findEastmostFreeTile eastNeighbour
    | Tile.Round | Tile.Cube => start
  else
    start

private def moveTileInDirection (direction : {c : ControlPanel}  c.Coordinate  c.Coordinate) {controlPanel : ControlPanel} (position : controlPanel.Coordinate) : ControlPanel :=
   match controlPanel[position] with
  | Tile.Space | Tile.Cube => controlPanel
  | Tile.Round =>
    let intermediate := controlPanel.set position Tile.Space
    let positionInIntermediate : intermediate.Coordinate := {x := position.x, y := position.y} -- how? well, I won't complain
    let northMost := direction positionInIntermediate
    intermediate.set northMost Tile.Round

private theorem moveTileInDirection_same_size (direction : {c : ControlPanel}  c.Coordinate  c.Coordinate) {controlPanel : ControlPanel} (position : controlPanel.Coordinate) : (moveTileInDirection direction position).width = controlPanel.width  (moveTileInDirection direction position).height = controlPanel.height := by
  unfold moveTileInDirection
  constructor <;> split <;> rfl

private def moveTileWest : {controlPanel : ControlPanel}  controlPanel.Coordinate  ControlPanel :=
  moveTileInDirection findWestmostFreeTile
private def moveTileSouth : {controlPanel : ControlPanel}  controlPanel.Coordinate  ControlPanel :=
  moveTileInDirection findSouthmostFreeTile
private def moveTileEast : {controlPanel : ControlPanel}  controlPanel.Coordinate  ControlPanel :=
  moveTileInDirection findEastmostFreeTile

private theorem moveTileWest_same_size : {controlPanel : ControlPanel}  (position : controlPanel.Coordinate)  (moveTileWest position).width = controlPanel.width  (moveTileWest position).height = controlPanel.height :=
  moveTileInDirection_same_size findWestmostFreeTile
private theorem moveTileEast_same_size : {controlPanel : ControlPanel}  (position : controlPanel.Coordinate)  (moveTileEast position).width = controlPanel.width  (moveTileEast position).height = controlPanel.height :=
  moveTileInDirection_same_size findEastmostFreeTile
private theorem moveTileSouth_same_size : {controlPanel : ControlPanel}  (position : controlPanel.Coordinate)  (moveTileSouth position).width = controlPanel.width  (moveTileSouth position).height = controlPanel.height :=
  moveTileInDirection_same_size findSouthmostFreeTile

private def moveAllTilesWest (controlPanel : ControlPanel) : ControlPanel := Id.run do
  let mut result : ((c : ControlPanel) ×' (c.width = controlPanel.width  c.height = controlPanel.height)) := controlPanel, rfl,rfl⟩⟩
  for hr : row in [:controlPanel.height] do
    for hc : column in [:controlPanel.width] do
      let x : Fin result.fst.width := Fin.cast result.snd.left.symm column,hc.upper
      let y : Fin result.fst.height := Fin.cast result.snd.right.symm row,hr.upper
      let coordinate : result.fst.Coordinate := {x, y}
      have : (moveTileWest coordinate).width = controlPanel.width  (moveTileWest coordinate).height = controlPanel.height :=
        (moveTileWest_same_size coordinate).right.substr $ (moveTileWest_same_size coordinate).left.substr result.snd
      result := moveTileWest coordinate, this
  result.fst

private def moveAllTilesSouth (controlPanel : ControlPanel) : ControlPanel := Id.run do
  let mut result : ((c : ControlPanel) ×' (c.width = controlPanel.width  c.height = controlPanel.height)) := controlPanel, rfl,rfl⟩⟩
  for row in [:controlPanel.height] do
    for hc : column in [:controlPanel.width] do
      let x : Fin result.fst.width := Fin.cast result.snd.left.symm column,hc.upper
      have : controlPanel.height - 1 - row < controlPanel.height :=
        Nat.lt_of_le_of_lt (Nat.sub_le _ _) (Nat.pred_lt (Nat.pos_iff_ne_zero.mp controlPanel.not_empty.right))
      let y : Fin result.fst.height := Fin.cast result.snd.right.symm controlPanel.height - 1 - row,this
      let coordinate : result.fst.Coordinate := {x, y}
      have : (moveTileSouth coordinate).width = controlPanel.width  (moveTileSouth coordinate).height = controlPanel.height :=
        (moveTileSouth_same_size coordinate).right.substr $ (moveTileSouth_same_size coordinate).left.substr result.snd
      result := moveTileSouth coordinate, this
  result.fst

private def moveAllTilesEast (controlPanel : ControlPanel) : ControlPanel := Id.run do
  let mut result : ((c : ControlPanel) ×' (c.width = controlPanel.width  c.height = controlPanel.height)) := controlPanel, rfl,rfl⟩⟩
  for hr : row in [:controlPanel.height] do
    for column in [:controlPanel.width] do
      have : controlPanel.width - 1 - column < controlPanel.width :=
        Nat.lt_of_le_of_lt (Nat.sub_le _ _) (Nat.pred_lt (Nat.pos_iff_ne_zero.mp controlPanel.not_empty.left))
      let x : Fin result.fst.width := Fin.cast result.snd.left.symm controlPanel.width - 1 - column,this
      let y : Fin result.fst.height := Fin.cast result.snd.right.symm row,hr.upper
      let coordinate : result.fst.Coordinate := {x, y}
      have : (moveTileEast coordinate).width = controlPanel.width  (moveTileEast coordinate).height = controlPanel.height :=
        (moveTileEast_same_size coordinate).right.substr $ (moveTileEast_same_size coordinate).left.substr result.snd
      result := moveTileEast coordinate, this
  result.fst

private def cycle (controlPanel : ControlPanel) : ControlPanel :=
  let n := moveAllTilesNorth controlPanel
  let w := moveAllTilesWest n
  let s := moveAllTilesSouth w
  moveAllTilesEast s

private def compareControlPanels (a b : ControlPanel) : Bool :=
  if h₁ : a.width = b.width  a.height = b.height then
    Id.run do
    for hr : row in [:a.height] do
      for hc : column in [:a.width] do
        let ca : a.Coordinate := {x := column, hc.upper, y := row, hr.upper}
        let cb : b.Coordinate := {x := column, h₁.left.subst hc.upper, y := row, h₁.right.subst hr.upper}
        if !(a[ca] == b[cb]) then
          return false
    return true
  else
    false

/-- (Bad) Hash-Function for ControlPanel. It's good enough for this riddle, but that's about it. -/
private def hashControlPanel (p : ControlPanel) : UInt64 := Id.run do
  let mut hash : UInt64 := mixHash Fin.ofNat' _ p.width Fin.ofNat' _ p.height
  for hi : index in [:p.elements.size] do
    match p.elements[index] with
    | Tile.Round => hash := mixHash hash Fin.ofNat' _ index
    | Tile.Cube => hash := mixHash hash Fin.ofNat' _ (index + p.elements.size)
    | Tile.Space => continue
  hash

private instance : BEq ControlPanel where
beq := compareControlPanels

private instance : Hashable ControlPanel where
hash := hashControlPanel

structure Part2Memory where
  seenHashes : Std.HashSet UInt64 -- If this becomes too large, it could be replaced by a Bloom Filter
  recordedPanels : Std.HashMap ControlPanel Nat
deriving Inhabited

private def runCycles (cycleCount : Nat) (controlPanel : ControlPanel) : StateM (Part2Memory) ControlPanel :=
  match cycleCount with
  | 0 => pure controlPanel
  | p+1 => do
    let r := cycle controlPanel
    let s  get
    let (seen, seenHashes) := s.seenHashes.containsThenInsert (hash r)
    set {s with seenHashes}
    if seen then
      let s  get
      let (maybePreviousCount, recordedPanels) := s.recordedPanels.getThenInsertIfNew? r p
      set {s with recordedPanels}
      if let some previousCount := maybePreviousCount then
        let period := previousCount - p
        let remaining := p % period
        runCycles remaining r
      else
        runCycles p r
    else
      runCycles p r
termination_by cycleCount
decreasing_by
  exact Nat.lt_succ_of_le $ Nat.mod_le _ _
  exact Nat.le_refl _

def part2 (input : ControlPanel) : Nat :=
  let r := StateT.run' (runCycles 1000000000 input) Inhabited.default
  weightOnNorthSupport r

------------------------------------------------------------------------------------

open DayPart
instance : Parse 14, by simp (ι := ControlPanel) where
  parse := Except.mapError toString  parse

instance : Part 14,_⟩ Parts.One (ι := ControlPanel) (ρ := Nat) where
  run := some  part1

instance : Part 14,_⟩ Parts.Two (ι := ControlPanel) (ρ := Nat) where
  run := some  part2

------------------------------------------------------------------------------------

private def testInput := "O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#...."

#eval parse testInput

#eval (weightOnNorthSupport  moveAllTilesNorth) <$> parse testInput

#eval part2 <$> parse testInput