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

Day 4

day04.livemd

Day 4

Mix.install([
  {:kino, "~> 0.11.3"}
])

Solutions

input = Kino.Input.textarea("Please paste your input file:")

Day 4: Scratchcards

For example:

Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1
Card 4: 41 92 73 84 69 | 59 84 76 51 58  5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11

tldr; find winning number count for each card double for the second, double again for third, etc. add up points.

So, in this example, the Elf’s pile of scratchcards is worth 13 points.

Part Two

tldr; number of winning numbers on card 1 gives you free cards:

  • Card 1 has four matching numbers, so you win one copy each of the next four cards: cards 2, 3, 4, and 5.
  • Repeat for card 2, etc.

In total, this example pile of scratchcards causes you to ultimately have 30 scratchcards!

input = Kino.Input.read(input)

defmodule Y2023.D4 do
  @moduledoc """
  https://adventofcode.com/2023/day/4
  """

  def p1(input) do
    input
    |> parse_input()
    |> Enum.map(&find_match_count/1)
    |> Enum.map(&calculate_points/1)
    |> Enum.sum()
  end

  def parse_input(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(&String.trim/1)
  end

  def find_match_count(card) do
    [_, winning_numbers, owned_numbers] = Regex.run(~r/^.+:([\s\d]+)\|([\d\s]+)/, card)

    parse_numbers(winning_numbers)
    |> MapSet.intersection(parse_numbers(owned_numbers))
    |> MapSet.size()
  end

  defp calculate_points(winning_count) do
    (2 ** (winning_count - 1)) |> trunc()
  end

  def parse_numbers(numbers_string) do
    numbers_string
    |> String.split(" ", trim: true)
    |> MapSet.new()
  end

  def p2(input) do
    input
    |> parse_input()
    |> Enum.map(&find_match_count/1)
    |> Enum.map(&initialize_card/1)
    |> claim_winning_cards()
    |> Enum.reduce(0, fn {_bonus, count}, acc -> acc + count end)
  end

  def initialize_card(bonus) do
    {bonus, 1}
  end

  def claim_winning_cards([]), do: []

  def claim_winning_cards([{bonus, count} = card | rest]) do
    {bonus_cards, remaining_cards} = Enum.split(rest, bonus)
    rest_cards = apply_bonus(bonus_cards, count) ++ remaining_cards
    [card | claim_winning_cards(rest_cards)]
  end

  def apply_bonus(cards, count) do
    Enum.map(cards, fn {bonus, card_count} -> {bonus, card_count + count} end)
  end
end

Y2023.D4.p1(input) |> IO.puts()
Y2023.D4.p2(input) |> IO.puts()

Observations

The part 1 and part 2 of each exercise help you think about how the code you write might be used in the future when requirements change. Yesterday, I had almost no reuse between parts 1 and 2. Today, I was able to reuse a good chunk of it.