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

Advent of Code 2024 - Day 20

elixir/2024/day_20.livemd

Advent of Code 2024 - Day 20

Mix.install([
  :kino_aoc
])

Introduction

2024 - Day 20

Puzzle

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

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.with_index()
    |> Enum.flat_map(fn {line, y} ->
      line
      |> String.graphemes()
      |> Enum.with_index()
      |> Enum.filter(fn {char, _} -> char != "#" end)
      |> Enum.map(fn {char, x} -> {{x, y}, char} end)
    end)
    |> Map.new()
  end
end

Tests - Parser

ExUnit.start(autorun: false)

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

  @input """
  #####
  #S.E#
  #####
  """
  @expected %{{1, 1} => "S", {2, 1} => ".", {3, 1} => "E"}

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

ExUnit.run()

Shared

defmodule Shared do
  def cheat([], _, _), do: 0

  def cheat([{{sx, sy}, from} | others], cheat_time, at_least_save) do
    found_cheat =
      others
      |> Enum.filter(fn {{dx, dy}, to} ->
        manhattan_dist = abs(sx - dx) + abs(sy - dy)
        default_dist = to - from

        if manhattan_dist > cheat_time or default_dist <= cheat_time do
          false
        else
          time_saved = default_dist - manhattan_dist
          time_saved >= at_least_save
        end
      end)
      |> Enum.count()

    found_cheat + cheat(others, cheat_time, at_least_save)
  end

  def path(map) do
    {start, _} = map |> Enum.find(fn {_, char} -> char == "S" end)

    1..(Enum.count(map) - 1)
    |> Enum.reduce([start], fn _, track = [{x, y} | _] ->
      [{1, 0}, {0, -1}, {-1, 0}, {0, 1}]
      |> Enum.map(fn {dx, dy} -> {x + dx, y + dy} end)
      |> Enum.find(fn next ->
        Map.has_key?(map, next) and !Enum.member?(track, next)
      end)
      |> then(fn next -> [next | track] end)
    end)
    |> Enum.reverse()
    |> Enum.with_index()
  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, cheat_time \\ 2, at_least_save \\ 100) do
    input
      |> Parser.parse()
      |> path()
      |> cheat(cheat_time, at_least_save)  
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

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

  @input """
  ###############
  #...#...#.....#
  #.#.#.#.#.###.#
  #S#...#.#.#...#
  #######.#.#.###
  #######.#.#...#
  #######.#.###.#
  ###..E#...#...#
  ###.#######.###
  #...###...#...#
  #.#####.#.###.#
  #.#...#.#.#...#
  #.#.#.#.#.#.###
  #...#...#...###
  ###############
  """
  @expected 1

  test "part one" do
    actual = run(@input, 2, 64)
    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, cheat_time \\ 20, at_least_save \\ 100) do
    input
      |> Parser.parse()
      |> path()
      |> cheat(cheat_time, at_least_save)  
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

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

  @input """
  ###############
  #...#...#.....#
  #.#.#.#.#.###.#
  #S#...#.#.#...#
  #######.#.#.###
  #######.#.#...#
  #######.#.###.#
  ###..E#...#...#
  ###.#######.###
  #...###...#...#
  #.#####.#.###.#
  #.#...#.#.#...#
  #.#.#.#.#.#.###
  #...#...#...###
  ###############
  """
  @expected 3

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

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)