From 793bc1538543bbfb48704bab071c6719c6da4aa6 Mon Sep 17 00:00:00 2001 From: Andreas Grois Date: Fri, 29 Nov 2024 22:23:40 +0100 Subject: Day14 Part 2 --- Day14.lean | 249 ++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 179 insertions(+), 70 deletions(-) (limited to 'Day14.lean') diff --git a/Day14.lean b/Day14.lean index bbaa616..d0acf6b 100644 --- a/Day14.lean +++ b/Day14.lean @@ -1,4 +1,6 @@ import «Common» +import Lean.Data.HashMap +import Lean.Data.HashSet namespace Day14 @@ -41,15 +43,6 @@ def parse : String → Except ParseInputError ControlPanel := ------------------------------------------------------------------------------------ -/- -there's probably a better way, but I'm too lazy, so I'll just move them row-by-row. -pseudocode -for(row) - for(column) - if(tile == Round) - moveTileAsFarNorthAsPossible --/ - /-- 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 @@ -71,70 +64,12 @@ private def moveTileNorth {controlPanel : ControlPanel} (position : controlPanel 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 - constructor - all_goals - unfold moveTileNorth - match controlPanel[position] with - | Tile.Space | Tile.Cube => trivial - | Tile.Round => - simp only[Parsing.RectangularGrid.set_same_size] - -/- --- Old solution, without do-notation. Proven to terminate, but needs a lot more code... - -/- had to move this to top level function to be able to prove stuff with it... "Declaration contains metavariables" -/ -private def moveAllTilesNorth_handleColumns (cp : ControlPanel) (row : Fin cp.height) (column : Fin cp.width) : ControlPanel := - let intermediate := moveTileNorth {x := column, y := row : cp.Coordinate} - let nextIndex := column.val + 1 - if h : nextIndex < intermediate.width then - have h₁ : intermediate.height = cp.height := (moveTileNorth_same_size {x := column, y := row : cp.Coordinate}).right - moveAllTilesNorth_handleColumns intermediate (h₁▸row) ⟨nextIndex,h⟩ - else - intermediate - termination_by cp.width - column - decreasing_by - have h₂ := (moveTileNorth_same_size { x := column, y := row }).left - rw[h₂] at h ⊢ - exact Nat.sub_lt_of_gt (Nat.le_of_lt h) (Nat.lt_succ.mpr $ Nat.le_refl _) - -private theorem moveAllTilesNorth_handleColumns_same_height (cp : ControlPanel) (row : Fin cp.height) (column : Fin cp.width) : (moveAllTilesNorth_handleColumns cp row column).height = cp.height := by - unfold moveAllTilesNorth_handleColumns - simp - have h₂ := (moveTileNorth_same_size { x := column, y := row }).right - split - case isFalse => exact h₂ - case isTrue => - have := moveAllTilesNorth_handleColumns_same_height (Day14.moveTileNorth { x := column, y := row }) (h₂ ▸ row) ⟨↑column + 1, by assumption⟩ - simp[h₂] at this - assumption - termination_by cp.width - column - decreasing_by - have h₂ := (moveTileNorth_same_size { x := column, y := row }).left - rename_i h - rw[h₂] at h ⊢ - exact Nat.sub_lt_of_gt (Nat.le_of_lt h) (Nat.lt_succ.mpr $ Nat.le_refl _) - -private def moveAllTilesNorth (controlPanel : ControlPanel) : ControlPanel := - handleRows (controlPanel) ⟨0,controlPanel.not_empty.right⟩ -where - handleRows (cp : ControlPanel) (row : Fin cp.height) : ControlPanel := - let intermediate := moveAllTilesNorth_handleColumns cp row ⟨0,cp.not_empty.left⟩ - let nextIndex := row.val + 1 - if h : nextIndex < intermediate.height then - handleRows intermediate ⟨nextIndex, h⟩ - else - intermediate - termination_by cp.height - row - decreasing_by - have h₁ : intermediate.height = cp.height := - moveAllTilesNorth_handleColumns_same_height cp row ⟨0,_⟩ - rw[h₁] at h ⊢ - exact Nat.sub_lt_of_gt (Nat.le_of_lt h) (Nat.lt_succ.mpr $ Nat.le_refl _) --/ + 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 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⟩ @@ -157,6 +92,175 @@ def part1 : (input : ControlPanel) → Nat := weightOnNorthSupport ∘ moveAllTi ------------------------------------------------------------------------------------ +/-- 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 @@ -164,6 +268,9 @@ instance : Parse ⟨14, by simp⟩ (ι := ControlPanel) where 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....#.... @@ -180,3 +287,5 @@ O.#..O.#.# #eval parse testInput #eval (weightOnNorthSupport ∘ moveAllTilesNorth) <$> parse testInput + +#eval part2 <$> parse testInput -- cgit v1.2.3