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
292
293
294
295
|
import «Common»
import Std.Data.HashMap
import Std.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
/-- Helper for runCycles. No point in keeping a memory once we have found a cycle. -/
private def runCycles_bruteForce (cycleCount : Nat) (controlPanel : ControlPanel) : ControlPanel :=
match cycleCount with
| 0 => controlPanel
| p+1 =>
let r := cycle controlPanel
runCycles_bruteForce p r
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
pure $ runCycles_bruteForce remaining r
else
runCycles p r
else
runCycles p r
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
|