advent_of_code/elixir/livebook/2024/day16.livemd

11 KiB

AOC 2024 Day 16 - Dijkstra's

Section

defmodule Grid2D do
  defstruct width: nil, height: nil

  @directions %{
    straight: [
      {1, 0},
      {-1, 0},
      {0, 1},
      {0, -1}
    ],
    diagonal: [
      {1, 1},
      {1, -1},
      {-1, 1},
      {-1, -1}
    ]
  }

  def new(width, height) do
    %Grid2D{width: width, height: height}
  end

  @doc """
  ## Examples
  iex> Grid2D.index_to_coord(Grid2D.new(5,5), 0)
  {0,0}

  iex> Grid2D.index_to_coord(Grid2D.new(5,5), 5)
  {0,1}

  iex> Grid2D.index_to_coord(Grid2D.new(5,5), 6)
  {1,1}

  iex> Grid2D.index_to_coord(Grid2D.new(5,5), 3)
  {3,0}

  iex> Grid2D.index_to_coord(Grid2D.new(5,5), 24)
  {4, 4}
  """
  def index_to_coord(%Grid2D{width: w}, index) do
    {rem(index, w), floor(index / w)}
  end

  @doc """
  ## Examples
  iex> Grid2D.coord_to_index( Grid2D.new(5,5), {0,0})
  0

  iex> Grid2D.coord_to_index( Grid2D.new(5,5), {4,4})
  24

  iex> Grid2D.coord_to_index( Grid2D.new(5,5), {2,2})
  12
  """
  def coord_to_index(%Grid2D{width: w}, {x, y}) do
    y * w + x
  end

  @doc """

  ## Examples
  iex> Grid2D.neighbors({0, 0}, Grid2D.new(5,5)) |> MapSet.new()
  MapSet.new([{0, 1},  {1, 1}, {1,0}])

  iex> Grid2D.neighbors({2, 2}, Grid2D.new(5,5)) |> MapSet.new()
  [{1, 1}, {1, 2}, {1, 3}, {2, 1}, {2, 3}, {3, 1}, {3, 2}, {3, 3}] |> MapSet.new()

  iex> Grid2D.neighbors({4, 4}, Grid2D.new(5,5)) |> MapSet.new
  [{3, 3}, {3, 4}, {4, 3}] |> MapSet.new
  """
  def neighbors(p, g, allowed_directions \\ :all) do
    directions(allowed_directions)
    |> Stream.map(&add(p, &1))
    |> Stream.filter(&is_in_grid?(&1, g))
  end

  @doc """
  Distance from two points in a grid

  iex> Grid2D.distance(Grid2D.new(5,5), {0,0}, {0,1}, :radial)
  1

  iex> Grid2D.distance(Grid2D.new(5,5), {0,0}, {1,1}, :radial)
  1

  iex> Grid2D.distance(Grid2D.new(5,5), {0,0}, {1,2}, :radial)
  2
  """
  def distance(grid, p1, p2, type)
  def distance(_, {x, y}, {a, b}, :radial), do: max(abs(x - a), abs(y - b))

  @doc """
  Add two points together pairwise

  iex> Grid2D.add({0,0}, {0,1})
  {0,1}

  iex> Grid2D.add({2,3}, {3,2})
  {5,5}
  """
  def add({x, y}, {j, k}), do: {x + j, y + k}

  @doc """
  Test if a point is in a grid

  iex> Grid2D.is_in_grid?({1, 2}, %Grid2D{width: 3, height: 4})
  true

  iex> Grid2D.is_in_grid?({2, 2}, %Grid2D{width: 2, height: 4})
  false
  """
  def is_in_grid?({x, y}, %Grid2D{width: w, height: h}), do: x < w and y < h and x >= 0 and y >= 0

  @spec directions(allowed :: :all | :straight | :diagonal) :: any()
  def directions(allowed \\ :all)
  def directions(:all), do: directions(:straight) ++ directions(:diagonal)
  def directions(:straight), do: @directions |> Map.get(:straight)
  def directions(:diagonal), do: @directions |> Map.get(:diagonal)

  def mul({x, y}, n), do: {n*x, n*y}
end

