Advent of Code 2021: Day 8 & 9

Andrew Fontaine <andrew@afontaine.ca>

Another multi-day combo post! I didn’t get to start day 8 until late, and finished it with 10 minutes to spare. Thus, no time to write a post yesterday.

Day 8

These elves need to maintain their stuff more. It seems the whale problem has left the seven-segment display busted.

Now… I know where this is going, and it’s going to be awful. Let’s play along though, all we have to do is find all the easy digits.

Parsing is a bit of a mess, so I’ll skip that. The end result is pretty simple though:

let find_easy list =
  List.filter
    (fun n ->
      let len = String.length n in
      len = 2 || len = 4 || len = 3 || len = 7)
    list

let p1 =
  List.map (fun (_, o) -> find_easy o) input
  |> List.flatten |> List.length |> string_of_int

All I do is check to see if the length of the pattern matches the length of a 1, 4, 7, or 8, and count the number of times those appear.

⭐ one done!

Part 2

Of course they want us to solve for all the wires.

This code is a mess, so I’m just going to start at the top and work my way in.

let group patterns =
  List.map
    (fun p ->
      match String.length p with
      | 2 -> (p, [ 1 ])
      | 3 -> (p, [ 7 ])
      | 4 -> (p, [ 4 ])
      | 5 -> (p, [ 2; 3; 5 ])
      | 6 -> (p, [ 0; 6; 9 ])
      | 7 -> (p, [ 8 ])
      | _ -> raise (BadPattern p))
    patterns

let initial_guess numbers map char =
  let pos =
    List.map
      (function
        | 0 -> [ 'a'; 'b'; 'c'; 'e'; 'f'; 'g' ]
        | 1 -> [ 'c'; 'f' ]
        | 2 -> [ 'a'; 'c'; 'd'; 'e'; 'g' ]
        | 3 -> [ 'a'; 'c'; 'd'; 'f'; 'g' ]
        | 4 -> [ 'b'; 'c'; 'd'; 'f' ]
        | 5 -> [ 'a'; 'b'; 'd'; 'f'; 'g' ]
        | 6 -> [ 'a'; 'b'; 'd'; 'e'; 'f'; 'g' ]
        | 7 -> [ 'a'; 'c'; 'f' ]
        | 8 -> [ 'a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g' ]
        | 9 -> [ 'a'; 'b'; 'c'; 'd'; 'f'; 'g' ]
        | i -> raise (BadNumber i))
      numbers
    |> List.flatten
  in
  let possibilities = CharSet.of_list pos in
  CharMap.update char
    (function
      | None -> Some possibilities
      | Some pos -> Some (CharSet.inter pos possibilities))
    map

let rec construct map = function
  | [] -> map
  | (pattern, numbers) :: rest ->
      let chars = Core.String.to_list pattern in
      let m = List.fold_left (initial_guess numbers) map chars in
      construct m rest

First, I need to parse the values into something workable. I want to turn the patterns into a map of sets. Each character in the patterns maps to a set containing the possibilities of what that character could actually be, so for the pattern ab, each character would point to cf, as they could be either segment making up the 1.

Once that’s done, just solve 🤷

First, I need some code to actually check if the result I found is valid.

let valid_patterns =
  [
    [ 'a'; 'b'; 'c'; 'e'; 'f'; 'g' ];
    [ 'c'; 'f' ];
    [ 'a'; 'c'; 'd'; 'e'; 'g' ];
    [ 'a'; 'c'; 'd'; 'f'; 'g' ];
    [ 'b'; 'c'; 'd'; 'f' ];
    [ 'a'; 'b'; 'd'; 'f'; 'g' ];
    [ 'a'; 'b'; 'd'; 'e'; 'f'; 'g' ];
    [ 'a'; 'c'; 'f' ];
    [ 'a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g' ];
    [ 'a'; 'b'; 'c'; 'd'; 'f'; 'g' ];
  ]

let is_number pattern result =
  let num = List.map (fun x -> CharMap.find x result) pattern in
  List.exists (fun n -> List.for_all (fun x -> List.mem x num) n) valid_patterns

let result map =
  CharMap.bindings map
  |> List.map (fun (k, v) -> (k, List.hd (CharSet.elements v)))
  |> List.to_seq |> CharMap.of_seq

let check_patterns patterns map =
  let r = result map in
  List.for_all (fun p -> is_number p r) patterns

let solved patterns map =
  if
    CharMap.for_all (fun _ v -> CharSet.cardinal v = 1) map
    && check_patterns patterns map
  then Some (result map)
  else None

Here, each map needs to be pointing to a single character, and I check to make sure each number is possible given the mapping I’ve found.

Next, I need some code that actually comes up with a solution:

let rec solver patterns map =
  match
    CharMap.bindings map |> List.filter (fun (_, v) -> CharSet.cardinal v <> 1)
  with
  | [] -> solved patterns map
  | ms ->
      let key, guesses =
        List.fold_left
          (fun (k1, g1) (k2, g2) ->
            if
              CharSet.cardinal g1 < CharSet.cardinal g2
              && CharSet.cardinal g1 <> 1
            then (k1, g1)
            else (k2, g2))
          (List.hd ms) (List.tl ms)
      in
      List.find_map
        (fun c ->
          let new_map =
            CharMap.mapi
              (fun k v ->
                if k = key then CharSet.singleton c else CharSet.remove c v)
              map
          in
          match solver patterns new_map with None -> None | Some m -> Some m)
        (CharSet.elements guesses)

Okay, here we go. First, I only want all the “not solved” characters. If I don’t have any “not solved” characters, I have a solution! I check if it is correct.

