Aetherus

Aetherus

Advent of Code 2023 - Day 8

The second part of today’s puzzle is very misleading.

FYI, each of the ghosts has only one possible position that ends with a "Z" on its path.

Most Liked

shritesh

shritesh

I tried letting it run for 5 minutes in part 2 hoping it would work out. It didn’t lol.

Turns out elixir already has gcd in the stdlib: Integer — Elixir v1.15.7. Then the LCM simply becomes div(x * y, Integer.gcd(x, y))

rugyoga

rugyoga

import AOC
import Math

aoc 2023, 8 do
  def p1(input) do
    {instructions, map} = parse(input)
    traverse("AAA", map, 0, instructions, instructions, &(&1 == "ZZZ"))
  end

  def extract(s) do
    <<key::binary-size(3), " = (", left::binary-size(3), ", ", right::binary-size(3), ")">> = s
    {key, {left, right}}
  end

  def traverse(key, map, n, [], path, finished?), do: traverse(key, map, n, path, path, finished?)
  def traverse(key, map, n, [move | moves], path, finished?) do
    if finished?.(key) do
      n
    else
      {left, right} = map[key]
      case move do
        "L" -> traverse(left, map, n+1, moves, path, finished?)
        _ -> traverse(right, map, n+1, moves, path, finished?)
      end
    end
  end

  def parse(input) do
    [instructions_str, map_str] = input |> String.split("\n\n")
    {instructions_str |> String.split("", trim: true),
     map_str |> String.split("\n") |> Enum.map(&extract/1) |> Map.new}
  end

  def p2(input) do
    {moves, map} = parse(input)
    map
    |> Map.keys()
    |> Enum.filter(&String.ends_with?(&1, "A"))
    |> Enum.map(fn start -> traverse(start, map, 0, moves, moves, &String.ends_with?(&1, "Z")) end)
    |> Enum.reduce(1, &lcm/2)
  end
end
bjorng

bjorng

Erlang Core Team

I suspected that was the case, but burned by previous AoC puzzles by making assumptions not explicitly stated in the problem descriptions, I now try to avoid simplifying my code based on assumptions. For this puzzle, I tested all possible end positions for each start positions.

My solution:

seeplusplus

seeplusplus

FYI, each of the ghosts has only one possible position that ends with a "Z" on its path.

Edit: I was thinking about this and this post might not be true because entry node could leave the terminal node in different ways (by being on different steps and exiting either left or right). I thought about removing this post, but maybe someone will find interesting. Skip past if you don’t like sort-of-wrong explanations of things.

A bit of graph theory, if folks are interested: the instructions induce a set of disjoint cyclic graphs (in non-graph theory speak the entry “room”, its exit, and all of the rooms in between form a ring). So, each entry node (read, node that ends in A) belongs to some cycle that also contains a terminating node (read, node that ends in a Z). Otherwise the problem wouldn’t be solvable.

What may not be obvious, is if any cycle contains more than one terminating node (if any room could reach multiple exits) it must also contain an additional entry node. That is, if any entry node could reach more than one terminating node then there exists at least one other entry node that can also reach both.

If such a cycle existed (one that contains two terminating nodes), the only way this problem would be solvable is if both entry nodes reach their corresponding terminus in the same number of steps.

Visually, this a graph like this would never be solvable:

stevensonmt

stevensonmt

Reading the discussion makes me think I just got lucky making an assumption about the paths cycling. I think the assumption must hold that all paths will cycle IF every starting node actually can reach a finishing node AND every finishing node can be reached by a starting node. As long as that is true the LCM approach should be valid, I think. Can anyone think of a counter example where the cycle of the instructions and the length of at least one path from start to finish are such you could endlessly cycle with out ever repeating?

Here’s my code:

  defmodule Parser do
    import NimbleParsec

    instructions = times(choice([ascii_char([?L]), ascii_char([?R])]), min: 1) |> wrap()

    node = times(ascii_char([?A..?Z]), 3) |> wrap()

    parent = node |> ignore(string(" = "))

    children =
      ignore(string("("))
      |> concat(node)
      |> ignore(string(", "))
      |> concat(node)
      |> ignore(string(")\n"))

    tree =
      ignore(string("\n\n"))
      |> times(parent |> concat(children) |> wrap(), min: 1)

    defparsec(:parser, instructions |> concat(tree))
  end

