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

Day 15

2022/elixir/day15.livemd

Day 15

Mix.install([
  {:kino, "~> 0.8.0"}
])

Puzzle Input

area = Kino.Input.textarea("Puzzle Input")
puzzle_input = Kino.Input.read(area)
example_input = """
Sensor at x=2, y=18: closest beacon is at x=-2, y=15
Sensor at x=9, y=16: closest beacon is at x=10, y=16
Sensor at x=13, y=2: closest beacon is at x=15, y=3
Sensor at x=12, y=14: closest beacon is at x=10, y=16
Sensor at x=10, y=20: closest beacon is at x=10, y=16
Sensor at x=14, y=17: closest beacon is at x=10, y=16
Sensor at x=8, y=7: closest beacon is at x=2, y=10
Sensor at x=2, y=0: closest beacon is at x=2, y=10
Sensor at x=0, y=11: closest beacon is at x=2, y=10
Sensor at x=20, y=14: closest beacon is at x=25, y=17
Sensor at x=17, y=20: closest beacon is at x=21, y=22
Sensor at x=16, y=7: closest beacon is at x=15, y=3
Sensor at x=14, y=3: closest beacon is at x=15, y=3
Sensor at x=20, y=1: closest beacon is at x=15, y=3
"""
input = puzzle_input

Common

defmodule Reading do
  defstruct [:sensor, :beacon]

  def new(sensor, beacon) do
    %Reading{sensor: sensor, beacon: beacon}
  end

  def distance(%Reading{} = reading) do
    manhattan_distance(reading.sensor, reading.beacon)
  end

  def vertical_range(%Reading{} = reading) do
    distance = distance(reading)

    {_sX, sY} = reading.sensor

    (sY - distance)..(sY + distance)
  end

  def horizontal_range(%Reading{} = reading) do
    distance = distance(reading)

    {sX, _sY} = reading.sensor

    (sX - distance)..(sX + distance)
  end

  def horizontal_range(%Reading{} = reading, row) do
    if row in vertical_range(reading) do
      distance = manhattan_distance(reading.sensor, reading.beacon)
      {sensorX, sensorY} = reading.sensor

      delta = distance - abs(sensorY - row)
      (sensorX - delta)..(sensorX + delta)
    else
      nil
    end
  end

  def covers?(%Reading{} = reading, point) do
    beacon_distance = manhattan_distance(reading.sensor, reading.beacon)
    point_distance = manhattan_distance(reading.sensor, point)

    point_distance <= beacon_distance
  end

  def manhattan_distance({x1, y1}, {x2, y2}) do
    abs(x1 - x2) + abs(y1 - y2)
  end
end
defmodule Ranges do
  defp concat(
         %Range{first: head_start, last: head_end},
         [%Range{first: next_start, last: next_end} | tail]
       )
       when next_end >= head_start do
    [next_start..max(next_end, head_end) | tail]
  end

  defp concat(head, rest), do: [head | rest]

  defp prep([]), do: []
  defp prep([head | tail]), do: [[head] | tail]

  def merge(ranges) do
    ranges
    |> Enum.sort()
    |> prep()
    |> Enum.reduce(&amp;concat/2)
    |> Enum.reverse()
  end
end
to_integer = fn str ->
  str |> Integer.parse() |> elem(0)
end

readings =
  input
  |> String.split("\n", trim: true)
  |> Enum.map(fn reading ->
    [[sensorX, sensorY], [beaconX, beaconY]] =
      Regex.scan(~r/x=(?-?\d+), y=(?-?\d+)/, reading, capture: :all_names)

    Reading.new(
      {to_integer.(sensorX), to_integer.(sensorY)},
      {to_integer.(beaconX), to_integer.(beaconY)}
    )
  end)
ExUnit.start(autorun: false)

defmodule ReadingTests do
  use ExUnit.Case, async: true

  test "covers edge points" do
    reading = Reading.new({8, 7}, {2, 10})

    assert Reading.covers?(reading, {2, 10})
  end

  test "doesn't cover points outside its distance" do
    reading = Reading.new({8, 7}, {2, 10})

    refute Reading.covers?(reading, {-1, -2})
    refute Reading.covers?(reading, {16, 17})
  end

  test "calculates horizontal range given inside row" do
    reading = %Reading{sensor: {8, 7}, beacon: {2, 10}}
    assert Reading.horizontal_range(reading, 10) == 2..14
  end

  test "calculates horizontal range given outside row" do
    reading = %Reading{sensor: {8, 7}, beacon: {2, 10}}
    refute Reading.horizontal_range(reading, 18)
  end
end

ExUnit.run()

Part One

row = 2_000_000
{%{beacon: {left, _}}, %{beacon: {right, _}}} = Enum.min_max_by(readings, &amp;elem(&amp;1.beacon, 0))
readings
|> Task.async_stream(fn reading ->
  {beaconX, beaconY} = reading.beacon

  reading
  |> Reading.horizontal_range(row)
  |> then(&amp;MapSet.new(if &amp;1 == nil, do: [], else: &amp;1))
  |> then(&amp;if beaconY == row, do: MapSet.delete(&amp;1, beaconX), else: &amp;1)
end)
|> Enum.reduce(MapSet.new(), fn {:ok, result}, acc -> MapSet.union(result, acc) end)
|> MapSet.size()

Part Two

width = height = 4_000_000
[{:ok, {row, ranges}}] =
  0..height
  |> Task.async_stream(fn row ->
    ranges =
      readings
      |> Stream.map(&amp;Reading.horizontal_range(&amp;1, row))
      |> Stream.reject(&amp;(&amp;1 == nil))
      |> Ranges.merge()

    {row, ranges}
  end)
  |> Enum.filter(fn {:ok, {_, ranges}} ->
    Enum.any?(ranges, fn %{first: first, last: last} ->
      first > 0 || last < width
    end)
  end)
[start, _] = ranges
{x, y} = {start.last + 1, row}
x * width + y