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

Advent of Code Day 4

day_04/day_four.livemd

Advent of Code Day 4

Setup

test_input = """
7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1

22 13 17 11  0
 8  2 23  4 24
21  9 14 16  7
 6 10  3 18  5
 1 12 20 15 19

 3 15  0  2 22
 9 18 13 17  5
19  8  7 25 23
20 11 10 24  4
14 21 16 12  6

14 21 17 24  4
10 16 15  9 19
18  8 23 26 20
22 11 13  6  5
 2  0 12  3  7
"""

input = File.read!("day_04/input.txt")

defmodule Setup do
  def parse(input) when is_binary(input) do
    [numbers | grids] = String.split(input, "\n\n", trim: true)

    numbers =
      numbers
      |> String.split(",")
      |> Enum.map(&String.to_integer/1)

    grids =
      grids
      |> Enum.map(fn grid ->
        grid
        |> String.split("\n", trim: true)
        |> Enum.map(&String.split(&1, " ", trim: true))
        |> Enum.map(fn row ->
          Enum.map(row, &String.to_integer/1)
        end)
      end)

    {grids, numbers}
  end
end

Part 1

defmodule DayFourPart1 do
  def play_bingo(grids, numbers) do
    Enum.reduce_while(numbers, [], fn next_number, played_numbers ->
      case find_first_grid_with_bingo(grids, played_numbers) do
        {:bingo, grid} ->
          {:halt, {grid, played_numbers}}

        :nobingo ->
          {:cont, [next_number | played_numbers]}
      end
    end)
  end

  def find_first_grid_with_bingo(grids, numbers) do
    Enum.reduce_while(grids, :nobingo, fn grid, :nobingo ->
      if grid_bingo?(grid, numbers) do
        {:halt, {:bingo, grid}}
      else
        {:cont, :nobingo}
      end
    end)
  end

  def grid_bingo?(grid, numbers) do
    numbers = MapSet.new(numbers)
    horizontal_bingo?(grid, numbers) || vertical_bingo?(grid, numbers)
  end

  defp horizontal_bingo?(grid, numbers) do
    Enum.any?(grid, &MapSet.subset?(MapSet.new(&1), numbers))
  end

  defp vertical_bingo?(grid, numbers) do
    grid
    |> transpose()
    |> Enum.any?(&MapSet.subset?(MapSet.new(&1), numbers))
  end

  defp transpose(grid) do
    grid
    |> Enum.zip()
    |> Enum.map(&Tuple.to_list/1)
  end

  def score(grid, numbers) do
    last_winning_number = hd(numbers)
    grid_mapset = grid |> List.flatten() |> MapSet.new()
    numbers_mapset = MapSet.new(numbers)

    sum_of_unmarked_numbers =
      MapSet.difference(grid_mapset, numbers_mapset)
      |> MapSet.to_list()
      |> Enum.sum()

    sum_of_unmarked_numbers * last_winning_number
  end
end

{grids, numbers} = Setup.parse(input)
{winning_grid, winning_numbers} = DayFourPart1.play_bingo(grids, numbers)
DayFourPart1.score(winning_grid, winning_numbers)

Part 2

defmodule DayFourPart2 do
  def last_bingo(grids, numbers) do
    Enum.reduce_while(numbers, {grids, []}, fn next_number, {grids_acc, played_numbers} ->
      case reject_bingo(grids_acc, played_numbers) do
        [last_bingo_grid] ->
          {:halt, {last_bingo_grid, played_numbers}}

        remaing_grids ->
          {:cont, {remaing_grids, [next_number | played_numbers]}}
      end
    end)
  end

  def reject_bingo(grids, numbers) do
    Enum.reject(grids, &DayFourPart1.grid_bingo?(&1, numbers))
  end
end

{grids, numbers} = Setup.parse(input)
{last_grid, played_numbers} = DayFourPart2.last_bingo(grids, numbers)

false = DayFourPart1.grid_bingo?(last_grid, played_numbers)

next_numbers = Enum.slice(numbers, 0..length(played_numbers)) |> Enum.reverse()
DayFourPart1.grid_bingo?(last_grid, next_numbers)

DayFourPart1.score(last_grid, next_numbers)