defmodule ReindeerMaze do
  def solve(input) do
    get_paths(input)
  end

  def get_paths(input) do
    {map, grid} = parse(input)
    {start, _} = map |> Enum.find(&(elem(&1, 1) == "S"))
    {fin, _} = map |> Enum.find(&(elem(&1, 1) == "E"))

    shortest_paths = map 
      |> Map.keys 
      |> Stream.map(&({&1, :infinity}))
      |> Enum.into(%{}) 
      |> Map.put(start, 0)
    shortest = find_min(
      map, 
      grid, 
      start, 
      {1, 0}, 
      shortest_paths, 
      fin, 
      Map.keys(map)|>MapSet.new|>MapSet.delete(start), 
      %{}
    )
    {shortest,
    fin, 
    shortest[fin]}
  end

  def cost({x,y}, {xx, yy}, prev_dir) do
    dir = {x - xx, y - yy}
    cost = if dir == prev_dir do
      1
    else
      1001
    end
    cost
  end

  def find_min(map, grid, {x,y} = start, facing, paths, fin, unvisited, facing_m) do
    neighbors = Grid2D.neighbors(start, grid, :straight) 
      |> Stream.reject(&Map.get(map, &1) == "#")
      |> Stream.filter(&(MapSet.member?(unvisited, &1)))
    
    {paths, facing_m} = 
      for {point, cost} <- neighbors
        |> Stream.map(&({&1,paths[start] + cost(&1, start, facing)})), reduce: {paths,facing_m} do
        {shortest,facing_m} ->
          if shortest[point] > cost do
            {xx, yy} = point
            {
              shortest |> Map.put(point, cost),
              facing_m |> Map.put(point, {xx - x, yy - y})
            }
          else 
            {shortest, facing_m}
          end
      end
    unvisited = unvisited |> MapSet.delete(start)
    
    next = unvisited |> Enum.min_by(fn m -> Map.get(paths, m) end)

    if next == nil or unvisited |> MapSet.size() == 0 or unvisited |> Enum.all?(&Map.get(paths, &1) == :infinity) do
      paths
    else
      find_min(map, grid, next, Map.get(facing_m, next), paths, fin, unvisited, facing_m)
    end
  end
  
  def parse(input) do
    map =
      for {line, y} <- input |> String.split("\n", trim: true) |> Stream.with_index(),
          {c, x} <- line |> String.graphemes() |> Stream.with_index(),
          into: %{} do
        {{x, y}, c}
      end

    max = map |> Map.keys() |> Enum.max()

    grid =
      Grid2D.new(
        (max |> elem(0)) + 1,
        (max |> elem(1)) + 1
      )

    {map, grid}
  end
end
e1 = """
###############
#.......#....E#
#.#.###.#.###.#
#.....#.#...#.#
#.###.#####.#.#
#.#.#.......#.#
#.#.#####.###.#
#...........#.#
###.#.#####.#.#
#...#.....#.#.#
#.#.#.###.#.#.#
#.....#...#.#.#
#.###.#.#.#.#.#
#S..#.....#...#
###############
"""

e2 = """
#################
#...#...#...#..E#
#.#.#.#.#.#.#.#.#
#.#.#.#...#...#.#
#.#.#.#.###.#.#.#
#...#.#.#.....#.#
#.#.#.#.#.#####.#
#.#...#.#.#.....#
#.#.#####.#.###.#
#.#.#.......#...#
#.#.###.#####.###
#.#.#...#.....#.#
#.#.#.#####.###.#
#.#.#.........#.#
#.#.#.#########.#
#S#.............#
#################
"""

e3 = """
########
#...####
#.#.E..#
#.##.#.#
#....#.#
####.#.#
#S.....#
########
"""

e4 = """
###
#E#
#.#
#.#
#.#
#.#
#.#
#.#
#.#
#S#
###
"""

e5 = """
##########
#S......E#
##########
"""

e6 = """
###########
#........E#
#.#########
#.........#
####..#####
#.........#
#.####..###
#S........#
###########
"""

e7 = """
##########
#E....####
#####.####
#S....####
##########
"""

r1 = """
###########################
#######################..E#
######################..#.#
#####################..##.#
####################..###.#
###################..##...#
##################..###.###
#################..####...#
################..#######.#
###############..##.......#
##############..###.#######
#############..####.......#
############..###########.#
###########..##...........#
##########..###.###########
#########..####...........#
########..###############.#
#######..##...............#
######..###.###############
#####..####...............#
####..###################.#
###..##...................#
##..###.###################
#..####...................#
#.#######################.#
#S........................#
###########################
"""

