diff --git a/day16/main.exs b/day16/main.exs new file mode 100644 index 0000000..9d3c634 --- /dev/null +++ b/day16/main.exs @@ -0,0 +1,128 @@ +defmodule Day16 do + def run do + map_list = + File.read!("data.txt") + |> String.trim() + |> String.split("\n") + |> Enum.map(&String.graphemes/1) + + height = length(map_list) + width = length(hd(map_list)) + + # convert the map List to a map Map + map = + Enum.concat(map_list) + |> Enum.with_index() + |> Enum.reduce(%{}, fn {el, idx}, acc -> + Map.put(acc, {div(idx, height), rem(idx, width)}, el) + end) + + # starting position always at bottom left, facing east + start = {height - 2, 1, {0, 1}} + # end is always at the top right + dest = {1, width - 2} + + # create a map of visited nodes an a queue + visited = %{start => 100_000_000} + q = :queue.from_list([{start, 0, [{0, 0}]}]) + + {paths, shortest} = walk(map, q, dest, visited, [], 100_000_000_000) + dbg(shortest) + + num_on_all_paths = + Enum.reduce(paths, [], fn p, acc -> Enum.concat(p, acc) end) + |> Enum.uniq() + |> length() + + dbg(num_on_all_paths) + end + + defp walk(map, q, {dx, dy} = dest, visited, paths, shortest_path) do + # dequeue the node with the lowest distnce + # no priority queue, so just find the lowest one and delete it from the queue + ql = :queue.to_list(q) + + if length(ql) == 0 do + {paths, shortest_path} + else + lowest = Enum.min_by(ql, fn {_, dist, _} -> dist end) + {{x, y, dir}, dist, path} = lowest + new_q = :queue.delete(lowest, q) + + # if we reached the destination + if x == dx and y == dy do + # remove everything from the queue with distances greater than shortest_path + new_q2 = :queue.filter(fn {_, d, _} -> d <= shortest_path end, new_q) + + # add it to the list of paths if it's <= to the current shortest path + if dist <= shortest_path do + new_paths = [path | paths] + walk(map, new_q2, dest, visited, new_paths, dist) + else + walk(map, new_q2, dest, visited, paths, shortest_path) + end + else + dirs = [{-1, 0}, {0, 1}, {1, 0}, {0, -1}] + + # find neighbors to enqueue + neighbors = + dirs + |> Enum.map(fn {dx, dy} -> {x + dx, y + dy} end) + |> Enum.map(fn key -> {key, Map.get(map, key, ".")} end) + |> Enum.filter(fn {_key, el} -> el != "#" end) + # visited must include the direction. You can validly visit a cell from multiple directions + |> Enum.reject(fn {{nx, ny}, _el} -> + key = {nx, ny, {nx - x, ny - y}} + Map.has_key?(visited, key) + end) + |> Enum.map(fn {key, _el} -> key end) + + new_q2 = + Enum.reduce(neighbors, new_q, fn {nx, ny}, acc -> + # dir is the previous direction + new_dir = {nx - x, ny - y} + new_dist = dist + if dir == new_dir, do: 1, else: 1001 + + :queue.in({{nx, ny, new_dir}, new_dist, [{x, y} | path]}, acc) + end) + + new_visited = Map.put(visited, {x, y, dir}, dist) + walk(map, new_q2, dest, new_visited, paths, shortest_path) + end + end + end + + defp render(map, location \\ nil, visited \\ %{}) do + width = Map.keys(map) |> Enum.map(&elem(&1, 1)) |> Enum.max() + height = Map.keys(map) |> Enum.map(&elem(&1, 0)) |> Enum.max() + + Enum.each(0..height, fn row -> + Enum.each(0..width, fn col -> + if !is_nil(location) and location == {row, col} do + IO.write(IO.ANSI.format([:red_background, "@"])) + else + el = Map.get(map, {row, col}, ".") + + visited? = Enum.member?(visited, {row, col}) + + formatted = + if visited? do + IO.ANSI.format([:blue_background, el]) + else + el + end + + IO.write(formatted) + end + end) + + IO.write("\n") + end) + + IO.write("\n") + + map + end +end + +Day16.run()