If I’m not done yet… I find the “most solved” character. The one with the least number of options.

Once I pick one, for each of its possible solutions, I construct a state assuming I am correct.

I take that possible solution and pass it back into my solver function. This continues until the first conditional is met and I check if I am right. If not, I construct a state with the next possible solution. Eventually, I am right.

The rest of the code is taking the solution, decoding the output, and finding the final answer:

let decode_code result code =
  let pattern = List.map (fun x -> CharMap.find x result) code in
  let numbers = List.mapi (fun i n -> (i, n)) valid_patterns in
  List.find_map
    (fun (i, n) ->
      if
        List.length pattern = List.length n
        && List.for_all (fun x -> List.mem x n) pattern
      then Some i
      else None)
    numbers

let decode codes result =
  codes |> List.map Core.String.to_list |> List.map (decode_code result)

let compute patterns codes =
  let map = patterns |> group |> construct CharMap.empty in
  let p = List.map Core.String.to_list patterns in
  match solver p map with
  | None -> [ None ]
  | Some result -> decode codes result

let p2 =
  List.fold_left
    (fun s (patterns, codes) ->
      let r = compute patterns codes in
      let n =
        List.map
          (function
            | None -> raise (BadPattern "BLAH")
            | Some i -> char_of_int (i + zero))
          r
        |> Core.String.of_char_list
      in
      int_of_string n + s)
    0 input
  |> string_of_int

That’s day eight ✔️

Day 9

Today was slightly easier, thankfully. This isn’t being written 10 minutes to midnight.

It turns out those case are lava tubes. yay.

The first problem is to find the lowest points of the caves, to avoid the smoke easier. I must find all the points where each adjacent point is higher.

Once parsing the output is out of the way, the solution is simple:

type coords = int * int

module Coords = struct
  type t = coords

  let compare (x1, y1) (x2, y2) =
    if x1 < x2 then -1
    else if x1 > x2 then 1
    else if y1 < y2 then -1
    else if y1 > y2 then 1
    else 0
end

module TupleMap = Map.Make (Coords)

let parse =
  let width = List.hd input |> List.length in
  let height = List.length input in
  let with_index =
    List.mapi (fun i r -> (i, List.mapi (fun j c -> (j, c)) r)) input
  in
  ( width,
    height,
    List.fold_left
      (fun m (i, r) ->
        List.fold_left (fun n (j, c) -> TupleMap.add (i, j) c n) m r)
      TupleMap.empty with_index )

let low_points (width, height, map) =
  TupleMap.filter
    (fun (i, j) c ->
      let n = if i = 0 then 9 else TupleMap.find (i - 1, j) map in
      let s = if i = height - 1 then 9 else TupleMap.find (i + 1, j) map in
      let w = if j = 0 then 9 else TupleMap.find (i, j - 1) map in
      let e = if j = width - 1 then 9 else TupleMap.find (i, j + 1) map in
      c < n && c < s && c < e && c < w)
    map

let p1 =
  let results = low_points parse in
  TupleMap.fold (fun _ c sum -> c + 1 + sum) results 0 |> string_of_int

I take the integers and put them into a map where the coordinate of the point points to the height of the point. Then, I filter through all the points, checking for bounds, and ensuring all adjacent points are higher. Once the lower points are found, I calculate the “total risk value”.

⭐ one done!

Part 2

Oh, so the low points make up basins. I think it’s pretty amazing how they manage to create the input required for these problems. A basin is a group of points bordered by the boundary or the highest point (a 9).

Good thing I have code that already finds the lowest points! I’m going to need a set to better keep track of what is in the basin, but the rest didn’t take too much to work out:

module TupleSet = Set.Make (Coords)

let rec find_basin width height m (i, j) acc =
  if TupleMap.find (i, j) m = 9 then acc
  else if TupleSet.mem (i, j) acc then acc
  else
    let new_acc = TupleSet.add (i, j) acc in
    let n =
      if i <> 0 then find_basin width height m (i - 1, j) new_acc else new_acc
    in
    let s =
      if i + 1 < height then find_basin width height m (i + 1, j) n else n
    in
    let w = if j <> 0 then find_basin width height m (i, j - 1) s else s in
    let e =
      if j + 1 < width then find_basin width height m (i, j + 1) w else w
    in
    TupleSet.union n (TupleSet.union s (TupleSet.union w e))

let basins =
  let w, h, m = parse in
  let finder = find_basin w h m in
  let low = low_points (w, h, m) in
  TupleMap.mapi (fun (i, j) _ -> finder (i, j) TupleSet.empty) low

let p2 =
  let results = basins in
  TupleMap.fold (fun _ v m -> TupleSet.cardinal v :: m) results []
  |> List.sort (fun x y -> y - x)
  |> List.filteri (fun i _ -> i < 3)
  |> List.fold_left (fun x y -> x * y) 1
  |> string_of_int

For each of the lowest points, I recursively map out the area surrounding the point, adding it to the set containing all the points of the basin. If I hit a boundary or a ‘9’, I stop.

Once all the basins are found, the solution is a matter of finding the 3 biggest, and finding the product of them.

I think problems like this one are a lot of fun. It’s a great use of recursion, and a set makes it easy to keep track of where I’ve visited.

Solvers like in day 8 are fun, too, but it makes such a mess of code. I am sure it could be cleaner somehow, but 10 minutes to midnight is not the time for cleanup 😅


Want to discuss this post?

Reach out via email to ~afontaine/blog-discuss@lists.sr.ht, and be sure to follow the mailing list etiquette.

Other posts