11 KiB
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())