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

Advent 2021 - Day 12

day12.livemd

Advent 2021 - Day 12

Setup

Mix.install([
  {:kino, github: "livebook-dev/kino"}
])
input = Kino.Input.textarea("Please paste your input file:")
input =
  input
  |> Kino.Input.read()
  |> String.trim()
  |> String.split("\n")
  |> Enum.map(fn line -> line |> String.split("-") end)

all = input |> List.flatten() |> MapSet.new()

tree =
  Enum.map(all, fn node ->
    neighbors =
      input
      |> Enum.filter(fn [from, to] -> node == from or node == to end)
      |> List.flatten()
      |> Enum.filter(fn n -> n !== node and n !== "start" end)
      |> MapSet.new()

    {node, neighbors}
  end)
  |> Map.new()
  |> Map.update!("end", fn _ -> MapSet.new() end)

Utils

defmodule Utils do
  def display(paths) do
    Kino.Markdown.new(~s"""
    Answer: `#{Enum.count(paths)}`

    ---

    Paths:
    ```
    #{Enum.map(paths, fn line -> "#{Enum.join(line, ",")}\n" end)}
    ```
    """)
  end

  def upcase?(string), do: string == String.upcase(string)
  def downcase?(string), do: string == String.downcase(string)

  def paths(_tree, "end", path, _visited, _second_visit_used), do: [["end" | path]]

  def paths(tree, position, path, visited, second_visit_used) do
    neighbors = tree[position]

    visited = Map.update(visited, position, 1, &(&1 + 1))

    cannot_visit =
      visited
      |> Enum.filter(fn {k, _} -> !upcase?(k) and k in neighbors end)
      |> Enum.map(fn {k, _} -> k end)
      |> MapSet.new()

    visitable = MapSet.difference(neighbors, cannot_visit)

    nothing_left_to_visit =
      MapSet.size(visitable) == 0 and
        !second_visit_used and
        MapSet.size(cannot_visit) == 0

    cond do
      nothing_left_to_visit == true ->
        [[position | path]]

      !second_visit_used ->
        path1 =
          Enum.reduce(visitable, [], fn node, acc ->
            paths(tree, node, [position | path], visited, false) ++ acc
          end)

        path2 =
          Enum.reduce(cannot_visit, [], fn node, acc ->
            paths(tree, node, [position | path], visited, true) ++ acc
          end)

        path1 ++ path2

      true ->
        Enum.reduce(visitable, [], fn node, acc ->
          paths(tree, node, [position | path], visited, second_visit_used) ++ acc
        end)
    end
  end
end

Part 1

part1_paths =
  Utils.paths(tree, "start", [], %{}, true)
  |> Enum.filter(fn [head | _] -> head == "end" end)
  |> Enum.map(fn path -> path |> Enum.reverse() end)
  |> Enum.reverse()
Utils.display(part1_paths)

Part 2

part2_paths =
  Utils.paths(tree, "start", [], %{}, false)
  |> Enum.filter(fn [head | _] -> head == "end" end)
  |> Enum.map(fn path -> path |> Enum.reverse() end)
  |> Enum.reverse()
Utils.display(part2_paths)