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

Advent of Code 2024 - Day 18

elixir/2024/day_18.livemd

Advent of Code 2024 - Day 18

Mix.install([
  :kino_aoc,
  :prioqueue
])

Introduction

2024 - Day 18

Puzzle

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2024", "18", 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 ->
      line
      |> String.split(",")
      |> Enum.map(&String.to_integer/1)
    end)
    |> Enum.map(fn [x, y] -> {x, y} end)
  end
end

Tests - Parser

ExUnit.start(autorun: false)

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

  @input """
  1,2
  3,4
  """
  @expected [{1, 2}, {3, 4}]

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

ExUnit.run()

Shared

defmodule Shared do
  def scan(map, queue, visited, distants, max_x, max_y) do
    if Prioqueue.empty?(queue) do
      distants
    else
      {{node, distant}, queue} = Prioqueue.extract_min!(queue)

      if MapSet.member?(visited, node) do
        scan(map, queue, visited, distants, max_x, max_y)
      else
        visited = MapSet.put(visited, node)

        {queue, distants} =
          neighbour(map, node, max_x, max_y)
          |> Enum.filter(fn next -> !MapSet.member?(visited, next) end)
          |> Enum.reduce({queue, distants}, fn next, {queue, distants} ->
            distant = distant + 1
            distants = Map.update(distants, next, distant, &min(&1, distant))
            queue = Prioqueue.insert(queue, {next, distant})

            {queue, distants}
          end)

        scan(map, queue, visited, distants, max_x, max_y)
      end
    end
  end

  def neighbour(map, {x, y}, max_x, max_y) do
    [{1, 0}, {0, -1}, {-1, 0}, {0, 1}]
    |> Enum.map(fn {dx, dy} -> {x + dx, y + dy} end)
    |> Enum.filter(fn next = {x, y} ->
      in_range?(x, y, max_x, max_y) and !MapSet.member?(map, next)
    end)
  end

  def in_range?(x, y, max_x, max_y) do
    x >= 0 and x <= max_x and y >= 0 and y <= max_y
  end
end

Part One

Code - Part 1

defmodule PartOne do
  import Shared

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

  def run(input, take \\ 1024, max_x \\ 70, max_y \\ 70) do
    input
    |> Parser.parse()
    |> Enum.take(take)
    |> MapSet.new()
    |> scan(
      Prioqueue.new(
        [{{0, 0}, 0}],
        cmp_fun: fn {_, a}, {_, b} -> Prioqueue.Helper.cmp(a, b) end
      ),
      _visited = MapSet.new(),
      _distants = Map.new(),
      max_x,
      max_y
    )
    |> Map.get({max_x, max_y})
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

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

  @input """
  5,4
  4,2
  4,5
  3,0
  2,1
  6,3
  2,4
  1,5
  0,6
  3,3
  2,6
  5,1
  """
  @expected 22

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

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

defmodule PartTwo do
  import Shared

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

  def run(input, take_from \\ 1024, max_x \\ 70, max_y \\ 70) do
    bytes = Parser.parse(input)

    # can do binary search style to speed up, but too lazy
    take_from..(Enum.count(bytes) - 1)
    |> Enum.take_while(fn take ->
      bytes
      |> Enum.take(take)
      |> MapSet.new()
      |> scan(
        Prioqueue.new(
          [{{0, 0}, 0}],
          cmp_fun: fn {_, a}, {_, b} -> Prioqueue.Helper.cmp(a, b) end
        ),
        _visited = MapSet.new(),
        _distants = Map.new(),
        max_x,
        max_y
      )
      |> Map.get({max_x, max_y}) != nil
    end)
    |> List.last()
    |> then(fn take -> 
      {x, y} = Enum.at(bytes, take)
      "#{x},#{y}"
    end)
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

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

  @input """
  5,4
  4,2
  4,5
  3,0
  2,1
  6,3
  2,4
  1,5
  0,6
  3,3
  2,6
  5,1
  1,2
  5,5
  2,5
  6,5
  1,4
  0,4
  6,4
  1,1
  6,1
  1,0
  0,5
  1,6
  2,0
  """
  @expected "6,1"

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

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)