bjorng

bjorng

Erlang Core Team

Advent of Code 2023 - Day 22

I went for a brute-force solution, implementing the most straight-forward algorithm I could think of. That initial implementation solved both parts in about 10 minutes.

After that, I did some simple improvements that reduced the time to about two and a half minutes.

Now I will optimize my spare time and take the rest of the day off.

Most Liked

lud

lud

Ok so finally finished it… I’m slow :smiley:

It takes 28 seconds for each part, mostly to make all the bricks fall. Summing each chain reaction takes 1 second.

I’m all ears for a better falling algorithm if anyone reads this ever :smiley:

Edit, ok, got it under 2sec for part 1 and 2.5 for part 2, thanks to this monstruosity:

  defp on_top_of?(
         {_, {xdeb, ydeb, zdeb}, {xfin, yfin, zfin}},
         {_, {xbotdeb, ybotdeb, zbotdeb}, {xbotfin, ybotfin, zbotfin}}
       )
       when (zdeb == zbotdeb + 1 or
               zfin == zbotfin + 1 or
               zdeb == zbotfin + 1 or
               zfin == zbotdeb + 1) and
              (xdeb in xbotdeb..xbotfin or
                 xfin in xbotdeb..xbotfin or
                 xbotdeb in xdeb..xfin or
                 xbotfin in xdeb..xfin) and
              (ydeb in ybotdeb..ybotfin or
                 yfin in ybotdeb..ybotfin or
                 ybotdeb in ydeb..yfin or
                 ybotfin in ydeb..yfin) do
    true
  end

  defp on_top_of?(_, _) do
    false
  end

I LOVE that it is possible to do basic math in guards

Solution

midouest

midouest

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:

output

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
code-shoily

code-shoily

Such a beautiful set of LoCs! Thank you for this, helped me quite a bit!

Where Next?

Popular in Challenges Top

ehayun
I have 2 arrays: a1 can be any combination of value or nil like that a1 = [1,nil,3] and array 2 the same a2 = [4,2, nil] How do I com...
New
sb8244
Note: This topic is to talk about Day 10 of the Advent of Code 2019 . There is a private leaderboard for elixirforum members. You can jo...
New
bjorng
This topic is about Day 17 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/l...
New
bjorng
Note: This topic is to talk about Day 5 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can join...
New
New
Aetherus
This topic is about Day 4 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
bjorng
My solution finishes both parts in 5 seconds on my computer. That time should be possible to reduce by optimizing my rather naive tilt/2 ...
New
bjorng
Note: This topic is to talk about Day 13 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can joi...
New
kwando
Took a while, but another use case for “move vectors” today and pattern matching. :slight_smile: The trick was to first generate a list...
New
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

We're in Beta

About us Mission Statement