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

Day22

2023/elixir/day22.livemd

Day22

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

Setup

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

Solve

defmodule Day22 do
  def parse(data) do
    data
    |> String.trim()
    |> String.split("\n")
    |> Enum.map(&parse_row/1)
    |> Enum.with_index()
    |> Enum.flat_map(fn {block, id} ->
      block |> to_cubes() |> Enum.map(fn cube -> {cube, id} end)
    end)
  end

  def parse_row(row) do
    row
    |> String.replace("~", ",")
    |> String.split(",", trim: true)
    |> Enum.map(&String.to_integer/1)
    |> then(fn [xa, ya, za, xb, yb, zb] ->
      {{xa, ya, za}, {xb, yb, zb}}
    end)
  end

  def to_cubes({{x, y, z}, {x, y, z}}), do: [{x, y, z}]
  def to_cubes({{xa, y, z}, {xb, y, z}}) when xb > xa, do: Enum.map(xa..xb, fn xx -> {xx, y, z} end)
  def to_cubes({{x, ya, z}, {x, yb, z}}) when yb > ya, do: Enum.map(ya..yb, fn yy -> {x, yy, z} end)
  def to_cubes({{x, y, za}, {x, y, zb}}) when zb > za, do: Enum.map(za..zb, fn zz -> {x, y, zz} end)
  def to_cubes({from, to}), do: raise(ArgumentError, message: "invalid block: #{from} -> #{to}")

  def cubes_by_id(data) do
    Enum.reduce(data, %{}, fn {cube, id}, acc ->
      Map.update(acc, id, [cube], fn cubes -> [cube | cubes] end)
    end)
    |> Enum.map(fn {id, line} ->
      {id, Enum.sort_by(line, fn {_, _, z} -> z end)}
    end)
    |> Enum.sort_by(fn {_id, b} -> b |> hd() |> elem(2) end)
  end

  def solve(data) do
    blocks = data |> parse() |> cubes_by_id()

    by_coord = do_fall(blocks, %{})
    fallen = cubes_by_id(by_coord)

    all = Enum.map(fallen, &elem(&1, 0)) |> MapSet.new()

    base = Enum.reduce(fallen, MapSet.new(), fn {id, block}, acc ->
      case supported_by(id, block, by_coord) do
        [base] ->
          MapSet.put(acc, base)
        _ -> acc
      end
    end)

    r1 = MapSet.difference(all, base) |> MapSet.size()

    r2 = base
      |> Enum.map(fn b -> chain_factor(b, fallen) end)
      |> Enum.sum()

    {r1, r2}
  end

  def chain_factor(bid, fallen) do
    blocks = Enum.reject(fallen, fn {id, _} -> id == bid end)
    k1 = to_key(blocks)
    k2 = do_fall(blocks, %{}) |> cubes_by_id() |> to_key()

    Enum.count(k2, fn {id, v} -> k1[id] != v end)
  end

  def to_key(blocks) do
    Enum.map(blocks, fn {k, line} -> {k, line |> hd() |> elem(2)} end) |> Map.new()
  end

  def supported_by(id, block, cubes) do
    block
    |> Enum.map(fn {x, y, z} -> Map.get(cubes, {x, y, z-1}) end)
    |> Enum.reject(fn sid -> is_nil(sid) or sid == id end)
    |> Enum.uniq()
  end

  def do_fall([], acc), do: acc
  def do_fall([{id, line} | t], acc) do
    if is_stable(line, acc) do
      # can't fall - save
      acc = Enum.reduce(line, acc, fn cube, acc -> Map.put(acc, cube, id) end)
      do_fall(t, acc)
    else
      # fall 1 level down
      line = Enum.map(line, fn {x, y, z} -> {x, y, z-1} end)
      do_fall([{id, line} | t], acc)
    end
  end

  def is_stable([{_, _, 1} | _cubes], _), do: true
  def is_stable(line, acc) do
    Enum.any?(line, fn {x, y, z} -> Map.has_key?(acc, {x,y,z-1}) end)
  end
end

t1 = """
1,0,1~1,2,1
0,0,2~2,0,2
0,2,3~2,2,3
0,0,4~0,2,4
2,0,5~2,2,5
0,1,6~2,1,6
1,1,8~1,1,9
"""

Day22.solve(t1) |> IO.inspect(label: "t1")
Day22.solve(data) # {457, 79122}