Advent of Code 2025 in Lean 4 – Day 12

Today’s problem looked like it would be really hard, and I had to study for an exam, so I decided to postpone it. Even after more than two weeks, my brain’s background processing yielded no breakthroughs, so today – the last day of the year – I sat down to see how far I could get.

I created the data types, wrote the parsers for the input format, using the great lean4-parser library, and started working out how many packages I can fit inside rectangular regions.

The simple case is…well, simple: Since the packages are square, we can simply count how many places there are for packages in a region. If the number of potential places is equal to or greater than the number of packages, they will fit. No tetris-ing needed. The other simple case is when even if we could somehow optimally pack the packages and they still wouldn’t fit, well, they won’t fit.

The third case is worse. Then it’s Korobeiniki time. Let’s postpone this for now:

  if numPackages <= places then
    true
  else
    if minPackagesArea > totalArea then
      false
    else
      panic! "Tetris time"

I wonder how many of these there are!

❯ lake exec day12
Part 1: 517

I…I…I’m not even mad.

And part 2? There was none! I’m done! A belated Merry Christmas to me!

Things I (re-)learned today

  • Nothing, really – only that using parser combinators is fun!

“Solution”

import Parser
import Aoc
open Aoc

structure Package (n m : Nat) where
  width : Nat := m
  length : Nat := n
  shape : Vector (Vector Char m) n
  deriving Repr

def Package.maxArea {n m : Nat} (_ : Package n m) : Nat :=
  n * m

def Package.minArea {n m : Nat} (p : Package n m) : Nat :=
  p.shape.flatten.toArray.filter (. == '#') |>.size

structure Region where
  width : Nat
  length : Nat
  packageCounts : List Nat
  deriving Repr

namespace Parser
  open Parser Char ASCII

   -- 0:
   -- ###
   -- ##.
   -- ##.
  def parsePackage (n m : Nat) : SimpleParser Substring Char (Package n m) := do
    let _index <- parseNat
    _ <- char ':'
    _ <- eol
    let rowsArray <- Array.mk <$> (List.range n).mapM λ _ => do
      let rowArray <- Array.mk <$> (List.range m).mapM λ _ => char '#' <|> char '.'
      _ <- eol
      return rowArray
    let rows := Vector.ofFn  i => Vector.ofFn  j => rowsArray[i]![j]!))
    return { shape := rows }

  -- 4x4: 0 0 0 0 2 0
  def parseRegion : SimpleParser Substring Char Region := do
    let width <- parseNat
    _ <- char 'x'
    let length <- parseNat
    _ <- char ':'
    _ <- takeMany1 whitespace
    let counts <- sepBy1 (char ' ') parseNat
    return { width := width, length := length, packageCounts := counts.toList }

  def parseInput (n m : Nat) : SimpleParser Substring Char (List (Package n m) × Array Region) := do
    let packages <- sepBy1 eol (parsePackage n m)
    _ <- takeMany1 eol
    let regions <- sepBy eol parseRegion
    return (packages.toList, regions)

end Parser

def canFit (packages : List (Package n m)) (region : Region) : Bool :=
  let places := (region.width / 3) * (region.length / 3)
  let packages := packages.zip region.packageCounts
    |>.filter  (_, count) => count > 0)
    |>.map  (p, count) => (p, count))
  let numPackages := packages.foldl  acc (_, count) => acc + count) 0
  if numPackages <= places then
    true
  else
    let totalArea := region.width * region.length
    let minPackagesArea := packages.foldl  acc (p, count) => acc + count * p.minArea) 0
    if minPackagesArea > totalArea then
      false
    else
      panic! "Tetris time"

def main : IO Unit := do
  let input <- IO.FS.readFile "Day12/input.txt"
  let (packages, regions) <- (match (Parser.parseInput 3 3).run input with
    | .ok _ result => Except.ok result
    | .error err _ => Except.error s!"{err}") |> IO.ofExcept

  let count := regions.filter  r => canFit packages r) |>.size

  -- part 1
  IO.println s!"Part 1: {count}"