r2 = """
####################################################
#......................................#..........E#
#......................................#...........#
#....................#.................#...........#
#....................#.................#...........#
#....................#.................#...........#
#....................#.................#...........#
#....................#.................#...........#
#....................#.................#...........#
#....................#.................#...........#
#....................#.................#...........#
#....................#.............................#
#S...................#.............................#
####################################################
"""
{p,_,sol} = ReindeerMaze.solve(e1)
sol
defmodule ReindeerMaze2 do
  def solve(input) do
    get_paths(input)
  end

  def get_paths(input) do
    {map, grid} = parse(input)
    {start, _} = map |> Enum.find(&(elem(&1, 1) == "S"))
    {fin, _} = map |> Enum.find(&(elem(&1, 1) == "E"))

    shortest_paths = map 
      |> Map.keys 
      |> Stream.map(&({&1, :infinity}))
      |> Enum.into(%{}) 
      |> Map.put(start, 0)
    {shortest,top} = find_min(
      map, 
      grid, 
      start, 
      {1, 0}, 
      shortest_paths, 
      fin, 
      Map.keys(map)|>MapSet.new|>MapSet.delete(start), 
      %{},
      %{} |> Map.put(start, MapSet.new([start]))
    )
    {
      shortest,
      fin, 
      shortest[fin],
      top
    }
  end

  def cost({x,y}, {xx, yy}, prev_dir) do
    dir = {x - xx, y - yy}
    cost = if dir == prev_dir do
      1
    else
      1001
    end
    cost
  end

  def find_min(map, grid, {x,y} = start, facing, paths, fin, unvisited, facing_m, top) do
    neighbors = Grid2D.neighbors(start, grid, :straight) 
      |> Stream.reject(&Map.get(map, &1) == "#")
      |> Stream.filter(&(MapSet.member?(unvisited, &1)))
    
    {paths, facing_m, top} = 
      for {point, cost} <- neighbors
        |> Stream.map(&({&1,paths[start] + cost(&1, start, facing)})),
        reduce: {paths,facing_m,top} do
        
        {shortest,facing_m, top} ->
          cond do 
            shortest[point] > cost ->
              {xx, yy} = point
              {
                shortest |> Map.put(point, cost),
                facing_m |> Map.put(point, {xx - x, yy - y}),
                top |> Map.put(point, MapSet.new([start]))
              }
            shortest[point] == cost ->
              {
                shortest, 
                facing_m,
                top |> Map.update!(point, &MapSet.put(&1, start))
              }
            true ->
              {shortest, facing_m, top}
          end
      end
    
    unvisited = unvisited |> MapSet.delete(start)
    
    next = unvisited |> Enum.min_by(fn m -> Map.get(paths, m) end)

    if unvisited |> MapSet.size() == 0 or unvisited |> Enum.all?(&Map.get(paths, &1) == :infinity) do
      {paths,top}
    else
      find_min(map, grid, next, Map.get(facing_m, next), paths, fin, unvisited, facing_m, top)
    end
  end


  def paths_cheaper_than(_, _, start, _, fin, _, n) when n < 0 and fin != start, do: MapSet.new
  def paths_cheaper_than(_, _, fin, _, fin, _, n) when n != 0, do: MapSet.new
  def paths_cheaper_than(_, _, fin, _, fin, visited, 0), do: MapSet.put(visited, fin)
  def paths_cheaper_than(map, grid, {x, y} = start, direction, fin, visited, budget) do
    Grid2D.neighbors(start, grid)
    |> Stream.reject(&(Map.get(map, &1) == "#"))
    |> Stream.reject(&MapSet.member?(visited, &1))
    |> Stream.map(&({&1, cost(&1, start, direction)}))
    |> Stream.filter(&(budget - elem(&1, 1) >= 0))
    |> Stream.flat_map(
      fn {{xx, yy} = p, cost} ->
        new_dir = {xx - x, yy - y}
        paths_cheaper_than(
          map,
          grid,
          p,
          new_dir,
          fin,
          visited |> MapSet.put(start),
          budget - cost
        )
      end
    )
    |> Enum.into(MapSet.new)
  end
  
  def parse(input) do
    map =
      for {line, y} <- input |> String.split("\n", trim: true) |> Stream.with_index(),
          {c, x} <- line |> String.graphemes() |> Stream.with_index(),
          into: %{} do
        {{x, y}, c}
      end

    max = map |> Map.keys() |> Enum.max()

    grid =
      Grid2D.new(
        (max |> elem(0)) + 1,
        (max |> elem(1)) + 1
      )

    {map, grid}
  end
end
m = r1
IO.puts(m)
{_,f,p1,top} = ReindeerMaze2.solve(m)
IO.puts(p1 |> inspect())
IO.puts(top[f] |> inspect())