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

Day 14

livebooks/2022/14.livemd

Day 14

Mix.install([
  {:req, "~> 0.3.2"},
  {:vega_lite, "~> 0.1.6"},
  {:kino_vega_lite, "~> 0.1.6"}
])

alias VegaLite, as: Vl

day = 14

aoc_session = System.fetch_env!("LB_AOC_SESSION")
input_url = "https://adventofcode.com/2022/day/#{day}/input"
{:ok, %{body: input}} = Req.get(input_url, headers: [cookie: "session=#{aoc_session}"])

Input

test_input = """
498,4 -> 498,6 -> 496,6
503,4 -> 502,4 -> 502,9 -> 494,9
"""
defmodule Layout do
  def build_map(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.reduce(%{}, fn line, stones -> parse_line(line, stones) end)
    |> Map.new(fn {k, v} -> {k, Enum.uniq(v)} end)
  end

  def parse_line(str, stones) do
    Regex.scan(~r/([0-9]+,[0-9]+)/, str)
    |> Enum.map(fn [a, a] ->
      [l, r] = a |> String.split(",") |> Enum.map(&String.to_integer/1)
      {l, r}
    end)
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.reduce(stones, fn
      [{x, y1}, {x, y2}], ranges -> Map.update(ranges, x, [y1..y2], fn old -> [y1..y2 | old] end)
      [{x1, y}, {x2, y}], ranges -> Map.update(ranges, y, [x1..x2], fn old -> [x1..x2 | old] end)
    end)
  end

  def blocked?(point, stones, sands) do
    stone?(point, stones) or sand?(point, sands)
  end

  def stone?({x, y}, stones) do
    Enum.any?(Map.get(stones, x, []), fn range -> y in range end) or
      Enum.any?(Map.get(stones, y, []), fn range -> x in range end)
  end

  def sand?(point, sands) do
    point in sands
  end

  def abyss(stones) do
    deepest =
      stones
      |> Map.keys()
      |> Enum.reject(&(&1 > 300))
      |> Enum.max()

    deepest + 1
  end

  def rock_floor(stones) do
    abyss(stones) + 1
  end
end
defmodule Sand do
  @source {500, 0}

  def drop(stones, sands) do
    # {:ok, {stones, sands}} or
    # {:abyss, {stones, sands}}

    abyss = Layout.abyss(stones)
    drop(stones, sands, abyss, @source)
  end

  def drop(stones, sands, abyss, {_x, abyss}) do
    {:abyss, {stones, sands}}
  end

  def drop(stones, sands, abyss, {x, y}) do
    cond do
      not Layout.blocked?({x, y + 1}, stones, sands) ->
        drop(stones, sands, abyss, {x, y + 1})

      not Layout.blocked?({x - 1, y + 1}, stones, sands) ->
        drop(stones, sands, abyss, {x - 1, y + 1})

      not Layout.blocked?({x + 1, y + 1}, stones, sands) ->
        drop(stones, sands, abyss, {x + 1, y + 1})

      true ->
        {:ok, {stones, [{x, y} | sands]}}
    end
  end
end

Part 1

# input = test_input
stones = Layout.build_map(input)

Enum.reduce_while(1..999_999, [], fn _i, sands ->
  case Sand.drop(stones, sands) do
    {:ok, {_, sands}} -> {:cont, sands}
    {:abyss, {_, sands}} -> {:halt, sands}
    _ -> {:halt, "what??"}
  end
end)
|> Enum.count()

Part 2

defmodule Sand2 do
  @source {500, 0}

  def drop(stones, sands) do
    if @source in sands,
      do: {:plugged, sands},
      else: {:ok, drop(stones, sands, @source)}
  end

  def drop(stones, sands, {x, y}) do
    cond do
      not Layout.blocked?({x, y + 1}, stones, sands) ->
        drop(stones, sands, {x, y + 1})

      not Layout.blocked?({x - 1, y + 1}, stones, sands) ->
        drop(stones, sands, {x - 1, y + 1})

      not Layout.blocked?({x + 1, y + 1}, stones, sands) ->
        drop(stones, sands, {x + 1, y + 1})

      true ->
        [{x, y} | sands]
    end
  end
end

This is slooow (~4 minutes). To improve, we should not calculate the falling sand path each time and instead start from the previous grains penultimate position.

# input = test_input

stones = Layout.build_map(input)
y_floor = Layout.rock_floor(stones)
stones = Map.put_new(stones, y_floor, [-99_999..99_999])

Enum.reduce_while(1..999_999, [], fn i, sands ->
  case Sand2.drop(stones, sands) do
    {:plugged, sands} -> {:halt, sands}
    {:ok, sands} -> {:cont, sands}
  end
end)
|> Enum.count()