# it was probably overkill to use NimbleParsec on this input, but I'm trying to get more comfortable with it. Starting to feel a little more intuitive.

defmodule Day8 do
  @moduledoc """
  Day8 AoC Solutions
  """

  alias AocToolbox.Input

  def input(:test),
    do: """
    RL

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

  def input(:test2),
    do: """
    LLR

    AAA = (BBB, BBB)
    BBB = (AAA, ZZZ)
    ZZZ = (ZZZ, ZZZ)
    """

  def input(:test3),
    do: """
    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)
    """

  def input(:real), do: Input.load(__DIR__ <> "/input.txt")

  def solve(1, mode) do
    __MODULE__.Part1.solve(input(mode))
  end

  def solve(2, mode) do
    __MODULE__.Part2.solve(input(mode))
  end

  defmodule Part1 do
    def solve(input) do
      input
      |> parse()
      |> build_tree()
      |> traverse_instructions()
    end

    def parse(input) do
      input
      |> Input.Parser.parser()
      |> elem(1)
    end

    defp build_tree([instructions | tree]) do
      graph =
        tree
        |> Enum.reduce(%{}, fn [p, l, r], acc ->
          Map.put(acc, p, %{~c"L" => l, ~c"R" => r})
        end)

      {instructions, graph}
    end

    defp traverse_instructions({instructions, tree}) do
      instructions
      |> Stream.cycle()
      |> Enum.reduce_while({~c"AAA", 0}, fn direction, {key, steps} ->
        if key == ~c"ZZZ" do
          {:halt, steps}
        else
          {:cont, {tree[key][[direction]], steps + 1}}
        end
      end)
    end
  end

  defmodule Part2 do
    def solve(input) do
      input
      |> parse()
      |> build_tree()
      |> traverse_all_starting_nodes()
      |> Enum.reduce(&AocToolbox.Math.lcm(&1, &2))
    end

    defp parse(input) do
      Part1.parse(input)
    end

    defp build_tree([instructions | tree]) do
      {instructions,
       tree
       |> Enum.reduce({%{}, MapSet.new()}, fn
         [[_, _, ?A] = p, l, r], {graph, starters} ->
           {Map.put(graph, p, %{~c"L" => l, ~c"R" => r}), MapSet.put(starters, p)}

         [p, l, r], {graph, starters} ->
           {Map.put(graph, p, %{~c"L" => l, ~c"R" => r}), starters}
       end)}
    end

    defp traverse_all_starting_nodes({instructions, {tree, starters}}) do
      starters
      |> Task.async_stream(fn starter ->
        instructions
        |> Stream.cycle()
        |> Enum.reduce_while({starter, 0}, fn direction, {key, steps} ->
          if match?([_, _, ?Z], key) do
            {:halt, steps}
          else
            {:cont, {tree[key][[direction]], steps + 1}}
          end
        end)
      end)
      |> Enum.map(&elem(&1, 1))
    end
  end
end

Where Next?

Popular in Challenges Top

Aetherus
This topic is about Day 3 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
gangstead
This is my second year doing AoC in Elixir and my first year doing it with Livebook. When I was doing just plain Elixir I usually set up...
New
kwando
Phew, this one took a while to get right. My naive attempts was way to slow so I reached for Dijkstras shortest path algorithm… and that ...
New
New
shritesh
I mapped both the cards and every possible hand to numeric values and sorted them. In part 2 I could only think of replacing the jokers w...
New
rugyoga
Fairly straightforward Dijkstra’s algorithm import AOC aoc 2023, 17 do def compute(input, candidates) do {{max_row, max_col}, ite...
New
bjorng
Note: This topic is to talk about Day 18 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can joi...
New
code-shoily
Here’s my day 3 code This was quite easy. I was afraid Part 2 would be “un-regex-able” and was preparing for hand crafting automata bu...
New
seeplusplus
Hello all, hopefully I post this before someone else does and I don’t dupe. IMO Day 4 was much easier than Day 3 (yay, I can sleep befor...
New
Aetherus
I spent 3 hours struggling in part 2, until I noticed a very basic mistake :joy: Here’s my code: By the way, the starting position in...
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New

We're in Beta

About us Mission Statement