Powered by AppSignal & Oban Pro

Day 18

notebooks/day18.livemd

Day 18

Mix.install([
  {:req, "~> 0.4.5"},
  {:kino, "~> 0.11.3"},
  {:nx, "~> 0.6.4"}
])

Input

input =
  Req.get!(
    "https://adventofcode.com/2023/day/18/input",
    headers: [{"Cookie", ~s"session=#{System.fetch_env!("LB_AOC_SESSION")}"}]
  ).body

Kino.Text.new(input, terminal: true)

Part 1

defmodule Part1 do
  @deltas %{
    "U" => {-1, 0},
    "D" => {1, 0},
    "L" => {0, -1},
    "R" => {0, 1}
  }

  def dig(input) do
    {_, map} =
      for line <- String.split(input, "\n", trim: true),
          reduce: {{0, 0}, MapSet.new()} do
        {curr, acc} ->
          [dir, amt, _] = String.split(line)
          amt = String.to_integer(amt)
          {dy, dx} = @deltas[dir]

          {next, acc} =
            for _ <- 0..(amt - 1), reduce: {curr, acc} do
              {{y, x}, acc} ->
                next = {y + dy, x + dx}
                acc = MapSet.put(acc, next)
                {next, acc}
            end

          {next, acc}
      end

    map
  end

  def bounds(map) do
    {{y0, _}, {y1, _}} = Enum.min_max(map)
    {{_, x0}, {_, x1}} = Enum.min_max_by(map, fn {_, x} -> x end)
    {{y0, x0}, {y1, x1}}
  end

  def print(map) do
    {{y0, x0}, {y1, x1}} = bounds(map)

    for y <- y0..y1 do
      for x <- x0..x1 do
        char = if MapSet.member?(map, {y, x}), do: "#", else: "."
        IO.write(char)
      end

      IO.puts("")
    end
  end

  def interior(map) do
    {{y0, x0}, {y1, x1}} = bounds(map)
    {{y0, x0}, {y1, x1}} = bbox = {{y0 - 1, x0 - 1}, {y1 + 1, x1 + 1}}
    exterior = flood(map, bbox)
    (y1 - y0 + 1) * (x1 - x0 + 1) - MapSet.size(exterior)
  end

  def flood(map, {start, _} = bbox), do: flood(map, bbox, [start], MapSet.new())
  def flood(_, _, [], explored), do: explored

  def flood(map, {{y0, x0}, {y1, x1}} = bbox, [{y, x} = next | frontier], explored) do
    explored = MapSet.put(explored, next)

    neighbors =
      @deltas
      |> Map.values()
      |> Enum.map(fn {dy, dx} -> {y + dy, x + dx} end)
      |> Enum.reject(fn {y, x} = neighbor ->
        y < y0 or y > y1 or x < x0 or x > x1 or
          MapSet.member?(explored, neighbor) or
          MapSet.member?(map, neighbor)
      end)

    frontier = neighbors ++ frontier
    flood(map, bbox, frontier, explored)
  end
end

Part1.dig(input)
|> Part1.interior()

Part 2

defmodule Part2 do
  @deltas %{
    "0" => {0, 1},
    "1" => {1, 0},
    "2" => {0, -1},
    "3" => {-1, 0}
  }

  def dig(input) do
    origin = [0, 0]

    {points, _} =
      input
      |> String.split("\n", trim: true)
      |> Enum.map_reduce(origin, fn line, [y, x] ->
        [_, <>] =
          String.split(line, ~r/[()#]/, trim: true)

        distance = String.to_integer(distance, 16)
        {dy, dx} = @deltas[direction]
        next = [y + distance * dy, x + distance * dx]
        {next, next}
      end)

    segments =
      [origin | points]
      |> Enum.chunk_every(2, 1, :discard)

    area =
      segments
      |> Enum.map(fn [[y1, x1], [y2, x2]] -> y1 * x2 - x1 * y2 end)
      |> Enum.sum()
      |> abs()
      |> div(2)

    perimeter =
      segments
      |> Enum.map(fn [[y1, x1], [y2, x2]] -> abs(y2 - y1) + abs(x2 - x1) end)
      |> Enum.sum()
      |> div(2)

    answer = area + perimeter + 1

    tensor =
      segments
      |> Nx.tensor(type: :f64)

    area_nx =
      tensor
      |> Nx.LinAlg.determinant()
      |> Nx.sum()
      |> Nx.abs()
      |> Nx.divide(2)

    perimeter_nx =
      Nx.abs(Nx.subtract(tensor[[.., 0, 0]], tensor[[.., 1, 0]]))
      |> Nx.add(Nx.abs(Nx.subtract(tensor[[.., 0, 1]], tensor[[.., 1, 1]])))
      |> Nx.sum()
      |> Nx.divide(2)
      |> Nx.add(1)

    answer_nx =
      area_nx
      |> Nx.add(perimeter_nx)
      |> Nx.as_type(:u64)
      |> Nx.to_number()

    {answer, answer_nx}
  end
end

t = Part2.dig(input)