# AOC 2024 Day 16 - Dijkstra's ## Section ```elixir 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 ``` ```elixir 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 ``` ```elixir 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...................#.............................# #################################################### """ ``` ```elixir {p,_,sol} = ReindeerMaze.solve(e1) sol ``` ```elixir 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 ``` ```elixir m = r1 IO.puts(m) {_,f,p1,top} = ReindeerMaze2.solve(m) IO.puts(p1 |> inspect()) IO.puts(top[f] |> inspect()) ```