Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Untitled notebook

2021-lb/day_4.livemd

Untitled notebook

Setup

Mix.install([:kino])

input = Kino.Input.textarea("Paste input here")
defmodule Day4 do
  def prepare_input(input) do
    [numbers | boards] =
      input
      |> String.split("\n\n", trim: true)

    boards =
      boards
      |> Stream.map(&String.split(&1, "\n", trim: true))
      |> Stream.map(fn board ->
        board
        |> Stream.map(&String.split(&1, " ", trim: true))
        |> Enum.reduce(&(&2 ++ &1))
        |> Stream.with_index()
        |> Stream.map(fn {n, i} ->
          col = rem(i, 5)
          row = div(i, 5)
          {{col, row}, {n, false}}
        end)
        |> Enum.into(%{})
      end)
      |> Enum.into([])

    {numbers |> String.split(","), boards}
  end

  def is_winner?(board) do
    row_won?(board) or col_won?(board)
  end

  def row_won?(board) do
    Enum.any?(0..4, fn row ->
      Enum.all?(0..4, fn col ->
        {_, marked} = Map.get(board, {col, row})
        marked
      end)
    end)
  end

  def col_won?(board) do
    Enum.any?(0..4, fn col ->
      Enum.all?(0..4, fn row ->
        {_, marked} = Map.get(board, {col, row})
        marked
      end)
    end)
  end

  def mark_board(board, n) do
    board
    |> Stream.map(fn
      {c, {^n, _}} -> {c, {n, true}}
      v -> v
    end)
    |> Enum.into(%{})
  end

  def sum_unmarked(board) do
    board
    |> Stream.filter(fn
      {_, {_, false}} -> true
      _ -> false
    end)
    |> Stream.map(fn {_, {v, _}} -> String.to_integer(v) end)
    |> Enum.sum()
  end
end
input = input |> Kino.Input.read() |> Day4.prepare_input()

Section

Part 1

{numbers, boards} = input

{winning_number, winning_board} =
  Enum.reduce_while(numbers, boards, fn n, boards ->
    marked_boards = Enum.map(boards, &Day4.mark_board(&1, n))

    case Enum.find(marked_boards, &Day4.is_winner?/1) do
      nil -> {:cont, marked_boards}
      board -> {:halt, {String.to_integer(n), board}}
    end
  end)

unmarked_sum = Day4.sum_unmarked(winning_board)

unmarked_sum * winning_number

Part 2

defmodule Part2 do
  def find_last_winner(numbers, boards, pos) do
    n = elem(numbers, pos)

    case boards
         |> Stream.map(&Day4.mark_board(&1, n))
         |> Stream.filter(&(!Day4.is_winner?(&1)))
         |> Enum.into([]) do
      # there's one board left, so it must be the last to win. Find its winning number before returning
      [last_board] -> Part1.find_first_winner(numbers, [last_board], pos + 1)
      losing_boards -> find_last_winner(numbers, losing_boards, pos + 1)
    end
  end
end
{numbers, boards} = input
{n, winning_board} = Part2.find_last_winner(numbers, boards, 0)

unmarked_sum = Day4.sum_unmarked(winning_board)

unmarked_sum * n