Zulip Chat Archive

Stream: general

Topic: Advent of Code 2024 - Day 6


Ayhon (Dec 08 2024 at 18:53):

I haven't seen any thread/channel dedicated to AoC day 6 solutions and later. It's a shame, becuase I really enjoyed the discussions that happened in them and reading the code that people shared. That's why I share my version in hope others may join me in sharing theirs.

I'm not super happy with my solution, but it definitely works. I think I could simplify a bit the definitions. I'm looking forward to reading other's solutions

import Std.Internal.Parsec
import Std.Internal.Parsec.String
import Std.Data.HashSet

namespace Day6
open Std.Internal.Parsec
open Std.Internal.Parsec.String


inductive Direction where
| Up
| Right
| Down
| Left
deriving Inhabited, Repr, BEq, Hashable

def Direction.turn: Direction -> Direction
| Up => Right
| Right => Down
| Down => Left
| Left => Up

inductive Cell where
| Obstacle
| Nothing
| Guard
deriving Inhabited, Repr

abbrev Position := Nat×Nat
structure Guard where
  pos: Position
  dir: Direction
  deriving Inhabited, Repr, BEq, Hashable

abbrev Grid := Array (Array Cell)

def Grid.width(g: Grid) := if h: 0 < g.size then (g[0]'h).size else 0
def Grid.height(g: Grid) := g.size

def Grid.get!(grid: Grid)(pos: Position) := Id.run do
  let (x,y) := pos
  let some row := grid[x]? | panic s!"Tried to access out of bounds x={x}"
  let some cell := row[y]? | panic s!"Tried to access out of bounds y={y}"
  cell

structure Lab where
  grid: Grid
  guard: Guard
deriving Inhabited

instance instReprLab:  Repr Lab where
  reprPrec lab _ := lab.grid.toList
    |>.map (fun row => row.toList
      |>.map (fun
        | Cell.Nothing => "░"
        | Cell.Guard => match lab.guard.dir with
          | Direction.Up => "^"
          | Direction.Left => "<"
          | Direction.Right => ">"
          | Direction.Down => "v"
        | Cell.Obstacle => "#"
        )
      |> String.join
    )
    |> String.intercalate "\n"

def List.enum' [Inhabited α](ls: List α) :=
  if ls.isEmpty then []
  else
    let rec enumFrom(n: Nat)(s: Nat)(ls: List α)(inv: s + ls.length = n): List (Fin n  × α)
    := match ls with
      | [] => []
      | x :: xs =>
        let m := ls.length
        let index: Fin n := by
          constructor <;> try exact s
          rw [<-inv]
          have h: 0 < (x :: xs).length := by
            rw [List.length_cons]
            exact Nat.zero_lt_succ xs.length
          apply Nat.lt_add_of_pos_right h
        have inv':  s + 1 + xs.length = n := by
          rw [Nat.add_assoc, Nat.add_comm 1,<-List.length_cons x]
          exact inv
        let rest := enumFrom n (s + 1) xs inv'
        (index, x) :: rest
    enumFrom ls.length 0 ls (by simp)

def Lab.fromGrid(grid: Grid): Option Lab := do
  let guard <- grid.toList
      |>.enum
      |>.findSome? (fun (i, row) =>
        row.toList
          |>.enum
          |>.findSome? fun (j, cell) =>
            match cell with
            | Cell.Guard => some (i,j), Direction.Up
            | _ => none
    )
  return {grid := grid, guard := guard : Lab}


abbrev Input := Lab

def parse_input(content: String): Option (Input) := do
  let p_cell := do
    pchar '.' *> pure Cell.Nothing <|>
    pchar '#' *> pure Cell.Obstacle <|>
    pchar '^' *> pure Cell.Guard
  let parser := many ((many p_cell) <* skipChar '\n')
  let .success _ grid := parser content.iter | failure
  (<- Lab.fromGrid grid)

def andThen(a: α)(b: Prop)[Decidable b]: Option α := if b then some a else none
def Grid.ahead (grid: Grid)(guard: Guard): Option Position := do
    let (w,h) := (grid.width, grid.height)
    let (x,y) := guard.pos
    match guard.dir with
      | Direction.Up    => (x  > 0)  |> andThen (x-1, y)
      | Direction.Right => (y+1 < w) |> andThen (x, y+1)
      | Direction.Down  => (x+1 < h) |> andThen (x+1, y)
      | Direction.Left  => (y  > 0)  |> andThen (x, y-1)

def Lab.toString (lab: Lab)(positions: Std.HashSet Position) := List.range lab.grid.height
  |>.map (fun i =>
  List.range lab.grid.width
    |>.map (fun j => if positions.contains (i,j) then "X" else "░")
    |> String.join
  )
  |> String.intercalate "\n"

def Lab.newGuardPosition(lab: Lab): Option Guard := getPosition lab 4 where
  getPosition lab
  | 0 => none
  | limit+1 => do
    let (nx,ny) <- lab.grid.ahead lab.guard
    let cell <- (<- lab.grid[nx]?)[ny]?
    match cell with
    | Cell.Obstacle =>
      getPosition {lab with guard := {lab.guard with dir := lab.guard.dir.turn}} limit
    | _ => some {lab.guard with pos := (nx,ny)}

def Lab.simulate(lab: Lab): Option Lab := do
  let newGuard <- Lab.newGuardPosition lab
  let (x,y) := lab.guard.pos
  let (nx,ny) := newGuard.pos
  return {lab with
    grid := lab.grid
      |>.modify x (·.set! y Cell.Nothing)
      |>.modify nx (·.set! ny (Cell.Guard))
    guard := newGuard
  }

namespace Part1
  def solve'(lab: Lab): Int :=  go (4*lab.grid.width*lab.grid.height) {lab.guard.pos} lab where
    go: Nat -> Std.HashSet Position -> Lab -> Int
    | 0, _, _ => -1
    | fuel+1, positions, lab =>
      match Lab.simulate lab with
      | some newLab =>
        go fuel (positions.insert newLab.guard.pos) newLab
      | none => Id.run do
        Int.ofNat positions.size

  partial def solve: Option (Input) -> Int
  | .some input => solve' input
  | .none => -1
end Part1

namespace Part2
  partial def hasCycle(lab: Lab): Bool := go {lab.guard} lab where
    go : Std.HashSet Guard -> Lab -> Bool
    | seenConfigurations, lab => Id.run do
      let some newLab := lab.simulate | false
      let hasCycle := seenConfigurations.contains newLab.guard
      hasCycle || go (seenConfigurations.insert newLab.guard) newLab

  partial def solve'(lab: Lab): Int :=
    lab.grid.toList
      |> List.enum'
      |>.map (fun (i, row) =>
        row.toList
          |> List.enum'
          |>.countP fun (j, c) => match c with
            | Cell.Nothing =>
              let modLab := {lab with grid := lab.grid.set i (row.set j Cell.Obstacle)}
              let res := hasCycle modLab
              res
            | _ => false
      )
      |>.sum

  partial def solve: Option (Input) -> Int
  | .some input => solve' input
  | .none => -1
end Part2

Ayhon (Dec 08 2024 at 18:54):

I'm a bit behind on AoC problems, but once I've solved day 7 and day 8 I'm thinking of posting my solutions. If someone wants to beat me to it, please, feel free to do so!

Matt Russell (Dec 08 2024 at 20:26):

Thanks for posting! I wasn't super happy with my Day 6 - https://github.com/mdr/aoc-2024/blob/main/Aoc2024/Day06/Solve.lean
I thought I'd mess around with the state monad, but I think it just made it more complex than a more direct approach in the end.
Also needing to pass around the right size fuel felt silly, I think I need to start learning about how to prove termination to Lean (I need to learn about proving things in Lean full stop really!)

Ayhon (Dec 08 2024 at 21:10):

Actually, I found your solution from the link you posted for Day 5! I saw it had some wip as the commit message that introduced it so I assumed that it wasn't finished yet.

I liked the idea of having a set of obstacles and bounds and doing away with the grid, I believe it makes things clearer. I also share your opinion on fuel. My prior solution was to add partial all over the place, but while the code is more readable, I do think it's against Lean's spirit, so to say.

I imagine there could be a monad though, in order not to have to pass fuel directly all the time. Something like

inductive StepBound (a: Type): Nat -> Type where
| TimedOut : StepBound a 0
| Finished (val: a): StepBound a steps
| StillRunning(n: Nat)(cont : Unit => StepBound a n): StepBound a n

That might be a cool exercise for monads and monad transformers, I'll try to formalize it a bit more once I find the time!

Greg Shuflin (Dec 13 2024 at 00:35):

Ayhon said:

I haven't seen any thread/channel dedicated to AoC day 6 solutions and later. It's a shame, becuase I really enjoyed the discussions that happened in them and reading the code that people shared. That's why I share my version in hope others may join me in sharing theirs.

I'm not super happy with my solution, but it definitely works. I think I could simplify a bit the definitions. I'm looking forward to reading other's solutions

import Std.Internal.Parsec
import Std.Internal.Parsec.String
import Std.Data.HashSet

namespace Day6
open Std.Internal.Parsec
open Std.Internal.Parsec.String


inductive Direction where
| Up
| Right
| Down
| Left
deriving Inhabited, Repr, BEq, Hashable

def Direction.turn: Direction -> Direction
| Up => Right
| Right => Down
| Down => Left
| Left => Up

inductive Cell where
| Obstacle
| Nothing
| Guard
deriving Inhabited, Repr

abbrev Position := Nat×Nat
structure Guard where
  pos: Position
  dir: Direction
  deriving Inhabited, Repr, BEq, Hashable

abbrev Grid := Array (Array Cell)

def Grid.width(g: Grid) := if h: 0 < g.size then (g[0]'h).size else 0
def Grid.height(g: Grid) := g.size

def Grid.get!(grid: Grid)(pos: Position) := Id.run do
  let (x,y) := pos
  let some row := grid[x]? | panic s!"Tried to access out of bounds x={x}"
  let some cell := row[y]? | panic s!"Tried to access out of bounds y={y}"
  cell

structure Lab where
  grid: Grid
  guard: Guard
deriving Inhabited

instance instReprLab:  Repr Lab where
  reprPrec lab _ := lab.grid.toList
    |>.map (fun row => row.toList
      |>.map (fun
        | Cell.Nothing => "░"
        | Cell.Guard => match lab.guard.dir with
          | Direction.Up => "^"
          | Direction.Left => "<"
          | Direction.Right => ">"
          | Direction.Down => "v"
        | Cell.Obstacle => "#"
        )
      |> String.join
    )
    |> String.intercalate "\n"

def List.enum' [Inhabited α](ls: List α) :=
  if ls.isEmpty then []
  else
    let rec enumFrom(n: Nat)(s: Nat)(ls: List α)(inv: s + ls.length = n): List (Fin n  × α)
    := match ls with
      | [] => []
      | x :: xs =>
        let m := ls.length
        let index: Fin n := by
          constructor <;> try exact s
          rw [<-inv]
          have h: 0 < (x :: xs).length := by
            rw [List.length_cons]
            exact Nat.zero_lt_succ xs.length
          apply Nat.lt_add_of_pos_right h
        have inv':  s + 1 + xs.length = n := by
          rw [Nat.add_assoc, Nat.add_comm 1,<-List.length_cons x]
          exact inv
        let rest := enumFrom n (s + 1) xs inv'
        (index, x) :: rest
    enumFrom ls.length 0 ls (by simp)

def Lab.fromGrid(grid: Grid): Option Lab := do
  let guard <- grid.toList
      |>.enum
      |>.findSome? (fun (i, row) =>
        row.toList
          |>.enum
          |>.findSome? fun (j, cell) =>
            match cell with
            | Cell.Guard => some (i,j), Direction.Up
            | _ => none
    )
  return {grid := grid, guard := guard : Lab}


abbrev Input := Lab

def parse_input(content: String): Option (Input) := do
  let p_cell := do
    pchar '.' *> pure Cell.Nothing <|>
    pchar '#' *> pure Cell.Obstacle <|>
    pchar '^' *> pure Cell.Guard
  let parser := many ((many p_cell) <* skipChar '\n')
  let .success _ grid := parser content.iter | failure
  (<- Lab.fromGrid grid)

def andThen(a: α)(b: Prop)[Decidable b]: Option α := if b then some a else none
def Grid.ahead (grid: Grid)(guard: Guard): Option Position := do
    let (w,h) := (grid.width, grid.height)
    let (x,y) := guard.pos
    match guard.dir with
      | Direction.Up    => (x  > 0)  |> andThen (x-1, y)
      | Direction.Right => (y+1 < w) |> andThen (x, y+1)
      | Direction.Down  => (x+1 < h) |> andThen (x+1, y)
      | Direction.Left  => (y  > 0)  |> andThen (x, y-1)

def Lab.toString (lab: Lab)(positions: Std.HashSet Position) := List.range lab.grid.height
  |>.map (fun i =>
  List.range lab.grid.width
    |>.map (fun j => if positions.contains (i,j) then "X" else "░")
    |> String.join
  )
  |> String.intercalate "\n"

def Lab.newGuardPosition(lab: Lab): Option Guard := getPosition lab 4 where
  getPosition lab
  | 0 => none
  | limit+1 => do
    let (nx,ny) <- lab.grid.ahead lab.guard
    let cell <- (<- lab.grid[nx]?)[ny]?
    match cell with
    | Cell.Obstacle =>
      getPosition {lab with guard := {lab.guard with dir := lab.guard.dir.turn}} limit
    | _ => some {lab.guard with pos := (nx,ny)}

def Lab.simulate(lab: Lab): Option Lab := do
  let newGuard <- Lab.newGuardPosition lab
  let (x,y) := lab.guard.pos
  let (nx,ny) := newGuard.pos
  return {lab with
    grid := lab.grid
      |>.modify x (·.set! y Cell.Nothing)
      |>.modify nx (·.set! ny (Cell.Guard))
    guard := newGuard
  }

namespace Part1
  def solve'(lab: Lab): Int :=  go (4*lab.grid.width*lab.grid.height) {lab.guard.pos} lab where
    go: Nat -> Std.HashSet Position -> Lab -> Int
    | 0, _, _ => -1
    | fuel+1, positions, lab =>
      match Lab.simulate lab with
      | some newLab =>
        go fuel (positions.insert newLab.guard.pos) newLab
      | none => Id.run do
        Int.ofNat positions.size

  partial def solve: Option (Input) -> Int
  | .some input => solve' input
  | .none => -1
end Part1

namespace Part2
  partial def hasCycle(lab: Lab): Bool := go {lab.guard} lab where
    go : Std.HashSet Guard -> Lab -> Bool
    | seenConfigurations, lab => Id.run do
      let some newLab := lab.simulate | false
      let hasCycle := seenConfigurations.contains newLab.guard
      hasCycle || go (seenConfigurations.insert newLab.guard) newLab

  partial def solve'(lab: Lab): Int :=
    lab.grid.toList
      |> List.enum'
      |>.map (fun (i, row) =>
        row.toList
          |> List.enum'
          |>.countP fun (j, c) => match c with
            | Cell.Nothing =>
              let modLab := {lab with grid := lab.grid.set i (row.set j Cell.Obstacle)}
              let res := hasCycle modLab
              res
            | _ => false
      )
      |>.sum

  partial def solve: Option (Input) -> Int
  | .some input => solve' input
  | .none => -1
end Part2

is there a reason you defined andThen instead of just using the built-in >>= operator?

Ayhon (Dec 13 2024 at 00:49):

If I understood correctly, >>= is the same as the monadic bind. But then, my andThen doesn't have the same signature, since its first parameter is a Bool and not Option a. I was more thinking of Rust's bool::then, and since I couldn't find something that fit the signature in Loogle I coded my own

Ayhon (Dec 13 2024 at 00:50):

@Greg Shuflin, does this answer your question? I haven't actually tested whether >>= can be used in this case, but assumed it wouldn't type check

Greg Shuflin (Dec 13 2024 at 01:37):

Ayhon said:

Greg Shuflin, does this answer your question? I haven't actually tested whether >>= can be used in this case, but assumed it wouldn't type check

ah yeah, I misunderstood at first glance how your andThen was different from >>=

Paul Nelson (Dec 29 2024 at 10:48):

@Ayhon How long does your code take on the actual 101*103 input for Part 2? When I try your code (or my own) on that part, it takes several minutes.

Ayhon (Dec 29 2024 at 11:23):

This is what I get when I run time lake exe aoc24 for day 6

  aoc24 git:(main)  time lake exe aoc24
Part 1: 5318
Part 2: 1831
lake exe aoc24  7,89s user 0,22s system 100% cpu 8,049 total

Ayhon (Dec 29 2024 at 11:25):

It may be that I have a reasonably new laptop. I'm using an AMD Ryzen 7 7840U CPU

Paul Nelson (Dec 29 2024 at 15:14):

Thanks, good tip -- I didn't know about lake exe and hadn't thought to look into compiling :). Runs fast on mine now too

Paul Nelson (Dec 29 2024 at 15:15):

Here's mine

import Lean.Data.HashSet

inductive Direction where
  | Up | Right | Down | Left
  deriving BEq, Hashable, Repr

instance : Inhabited Direction where
  default := Direction.Up

def Direction.next (d : Direction) : Direction :=
  let dirs := #[Up, Right, Down, Left]
  let i := (dirs.findIdx? (· == d)).get!
  dirs[(i + 1) % 4]!

abbrev Point := Nat × Nat

def step (x_max y_max : Nat) (pos : Point) (dir : Direction) : Option Point :=
  match dir, pos with
  | .Up,    x, y => if x > 0 then some x - 1, y else none
  | .Down,  x, y => if x < x_max then some x + 1, y else none
  | .Left,  x, y => if y > 0 then some x, y - 1 else none
  | .Right, x, y => if y < y_max then some x, y + 1 else none

abbrev Grid := Array (Array Char)

structure Input where
  grid : Grid
  start : Point

def Array.enum (ar : Array α) : Array (Nat × α) :=
  Array.mapIdx (fun i x => (i, x)) ar

def findInGrid (grid : Grid) (char : Char) : Option Point :=
  grid.enum.findSome? fun (i, row) =>
    row.enum.findSome? fun (j, c) =>
      if c == char then some i, j else none

def parse (input : String) : Input :=
  let grid := input.splitOn "\n"
    |>.filter (·.length > 0)
    |>.map String.toList
    |>.map List.toArray
    |>.toArray
  let start := findInGrid grid '^' |>.get!
   grid, start 

structure State where
  pos : Point
  dir : Direction
  deriving BEq, Hashable, Repr, Inhabited

def facingObstacle (grid : Grid) (pos : Point) (dir : Direction) : Bool :=
  let next_pos := step (grid.size - 1) (grid[0]!.size - 1) pos dir
  match next_pos with
  | none => false
  | some next_pos => grid[next_pos.1]![next_pos.2]! == '#'

def State.next (grid : Grid) (state : State) : Option State :=
  let pos, dir := state
  if facingObstacle grid pos dir then
    some pos, dir.next
  else
    step (grid.size - 1) (grid[0]!.size - 1) pos dir
      |>.map (fun p => p, dir)

def exitPath (grid : Grid) (start : State) : Option (Array State) :=
  let max_states := 4 * grid.size * grid[0]!.size
  let rec go (fuel : Nat) (seen : Std.HashSet State) (path : Array State)
      (current : State) : Option (Array State) :=
    match fuel with
    | 0 => panic! "Exceeded maximum possible states"
    | fuel+1 =>
      if seen.contains current then
        none
      else
        let path := path.push current
        match current.next grid with
        | none => some path
        | some next => go fuel (seen.insert current) path next
  go max_states Std.HashSet.empty #[] start

def part1 (input : Input) : Nat :=
  exitPath input.grid input.start, Direction.Up
  |>.getD (panic! "Unexpected cycle detected")
  |>.map (·.pos)
  |>.foldl (·.insert ·) Std.HashSet.empty
  |>.size

def windows2 {α : Type} (xs : List α) : List (α × α) :=
  xs.zip xs.tail

def part2 (input : Input) : Nat :=
  let path := match exitPath input.grid input.start, Direction.Up with
    | none => panic! "Unexpected cycle detected"
    | some path => path
  -- Collect (state, position) pairs where position obstructs state.
  let trials :=
    let rec go (pairs : List (State × State))
        (seen : Std.HashSet Point) : List (State × Point) :=
      match pairs with
      | [] => []
      | (before, after) :: rest =>
        if seen.contains after.pos || after.pos == input.start then
          go rest seen
        else
          (before, after.pos) :: go rest (seen.insert after.pos)
    go (windows2 path.toList) Std.HashSet.empty
  trials |>.filter (fun (start, barrier) =>
      let grid := input.grid.modify barrier.1 (fun a => a.set! barrier.2 '#')
        match exitPath grid start with
        | none => true
        | _ => false)
    |>.length

Last updated: May 02 2025 at 03:31 UTC