diff --git a/elixir/livebook/2024/day16.livemd b/elixir/livebook/2024/day16.livemd new file mode 100644 index 0000000..75da9a6 --- /dev/null +++ b/elixir/livebook/2024/day16.livemd @@ -0,0 +1,505 @@ +# 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()) +```