aoc 2024 day 6
This commit is contained in:
parent
9ebf0f57b1
commit
f8f0a9db2a
1 changed files with 230 additions and 0 deletions
230
elixir/livebook/2024/day6.livemd
Normal file
230
elixir/livebook/2024/day6.livemd
Normal file
|
@ -0,0 +1,230 @@
|
|||
# AoC 2024 Day 6
|
||||
|
||||
## 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)
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
```elixir
|
||||
input = ""
|
||||
```
|
||||
|
||||
```elixir
|
||||
height = input |> String.trim() |> String.split("\n") |> Enum.count()
|
||||
width = input |> String.trim() |> String.split("\n") |> Enum.at(0) |> String.trim() |> String.length()
|
||||
```
|
||||
|
||||
```elixir
|
||||
grid = Grid2D.new(width, height)
|
||||
```
|
||||
|
||||
```elixir
|
||||
input = input |> String.trim() |> String.replace("\n", "")
|
||||
```
|
||||
|
||||
```elixir
|
||||
guard_idx = input |> String.graphemes() |> Enum.find_index(fn c -> c == "^" or c == ">" or c == "<" or c == "v" end)
|
||||
```
|
||||
|
||||
```elixir
|
||||
start = Grid2D.index_to_coord(grid, guard_idx)
|
||||
```
|
||||
|
||||
```elixir
|
||||
guard_dir = case String.at(input, guard_idx) do
|
||||
"^" -> {0, -1}
|
||||
">" -> {1, 0}
|
||||
"<" -> {-1, 0}
|
||||
"v" -> {0, 1}
|
||||
end
|
||||
```
|
||||
|
||||
Removing the below and operating on input as a string below reduces runtime by a few orders of magnitude. Why?
|
||||
|
||||
```elixir
|
||||
input = for {c, i} <- input |> String.graphemes() |> Stream.with_index(), into: %{} do
|
||||
{Grid2D.index_to_coord(grid, i), c}
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
defmodule Guard do
|
||||
# the problem states the guard turns "right", but
|
||||
# y = -1 is "up" so a ccw rotation is equivalent to a "right" turn
|
||||
defp rotate90({a, b}), do: {-b, a}
|
||||
|
||||
def wander(input, grid, location, facing, visited) do
|
||||
if Grid2D.is_in_grid?(location, grid) do
|
||||
visited = visited |> MapSet.put(location)
|
||||
# if input is a string replace with
|
||||
# case String.at(input, location |> Grid2D.add(facing) |> then(&Grid2D.coord_to_index(grid, &1))) do
|
||||
case Map.get(input, location |> Grid2D.add(facing)) do
|
||||
"#" -> wander(input, grid, location, rotate90(facing), visited)
|
||||
_ -> wander(input, grid, Grid2D.add(location, facing), facing, visited)
|
||||
end
|
||||
else
|
||||
visited
|
||||
end
|
||||
end
|
||||
|
||||
def wander_with_block(input, grid, block, location, facing, visited) do
|
||||
cond do
|
||||
visited |> MapSet.member?({location,facing}) -> :loop
|
||||
Grid2D.is_in_grid?(location, grid) ->
|
||||
visited = visited |> MapSet.put({location, facing})
|
||||
next = Grid2D.add(facing, location)
|
||||
# if input is a string as mentioned above, replace the Map.get with
|
||||
# String.at(input, next |> then(&Grid2D.coord_to_index(grid, &1)))
|
||||
tile = if next == block, do: "#", else: Map.get(input, next)
|
||||
case tile do
|
||||
"#" -> wander_with_block(input, grid, block, location, rotate90(facing), visited)
|
||||
_ -> wander_with_block(input, grid, block, Grid2D.add(location, facing), facing, visited)
|
||||
end
|
||||
true -> visited
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
|
||||
# takes around .5s with strings
|
||||
# 10ms with map
|
||||
path = Guard.wander(input, grid, start, guard_dir, MapSet.new)
|
||||
```
|
||||
|
||||
```elixir
|
||||
path |> MapSet.size
|
||||
```
|
||||
|
||||
```elixir
|
||||
# with strings - will run for ~6+ minutes
|
||||
# with maps - 800ms
|
||||
:persistent_term.put(Day6, input)
|
||||
r = for p <- path,
|
||||
p != start do
|
||||
|
||||
Task.async(fn -> Guard.wander_with_block(:persistent_term.get(Day6), grid, p, start, guard_dir, MapSet.new) == :loop end )
|
||||
end |> Task.await_many(:infinity) |> Enum.count(&(&1))
|
||||
|
||||
```
|
Loading…
Add table
Reference in a new issue