Advent of Code 2021: Day 3

Andrew Fontaine <andrew@afontaine.ca>

Okay, the submarine is moving, we can see where we’re going. Now to make sure systems are ✔.

Today’s struggle in learning was motivation. Fridays are tricky that way, sometimes it’s a mad rush to get everything done in time for Saturday, others it’s just a slog to the weekend. Today was the latter.

I found the first problem’s trickiest bit to be how to parse the list properly. Only after eating dinner and not being distracted by hunger did the solution become obvious.

Parsing blues

First, I make a bit type:

(* bits are one or zero *)
type bit = One | Zero

(* parse a character into a bit *)
let bit_of_char c =
  match c with '1' -> One | '0' -> Zero | c -> raise (BadBit c)

(* turn a bit into a string for combining later *)
let string_of_bit b =
  match b with
  | One -> "1"
  | Zero -> "0"

(* take a list of bits and turn that into a number *)
let int_of_bit r =
  List.fold_left (fun b x -> b ^ (string_of_bit x)) "0b" r
  |> int_of_string

Next, I map the list of codes from the rows to the columns:

let input =  Core.In_channel.read_lines "./priv/2021/D03"
                |> List.map (fun row -> List.map bit_of_char (Core.String.to_list row))

let p1_input =
  match input with
  | [] -> raise (BadBit 'l')
  | hd :: t -> List.fold_left (fun result row ->
      List.map2 (fun r bit ->  bit :: r) result row) (List.map (fun x -> [x]) hd) t

Ocaml has the lovely map2 function to map 2 lists together. I inject the first row, split into lists of single bits, to start the rows off. Then, I fold over the rest of the rows, adding each character to the list in the correct column. The end result:

[1, 2, 3]         [1, 4, 7]
[4, 5, 6] becomes [2, 5, 8]
[7, 8, 9]         [3, 6, 9]

The rest of the problem is simple. Mark the most common bit in each column, and multiply the resulting final numbers together:

let counts row =
  List.fold_left
    (fun (one, zero) b ->
      match b with One -> (one + 1, zero) | Zero -> (one, zero + 1))
    (0, 0) row

let greatest_bit (one, zero) = if one >= zero then One else Zero

let p1_sol input =
  let g, e =
    List.fold_left
      (fun (g, e) row ->
        let c = counts row in
        match greatest_bit c with
        | One -> (g ^ "1", e ^ "0")
        | Zero -> (g ^ "0", e ^ "1"))
      ("0b", "0b") input
  in
  (int_of_string g, int_of_string e)

let p1 =
  let g, e = p1_sol p1_input in
  let sol = g * e in
  Int.to_string sol

⭐ one done!

Problem 2

Problem 2, while similar, certainly starts to raise the difficultly. I fear for the weekend challenges now!

In essence, I am seeking for a specific row. That row needs to match a set of rules, where, for one, we chase the most common bit, and the other, the least common. It took me a few re-reads before I understood the problem at hand. This one is fun, in that there’s a good amount of re-usable code between both seeking:

let consider f input =
  let firsts = List.map (fun r -> match r with
      | ([], _) -> raise (BadBit 'l')
      | (h :: _, _) -> h) input in
  let bit = greatest_bit (counts firsts) in
  List.filter (fun r -> match r with
      | ([], _) -> raise (BadBit 'l')
      | (h :: _, _) -> f bit h) input
  |> List.map (fun (r, i) -> match r with
      | [] -> raise (BadBit 'c')
      | _ :: t -> (t, i))

let oxygen bit h = bit = h

let co2 bit h = bit <> h

let rec find crit input =
  match input with
  | [] -> -1
  | [(_, x)] -> x
  | list -> let results = consider crit list in find crit results

let compute input crit =
  let i = List.mapi (fun x r -> (r, x)) input in
  find crit i
  |> List.nth input
  |> int_of_bit

let p2_sol input =
  let co2_result = compute input co2 in
  let oxygen_result = compute input oxygen in
  string_of_int (co2_result * oxygen_result)

let p2 = p2_sol input

find is the main entry point into the interesting stuff. It recursively seeks the index of the row that matches the specific criteria. consider takes the criteria, puts together the first column of numbers, and determines which rows make the cut. It also lops off the considered column, so the recursive find call eventually goes through all the columns. The criteria are encoded as oxygen (the most common bit) and co2 (the least common bit). Once the right index is found, its full row is retrieved and parsed into a number to find the final solution.

This one I found the parsing to be the most painful part. Once I was over that hurdle, it was smooth sailing. Eventually I hit a few bumps. There’s an extra rule in part 2 where One wins tie breakers for the most common bit that I missed, and it took some puzzling to realize I wasn’t lopping off the column in the beginning. Once those two issues were caught, the ⭐ was mine.

Now if you’ll excuse me, Day 4 just unlocked, and I better see how much of a crunch my weekend will be.


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