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

Advent of Code 2023

2023/day08.livemd

Advent of Code 2023

Mix.install([
  {:req, "~> 0.3.2"}
])

Day 8

input =
  "https://adventofcode.com/2023/day/8/input"
  |> Req.get!(headers: [cookie: "session=#{System.get_env("AOC_COOKIE")}"])
  |> Map.get(:body)
sample = """
RL

AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)
"""
sample2 = """
LLR

AAA = (BBB, BBB)
BBB = (AAA, ZZZ)
ZZZ = (ZZZ, ZZZ)
"""
sample3 = """
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)
"""
defmodule A do
  def parse(input) do
    input
    |> String.split("\n\n", trim: true)
    |> then(fn [instructions, map] ->
      [
        String.split(instructions, "", trim: true),
        map
        |> String.split("\n", trim: true)
        |> Enum.map(fn mapping ->
          Regex.run(~r/(\w+) = \((\w+), (\w+)\)/, mapping)
          |> tl
          |> then(fn [key, left, right] ->
            {key, %{"L" => left, "R" => right}}
          end)
        end)
        |> Enum.into(%{})
      ]
    end)
  end

  def navigate([ins, map]) do
    ins
    |> Stream.cycle()
    |> Enum.reduce_while({"AAA", 0}, fn dir, {cur, n} ->
      case get_in(map, [cur, dir]) do
        "ZZZ" -> {:halt, n + 1}
        next -> {:cont, {next, n + 1}}
      end
    end)
  end

  def navigate2([ins, map]) do
    to_navigate =
      map
      |> Map.keys()
      |> Enum.filter(&String.ends_with?(&1, "A"))

    ins
    |> Stream.cycle()
    |> Stream.with_index()
    # this 100_000 is arbitrary and depends on the number of entries we want for each value
    # luckily, the pattern is constant for this puzzle
    # ie. we may end up on the end point every 10 steps on a single path
    |> Enum.take(100_000)
    |> Enum.reduce_while({to_navigate, %{}}, fn {dir, i}, {to_navigate, on} ->
      to_navigate
      |> Enum.map(&get_in(map, [&1, dir]))
      |> then(fn next ->
        on =
          next
          |> Enum.with_index()
          |> Enum.reduce(on, fn {pos, idx}, on ->
            if pos |> String.ends_with?("Z") do
              landed = {pos, i}

              on
              |> Map.update(idx, [landed], &[landed | &1])
            else
              on
            end
          end)

        {:cont, {next, on}}
      end)
    end)
  end

  def part1(input) do
    input
    |> parse()
    |> navigate()
  end

  def part2(input) do
    input
    |> parse()
    |> navigate2()
    # calculate periods
    |> elem(1)
    |> Enum.map(&(elem(&1, 1) |> Enum.unzip() |> elem(1)))
    |> Enum.map(&(Enum.take(&1, 2) |> then(fn [a, b] -> a - b end)))
    |> least_common_multiple()
  end

  # Here's a good walkthrough with calculator for LCM
  # https://www.calculatorsoup.com/calculators/math/lcm.php
  def least_common_multiple(ns) do
    Enum.reduce(ns, fn a, b -> lcm(a, b) end)
  end

  def lcm(a, b), do: div(a * b, gcd(a, b))

  # Euclid's algorithm
  def gcd(0, b), do: b
  def gcd(a, b), do: gcd(rem(b, a), a)
end

Part 1

input
|> A.part1()

Part 2

input
|> A.part2()