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

Advent of Code - Day 4

elixir-livebook/day4.livemd

Advent of Code - Day 4

Mix.install([
  {:kino_aoc, "~> 0.1"},
  # {:benchee, "~> 1.0", only: :dev}
  {:kino_benchee, github: "akoutmos/kino_benchee", branch: "initial_release_prep"}
])

Introduction

Puzzle

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "4", System.fetch_env!("LB_AOC_SESSION"))

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(fn line ->
      [card, play, win] = String.split(line, ["Card ", ": ", " | "], trim: true)

      %{
        card: card |> String.trim() |> String.to_integer(),
        play: play |> String.split(" ", trim: true) |> Enum.map(&String.to_integer/1),
        win: win |> String.split(" ", trim: true) |> Enum.map(&String.to_integer/1)
      }
    end)
  end
end

Tests - Parser

ExUnit.start(autorun: false)

defmodule ParserTest do
  use ExUnit.Case, async: true
  import Parser

  @input """
  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
  """
  @expected [
    %{card: 1, play: [41, 48, 83, 86, 17], win: [83, 86, 6, 31, 17, 9, 48, 53]},
    %{card: 2, play: [13, 32, 20, 16, 61], win: [61, 30, 68, 82, 17, 32, 24, 19]},
    %{card: 3, play: [1, 21, 53, 59, 44], win: [69, 82, 63, 72, 16, 21, 14, 1]},
    %{card: 4, play: [41, 92, 73, 84, 69], win: [59, 84, 76, 51, 58, 5, 54, 83]},
    %{card: 5, play: [87, 83, 26, 28, 32], win: [88, 30, 70, 12, 93, 22, 82, 36]},
    %{card: 6, play: [31, 18, 13, 56, 72], win: [74, 77, 10, 23, 35, 67, 36, 11]}
  ]

  test "parse test" do
    actual = parse(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Part One

Code - Part 1

defmodule PartOne do
  def solve(input) do
    IO.puts("--- Part One ---")
    IO.puts("Result: #{run(input)}")
  end

  def derive_card_value(card) do
    derive_card_value(card.play, card.win, 0)
  end

  def derive_card_value([], _, 0), do: 0
  def derive_card_value([], _, acc), do: round(:math.pow(2, acc - 1))

  def derive_card_value([head | tail], win, acc) do
    if head in win do
      derive_card_value(tail, win, acc + 1)
    else
      derive_card_value(tail, win, acc)
    end
  end

  def run(input) do
    input
    |> Parser.parse()
    |> Enum.map(&derive_card_value/1)
    |> Enum.sum()
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

defmodule PartOneTest do
  use ExUnit.Case, async: true
  import PartOne

  @input """
  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
  """
  @expected 13

  test "part one" do
    actual = run(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

defmodule PartTwo do
  def solve(input) do
    IO.puts("--- Part Two ---")
    IO.puts("Result: #{run(input)}")
  end

  def insert_card_value(card) do
    value = insert_card_value(card.play, card.win, 0)
    Map.put(card, :value, value)
  end

  def insert_card_value([], _, acc), do: acc

  def insert_card_value([head | tail], win, acc) do
    if head in win do
      insert_card_value(tail, win, acc + 1)
    else
      insert_card_value(tail, win, acc)
    end
  end

  def run(input) do
    input
    |> Parser.parse()
    |> Enum.map(&insert_card_value/1)
    |> Enum.reduce(%{}, fn %{card: i, value: v}, acc ->
      Enum.reduce(i..(i + v), acc, fn n, acc ->
        # for each card we always have one of it
        if i == n do
          Map.update(acc, n, 1, &(&1 + 1))
        else
          Map.update(acc, n, acc[i], &(&1 + acc[i]))
        end
      end)
    end)
    |> Map.values()
    |> Enum.sum()
  end
end

PartTwo.run("""
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
""")

Tests - Part 2

ExUnit.start(autorun: false)

defmodule PartTwoTest do
  use ExUnit.Case, async: true
  import PartTwo

  @input """
  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
  """
  @expected 30

  test "part two" do
    actual = run(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)

Benchmarking

defmodule Benchmarks do
  def part1(input) do
    PartOne.run(input)
  end

  def part2(input) do
    PartTwo.run(input)
  end
end

Benchee.run(
  %{
    "day04.part1" => &Benchmarks.part1/1,
    "day04.part2" => &Benchmarks.part2/1
  },
  inputs: %{
    "puzzle" => puzzle_input
  },
  time: 1,
  memory_time: 1,
  reduction_time: 1
)