|
| 1 | +defmodule AdventOfCode.Y2024.Day06 do |
| 2 | + @moduledoc """ |
| 3 | + --- Day 6: Guard Gallivant --- |
| 4 | + Problem Link: https://adventofcode.com/2024/day/6 |
| 5 | + Difficulty: m |
| 6 | + Tags: grid simulation cycle-detection slow |
| 7 | + """ |
| 8 | + alias AdventOfCode.Helpers.{InputReader, Transformers} |
| 9 | + |
| 10 | + def input, do: InputReader.read_from_file(2024, 6) |
| 11 | + |
| 12 | + # Directions: up, right, down, left |
| 13 | + @dx {0, 1, 0, -1} |
| 14 | + @dy {-1, 0, 1, 0} |
| 15 | + |
| 16 | + def run(input \\ input()) do |
| 17 | + {grid, rows, cols, sx, sy} = parse(input) |
| 18 | + {run_1(grid, rows, cols, sx, sy), run_2(grid, rows, cols, sx, sy)} |
| 19 | + end |
| 20 | + |
| 21 | + # Part 1: Simulate guard path |
| 22 | + defp run_1(grid, rows, cols, sx, sy) do |
| 23 | + {pos_map, _} = simulate(grid, rows, cols, sx, sy, 0, %{}, %{}) |
| 24 | + map_size(pos_map) |
| 25 | + end |
| 26 | + |
| 27 | + # Simulate guard movement - returns {position_map, :exit | :loop} |
| 28 | + defp simulate(grid, rows, cols, x, y, dir, pos_map, state_map) do |
| 29 | + # Encode state as integer for fast lookup |
| 30 | + state_key = (x * rows + y) * 4 + dir |
| 31 | + |
| 32 | + if Map.has_key?(state_map, state_key) do |
| 33 | + {pos_map, :loop} |
| 34 | + else |
| 35 | + new_state = Map.put(state_map, state_key, true) |
| 36 | + new_pos = Map.put(pos_map, {x, y}, true) |
| 37 | + |
| 38 | + nx = x + elem(@dx, dir) |
| 39 | + ny = y + elem(@dy, dir) |
| 40 | + |
| 41 | + cond do |
| 42 | + nx < 0 or nx >= cols or ny < 0 or ny >= rows -> |
| 43 | + {new_pos, :exit} |
| 44 | + |
| 45 | + elem(grid, ny * cols + nx) == ?# -> |
| 46 | + simulate(grid, rows, cols, x, y, rem(dir + 1, 4), new_pos, new_state) |
| 47 | + |
| 48 | + true -> |
| 49 | + simulate(grid, rows, cols, nx, ny, dir, new_pos, new_state) |
| 50 | + end |
| 51 | + end |
| 52 | + end |
| 53 | + |
| 54 | + # Part 2: Count positions where adding an obstacle creates a loop |
| 55 | + defp run_2(grid, rows, cols, sx, sy) do |
| 56 | + {orig_pos, _} = simulate(grid, rows, cols, sx, sy, 0, %{}, %{}) |
| 57 | + |
| 58 | + orig_pos |
| 59 | + |> Map.keys() |
| 60 | + |> Enum.reject(fn {px, py} -> px == sx and py == sy end) |
| 61 | + |> Task.async_stream( |
| 62 | + fn {ox, oy} -> |
| 63 | + new_grid = put_elem(grid, oy * cols + ox, ?#) |
| 64 | + |
| 65 | + case simulate(new_grid, rows, cols, sx, sy, 0, %{}, %{}) do |
| 66 | + {_, :loop} -> 1 |
| 67 | + _ -> 0 |
| 68 | + end |
| 69 | + end, |
| 70 | + ordered: false, |
| 71 | + max_concurrency: System.schedulers_online() |
| 72 | + ) |
| 73 | + |> Enum.reduce(0, fn {:ok, count}, acc -> acc + count end) |
| 74 | + end |
| 75 | + |
| 76 | + # Parse into flat tuple for O(1) access |
| 77 | + defp parse(input) do |
| 78 | + lines = Transformers.lines(input) |
| 79 | + rows = length(lines) |
| 80 | + cols = String.length(hd(lines)) |
| 81 | + |
| 82 | + {grid_list_rev, sx, sy} = |
| 83 | + lines |
| 84 | + |> Enum.with_index() |
| 85 | + |> Enum.reduce({[], nil, nil}, fn {line, y}, {acc, sx, sy} -> |
| 86 | + chars = String.to_charlist(line) |
| 87 | + |
| 88 | + {new_sx, new_sy} = |
| 89 | + case Enum.find_index(chars, &(&1 == ?^)) do |
| 90 | + nil -> {sx, sy} |
| 91 | + x -> {x, y} |
| 92 | + end |
| 93 | + |
| 94 | + normalized = Enum.map(chars, fn c -> if c == ?^, do: ?., else: c end) |
| 95 | + {[normalized | acc], new_sx, new_sy} |
| 96 | + end) |
| 97 | + |
| 98 | + grid_tuple = |
| 99 | + grid_list_rev |
| 100 | + |> Enum.reverse() |
| 101 | + |> List.flatten() |
| 102 | + |> List.to_tuple() |
| 103 | + |
| 104 | + {grid_tuple, rows, cols, sx, sy} |
| 105 | + end |
| 106 | +end |
0 commit comments