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

Day 8: Haunted Wasteland – Advent of Code 2023

2023/08.livemd

Day 8: Haunted Wasteland – Advent of Code 2023

input =
  File.stream!("/Users/pw/src/weiland/adventofcode/2023/input/08.txt")
  |> Stream.map(&String.trim/1)

test_input = "RL

AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)" |> String.split("\n")

test_input2 = "LLR

AAA = (BBB, BBB)
BBB = (AAA, ZZZ)
ZZZ = (ZZZ, ZZZ)" |> String.split("\n")

test_input3 = "LR

11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11B, XXX)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)" |> String.split("\n")

Parsing

parse = fn input ->
  directions =
    input
    |> Enum.at(0)
    |> String.graphemes()

  network =
    input
    |> Stream.drop(2)
    |> Stream.map(fn line -> Regex.scan(~r/\w\w\w/, line) |> Enum.map(&Enum.at(&1, 0)) end)
    |> Enum.reduce(Map.new(), fn [key | value], map -> Map.put(map, key, value) end)

  {directions, network}
end

parse.(test_input) |> elem(1) |> Enum.to_list()

Part One

defmodule PartOne do
  def find_index(directions, i) do
    max_count = Enum.count(directions)
    if i >= max_count, do: rem(i, max_count), else: i
  end

  def find_next(network, directions, key, i) do
    if String.ends_with?(key, "Z") do
      i
    else
      dir = Enum.at(directions, find_index(directions, i))
      [l, r] = Map.get(network, key)
      next = if dir == "R", do: r, else: l
      find_next(network, directions, next, i + 1)
    end
  end
end

part_one = fn input ->
  {directions, network} =
    input
    |> parse.()

  PartOne.find_next(network, directions, "AAA", 0)
end

IO.inspect(
  part_one.(test_input) == 2 &&
    part_one.(test_input2) == 6,
  label: "Smoke test part one"
)

part_one.(input)

Part Two

defmodule MathUtils do
  def lcm(curr, nil), do: curr
  def lcm(0, 0), do: 0
  def lcm(a, b), do: round(a * b / Integer.gcd(a, b))
end

part_two = fn input ->
  {directions, network} =
    input
    |> parse.()

  network
  |> Map.filter(&String.ends_with?(elem(&1, 0), "A"))
  |> Enum.map(&elem(&1, 0))
  |> Enum.map(&PartOne.find_next(network, directions, &1, 0))
  |> Enum.reduce(&MathUtils.lcm/2)
end

IO.inspect(
  part_two.(test_input3) == 6 &&
    part_two.(test_input2) == 6 &&
    part_two.(test_input) == 2,
  label: "Smoke test part two"
)

part_two.(input)