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.

https://github.com/bjorng/advent-of-code-2023/blob/main/day22/lib/day22.ex

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

Aetherus
Hello, guys. I’m back again, but only for the weekends, maybe. This topic is about Day 13 of the Advent of Code 2020 . Thanks to @egze,...
New
bjorng
This topic is about Day 15 of the Advent of Code 2021. We have a private leaderboard (shared with users of Erlang Forums): https://adve...
New
christhekeele
Continuation of Advent of Code 2022​:christmas_tree:, Day 1: Day 2! Leaderboard:
New
Aetherus
Today’s problem is really tense. I don’t think I can do it without libgraph.
New
coen.bakker
Since I started using Elixir, I have benefited greatly from being able to study various open-source projects. The codebase of LiveBook, i...
New
liamcmitchell
A frustrating one for me. I spent a long time trying to understand why some combinations resulted in fewer presses and struggled to keep ...
New
seeplusplus
Hello all, hopefully I post this before someone else does and I don’t dupe. IMO Day 4 was much easier than Day 3 (yay, I can sleep befor...
New
mattbaker
I’m having so much fun working on the “Protohackers” challenges, I never got into Advent of Code much but this has been amazing. The chal...
New
connorlay
Note by the Moderators: This topic is for general discussion about the Advent of Code 2018. To prevent people from being spoiled about s...
New
Aetherus
Don’t know why the regex ~r/[\W &amp;&amp; [^\.]]/x does not work in Elixir. It works pretty well in Ruby. Anyway, here is my solution: ...
New

Other popular topics Top

msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43622 214
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement