Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Day20

2024/elixir/day20.livemd

Day20

Mix.install([
  {:kino_aoc, git: "https://github.com/ljgago/kino_aoc"}
])

Setup

{:ok, data} = KinoAOC.download_puzzle("2024", "20", System.fetch_env!("LB_AOC_SECRET"))

Helpers

defmodule Aoc.Grid do
  @type aoc_grid :: %{grid: map(), mx: non_neg_integer(), my: non_neg_integer()}
  @type data :: String.t()

  @up {-1, 0}
  @down {1, 0}
  @left {0, -1}
  @right {0, 1}
  @moves [@up, @down, @left, @right]

  def parse(data), do: parse(data, &(&1))

  def parse(data, fun) do
    raw =
      data
      |> String.split("\n", trim: true)
      |> Enum.map(&String.graphemes/1)

    my = length(raw)
    mx = raw |> hd() |> length()

    grid =
      for {row, y} <- Enum.with_index(raw),
          {sym, x} <- Enum.with_index(row),
          into: %{} do
        {{y, x}, fun.(sym)}
      end

    %{grid: grid, mx: mx - 1, my: my - 1}
  end

  def in_grid?({y, x}, aoc) do
    y >= 0 and y <= aoc.my and x >= 0 and x <= aoc.mx
  end

  def next_moves({y, x}, aoc) do
    @moves
    |> Enum.filter(fn {dy, dx} -> in_grid?({y + dy, x + dx}, aoc) end)
    |> Enum.map(fn {dy, dx} -> {y + dy, x + dx} end)
  end

  def moves, do: @moves

  def find(aoc, val), do: Enum.find(aoc.grid, fn {_k, v} -> v == val end)

  def plot(aoc) do
    Enum.map(0..aoc.my, fn y ->
      Enum.reduce(0..aoc.mx, "", fn x, acc -> acc <> aoc.grid[{y, x}] end)
    end)
    |> Enum.join("\n")
    |> IO.puts()
  end
end

Solve

defmodule Day20 do
  alias Aoc.Grid, as: G

  def solve(data, radius, save) do
    aoc = G.parse(data)
    {st, "S"} = G.find(aoc, "S")

    seen = bfs([{st, 0}], aoc, %{})
    points = points_in(radius)

    Map.keys(seen)
    |> Enum.flat_map(fn p -> cheats_for(p, points, save, seen) end)
    |> Enum.count()
  end

  # won't count backtracks/duplicates
  # as cheat save will be negative in that case
  def cheats_for({y, x}, points, save, seen) do
    points
    |> Enum.map(fn {{dy, dx}, r} -> {{y, x}, {y+dy, x+dx}, r} end)
    |> Enum.filter(fn {p1, p2, r} ->
      Map.has_key?(seen, p2) and
        (seen[p2] - seen[p1] - r) >= save
    end)
  end

  # we don't need points at radius 1
  # as they will be on a path or on a wall
  # for radius 2 will produce:
  # . . x . .
  # . x . x .
  # x . . . x
  # . x . x .
  # . . x . .
  def points_in(radius) do
    for r <- 2..radius,
        dy <- 0..r,
        sy <- [-1,1],
        sx <- [-1,1],
        uniq: true do
      dx = r-dy
      {{sy*dy, sx*dx}, r}
    end
  end

  def bfs([], _aoc, seen), do: seen
  def bfs(q, aoc, seen) do
    [h | t] = q
    {pos, sc} = h

    seen = Map.put(seen, pos, sc)

    G.next_moves(pos, aoc)
    |> Enum.reject(fn pos -> Map.has_key?(seen, pos) end)
    |> Enum.reject(fn pos -> aoc.grid[pos] == "#" end)
    |> Enum.map(fn pos -> {pos, sc+1} end)
    |> Enum.reduce(t, fn el, acc -> [el | acc] end)
    |> bfs(aoc, seen)
  end
end

t1 = """
###############
#...#...#.....#
#.#.#.#.#.###.#
#S#...#.#.#...#
#######.#.#.###
#######.#.#...#
#######.#.###.#
###..E#...#...#
###.#######.###
#...###...#...#
#.#####.#.###.#
#.#...#.#.#...#
#.#.#.#.#.#.###
#...#...#...###
###############
"""

Day20.solve(t1, 2, 20)     |> IO.inspect(label: "t1") # 5
Day20.solve(t1, 20, 70)    |> IO.inspect(label: "t2") # 41
Day20.solve(data, 2, 100)  |> IO.inspect(label: "r1") # 1417
Day20.solve(data, 20, 100) |> IO.inspect(label: "r2") # 1014683