diff --git a/elixir/livebook/2024/day6.livemd b/elixir/livebook/2024/day6.livemd new file mode 100644 index 0000000..67d22bb --- /dev/null +++ b/elixir/livebook/2024/day6.livemd @@ -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)) + +```