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

Day 9: All in a Single Night

2015/9.livemd

Day 9: All in a Single Night

Mix.install([
  {:kino, "~> 0.12.3"}
])

Input

input = Kino.Input.textarea("Please paste your puzzle input:")
defmodule CityGraph do
  @doc """
  Creates a new CityGraph data structure.
  """
  def new() do
    Map.new()
  end

  @doc """
  Adds a new connection to the graph. Uses nested maps to store 
  connection distances.

  ## Example
  ```elixir
  iex> CityGraph.new()
  iex> |> CityGraph.add_connection({:london, :dublin, 464})
  %{london: %{dublin: 464}, dublin: %{london: 464}}
  """
  def add_connection(graph, {from, to, dist}) do
    graph
    |> Map.update(from, Map.new([{to, dist}]), &Map.put(&1, to, dist))
    |> Map.update(to, Map.new([{from, dist}]), &Map.put(&1, from, dist))
  end

  @doc """
  Finds the shortest Hamiltonian path amongst all provided cities
  """
  def shortest_path(graph) do
    graph
    |> path_distances()
    |> Enum.min()
  end

  @doc """
  Finds the longest Hamiltonian path amongst all provided cities.
  """
  def longest_path(graph) do
    graph
    |> path_distances()
    |> Enum.max()
  end

  # returns a list of all the path distances
  defp path_distances(graph) do
    graph
    |> Map.keys()
    |> permutations()
    |> Enum.map(&path_distance(graph, &1))
  end

  # calculates a single path's length/distance
  defp path_distance(graph, path) do
    path
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.map(fn [from | [to]] -> graph[from][to] end)
    |> Enum.sum()
  end

  # base case for permutations
  defp permutations([]), do: [[]]

  # returns a list of all the permutations of elements within 
  # the provided list
  defp permutations(list) do
    for elem <- list, rest <- permutations(list -- [elem]), do: [elem | rest]
  end
end
defmodule Parser do
  @doc ~S"""
  Parses a string into a list of city connections

  ## Example
  iex> Parser.parse("London to Dublin = 464\nLondon to Belfast = 518")
  [{:london, :dublin, 464}, {:london, :belfast, 518}]
  """
  def parse(str) do
    str
    |> String.split("\n")
    |> Enum.map(&amp;parse_line/1)
  end

  # parses a single line into a city connection
  defp parse_line(str) do
    [from | [to | [dist]]] = String.split(str, [" to ", " = "])
    {parse_city(from), parse_city(to), parse_distance(dist)}
  end

  # atomises the city name
  defp parse_city(str) do
    str
    |> String.downcase()
    |> String.to_atom()
  end

  # parses the distance as an integer
  defp parse_distance(str) do
    str
    |> String.to_integer()
  end
end

In part 1, we create the Map used as a CityGraph by first parsing the input and then applying Enum.reduce/3 to add all of the connections. The CityGraph data structure then calculates every path that can connects all of the cities as described in the puzzle (we are bruteforcing a solution as this is the only reasonable method). Once this has been done we can calculate the distances and report the minimum with CityGraph.shortest_path/1.

input
|> Kino.Input.read()
|> Parser.parse()
|> Enum.reduce(CityGraph.new(), &amp;CityGraph.add_connection(&amp;2, &amp;1))
|> CityGraph.shortest_path()

Part 2

This follows the exact same procedure and caluclations, however we now take the maximum of all possible paths.

input
|> Kino.Input.read()
|> Parser.parse()
|> Enum.reduce(CityGraph.new(), &amp;CityGraph.add_connection(&amp;2, &amp;1))
|> CityGraph.longest_path()