Got stuck on part 1 for a couple days and could not figure out what I was doing wrong. My code was working on all the examples I could find, but no the puzzle input. This morning I decided I would try to visualize the stack using three.js and a custom Kino:

The z-fighting between the bricks revealed that my stacking algorithm was off. I thought that I could sort the bricks and then just use Enum.find
to take the first intersecting brick that was settled. I actually needed to take the intersecting brick with the maximum z position.
Part 1 runs in 0.3s and part 2 runs in 0.8s.
Part 1
defmodule Part1 do
def to_alpha(i), do: to_alpha(i, [])
def to_alpha(i, acc) when i < 26, do: to_string([rem(i, 26) + 65 | acc])
def to_alpha(i, acc), do: to_alpha(div(i, 26) - 1, [rem(i, 26) + 65 | acc])
def parse(input) do
for {line, i} <- String.split(input, "\n", trim: true) |> Enum.with_index() do
{x, y, z} =
String.split(line, [",", "~"], trim: true)
|> Enum.map(&String.to_integer/1)
|> Enum.chunk_every(3)
|> Enum.zip_with(fn [a, b] -> Range.new(min(a, b), max(a, b)) end)
|> List.to_tuple()
{to_alpha(i), x, y, z}
end
end
def disjoint?(bricks) do
Enum.all?(bricks, fn {_, ax, ay, az} = brick ->
Enum.filter(bricks, fn other -> other != brick end)
|> Enum.all?(fn {_, bx, by, bz} ->
Range.disjoint?(ax, bx) or
Range.disjoint?(ay, by) or
Range.disjoint?(az, bz)
end)
end)
end
def drop(bricks),
do:
bricks
|> Enum.sort_by(fn {_, _, _, z1.._} -> z1 end)
|> drop([])
def drop([], settled), do: settled
def drop([{_, _, _, 1.._} = brick | bricks], settled),
do: drop(bricks, [brick | settled])
def drop([{id, ax, ay, az} | bricks], settled) do
{_, _, _, _..bz2} =
Enum.filter(settled, fn {_, bx, by, _} ->
not (Range.disjoint?(ax, bx) or
Range.disjoint?(ay, by))
end)
|> Enum.max_by(
fn {_, _, _, _..bz2} -> bz2 end,
fn -> {nil, nil, nil, 0..0} end
)
az1 = bz2 + 1
az2 = bz2 + Range.size(az)
brick = {id, ax, ay, az1..az2}
drop(bricks, [brick | settled])
end
def support(bricks), do: support(bricks, bricks, %{})
def support([], _, supports), do: supports
def support([{_, ax, ay, az1.._} = current | rest], bricks, supports) do
others =
Enum.filter(bricks, fn {_, bx, by, _..bz2} ->
bz2 == az1 - 1 and
not (Range.disjoint?(ax, bx) or
Range.disjoint?(ay, by))
end)
supports =
Map.update(
supports,
current,
%{is_above: others, is_below: []},
fn %{is_above: above} = existing ->
%{existing | is_above: others ++ above}
end
)
supports =
Enum.reduce(others, supports, fn other, supports ->
Map.update(
supports,
other,
%{is_above: [], is_below: [current]},
fn %{is_below: below} = existing ->
%{existing | is_below: [current | below]}
end
)
end)
support(rest, bricks, supports)
end
def free(supports) do
required =
supports
|> Enum.filter(fn {_, %{is_above: is_above}} -> length(is_above) == 1 end)
|> Enum.flat_map(fn {_, %{is_above: is_above}} -> is_above end)
|> MapSet.new()
supports
|> Map.keys()
|> MapSet.new()
|> MapSet.difference(required)
|> Enum.to_list()
end
end
bricks =
input
|> Part1.parse()
|> Part1.drop()
bricks
|> Part1.disjoint?()
|> IO.inspect(label: "disjoint?")
bricks
|> KinoThree.new(z: 15, y: 0, w: 640, h: 480)
|> Kino.render()
bricks
|> Part1.support()
|> Part1.free()
|> length()
Part 2
defmodule Part2 do
def disintegrate(supports) do
supports
|> Map.keys()
|> Enum.map(fn brick ->
brick
|> chain(supports)
|> MapSet.size()
end)
|> Enum.sum()
end
def chain(brick, supports), do: chain([brick], supports, MapSet.new())
def chain([], _, acc), do: acc
def chain([brick | rest], supports, acc) do
destroyed =
supports[brick].is_below
|> Enum.filter(fn above_brick ->
below_bricks = supports[above_brick].is_above
length(below_bricks) == 1 or
Enum.all?(below_bricks, fn below -> MapSet.member?(acc, below) end)
end)
acc =
destroyed
|> MapSet.new()
|> MapSet.union(acc)
chain(rest ++ destroyed, supports, acc)
end
end
input
|> Part1.parse()
|> Part1.drop()
|> Part1.support()
|> Part2.disintegrate()
Kino + three.js
defmodule KinoThree do
use Kino.JS
def new(bricks, opts) do
bricks =
for {id, x, y, z} <- bricks do
%{
id: id,
x: x.first,
y: z.first,
z: y.first,
w: Range.size(x),
h: Range.size(z),
d: Range.size(y)
}
end
%{y: y, h: h} = Enum.max_by(bricks, fn %{y: y, h: h} -> y + h end)
max_y = y + h
z = Keyword.get(opts, :z, 5)
y = Keyword.get(opts, :y, 0)
w = Keyword.get(opts, :w, 640)
h = Keyword.get(opts, :h, 480)
opts = %{bricks: bricks, max_y: max_y, z: z, y: y, w: w, h: h}
Kino.JS.new(__MODULE__, opts)
end
asset "main.js" do
"""
import * as THREE from "https://unpkg.com/three@0.160.0/build/three.module.min.js";
export function init(ctx, opts) {
const renderer = new THREE.WebGLRenderer();
renderer.setSize(opts.w, opts.h);
ctx.root.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, opts.w / opts.h, 0.1, 1000);
for (const {id, x, y, z, w, h, d} of opts.bricks) {
const geometry = new THREE.BoxGeometry(w, h, d);
const color = new THREE.Color(Math.random(), Math.random(), Math.random());
const material = new THREE.MeshBasicMaterial({ color });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.x = x + w / 2;
mesh.position.y = y + h / 2;
mesh.position.z = z + d / 2;
scene.add(mesh);
}
let dy = opts.max_y / 1000;
camera.position.y = opts.y;
camera.position.z = opts.z;
function animate() {
requestAnimationFrame(animate);
scene.rotation.y += 0.01;
camera.position.y += dy;
if (camera.position.y > opts.max_y) {
camera.position.y = opts.max_y;
dy = -dy;
} else if (camera.position.y < 0) {
camera.position.y = 0;
dy = -dy;
}
renderer.render(scene, camera);
}
animate();
}
"""
end
end