Advent of Code 2020 - Day 24

This topic is about Day 24 of the Advent of Code 2020 .

Thanks to @egze, we have a private leaderboard:
https://adventofcode.com/2020/leaderboard/private/view/39276

The join code is:
39276-eeb74f9a

Nice little puzzle to solve before breakfast. Here is my solution.

Nice one. Parsing with elixir is a breeze. I used 2D location instead of 3D and it works.

Haha ! Here is my solution. Instead of x,y,z I used only x,z (labelled q,r).

1 Like

I’m back after skipping a few days.

For part 1, figuring out how to represent the grid, was the most interesting. I also decided to keep it 2D, representing horizontally adjacent neighbours with an x-offset of 2. For the variable-length input, I’ve mostly been using multi-clause recursion for things like this so far, so this time I went with Enum.chunk_while - although ended up using a multi-clause anonymous function inside it anyway…

For part 2, it was basically the same as Day 17. I just updated that answer for the new coordinate system.

Full answer and notes.

I borrowed day 17’s logic for part two, only to encounter > 8 mins runtime due to counting the number of black tiles and comparing it with the relevant range. I optimized that to an Enum.reduce_while/3 so that I can return early once I’ve gone past the max, and shaved off a little more than 50% the time! I backported the change to day 17 and got a good 1/3 improvement (30s to 20s) too, not bad.

I guess where I differ from the solutions above is in the parsing (regex) and the looping (recursion).

defmodule AdventOfCode.Day24 do
  @moduledoc "Day 24"

  defp parse(line), do:
    Enum.map(Regex.scan(~r/([ns]?[ew])/, line), fn [x, _] -> String.to_atom(x) end)

  defp offset(:e, {x, y, z}), do: {x + 1, y - 1, z}
  defp offset(:w, {x, y, z}), do: {x - 1, y + 1, z}
  defp offset(:nw, {x, y, z}), do: {x, y + 1, z - 1}
  defp offset(:ne, {x, y, z}), do: {x + 1, y, z - 1}
  defp offset(:sw, {x, y, z}), do: {x - 1, y, z + 1}
  defp offset(:se, {x, y, z}), do: {x, y - 1, z + 1}

  defp shift(moves), do: Enum.reduce(moves, {0, 0, 0}, &offset/2)

  defp map_tiles(input), do:
    Enum.reduce(input, %{}, fn line, map -> Map.update(map, shift(parse(line)), true, &(!&1)) end)

  def part1(input), do: Enum.count(map_tiles(input), fn {_, black?} -> black? end)

  def nearby?({x1, y1, z1}, {x2, y2, z2}), do: abs(x1 - x2) <= 1 && abs(y1 - y2) <= 1 && abs(z1 - z2) <= 1

  def black?(tiles, min..max), do: fn tile ->
    # this was Enum.count(tiles, &(&1 != tile && nearby?(&1, tile))) in min..max
    Enum.reduce_while(
      tiles,
      0,
      fn x, acc ->
        if x != tile && nearby?(x, tile),
           do: {(if acc + 1 > max, do: :halt, else: :cont), acc + 1},
           else: {:cont, acc}
      end
    ) in min..max
  end

  defp expand(tiles), do:
    MapSet.new(Enum.flat_map(tiles, fn tile -> Enum.map([:e, :w, :nw, :ne, :sw, :se], &(offset(&1, tile))) end))

  defp cycle(tiles, 0), do: Enum.count(tiles)

  defp cycle(tiles, i), do: cycle(
    MapSet.new(Enum.filter(expand(tiles), black?(tiles, 2..2)) ++ Enum.filter(tiles, black?(tiles, 1..2))),
    i - 1
  )

  def part2(input), do:
    map_tiles(input)
    |> Enum.reduce([], fn {tile, black?}, acc -> if black?, do: [tile | acc], else: acc end)
    |> MapSet.new
    |> cycle(100)
end

What a nice day ! Nice and clean with Elixir.

In this journey I found this website : a nice and clean explanation on how to organize coordinates in an hexagonal system.

I did not enjoy part 2 at all.

When I worked on AoC 2017, day 11, I used Python, and I used complex numbers to represent directions –

C = {
    "ne": complex(0.5, 0.5),
    "n": complex(0, 1),
    "nw": complex(-0.5, 0.5),
    "se": complex(0.5, -0.5),
    "s": complex(0, -1),
    "sw": complex(-0.5, -0.5)
}

Since complex numbers are a built-in type in Python, I could put a bunch of them in a list and call the sum function to add them up. In Elixir, I had to do a little more work.

defmodule Day24 do
  @dirs %{
    "w" => {0, -1},
    "e" => {0, 1},
    "nw" => {0.5, -0.5},
    "ne" => {0.5, 0.5},
    "se" => {-0.5, 0.5},
    "sw" => {-0.5, -0.5}
  }

  def readinput() do
    File.read!("24.input.txt")
    |> String.split("\n", trim: true)
    |> Enum.map(fn line ->
      Regex.scan(~r/(se|ne|sw|nw|w|e)/, line, capture: :all_but_first)
      |> List.flatten()
      |> Enum.map(&Map.get(@dirs, &1))
    end)
  end

  def part1(input \\ readinput()) do
    flip(input, %{{0, 0} => :white})
    |> blacktiles()
  end

  def flip([], floor), do: floor

  def flip([path | paths], floor) do
    tile = Enum.reduce(path, {0, 0}, fn {ns, ew}, {nsacc, ewacc} -> {nsacc + ns, ewacc + ew} end)

    case Map.get(floor, tile, :white) do
      :black -> flip(paths, Map.put(floor, tile, :white))
      :white -> flip(paths, Map.put(floor, tile, :black))
    end
  end

  def blacktiles(floor) do
    floor
    |> Map.values()
    |> Enum.count(&(&1 == :black))
  end

  def part2(input \\ readinput()) do
    flip(input, %{{0, 0} => :white})
    |> dayflip(100)
    |> blacktiles()
  end

  def dayflip(floor, 0), do: floor

  def dayflip(floor, count) do
    expand(floor)
    |> Enum.reduce(%{}, fn {tile, color} = t, newfloor ->
      blacks =
        adjtiles(tile)
        |> Enum.map(&Map.get(floor, &1))
        |> Enum.count(&(&1 == :black))

      cond do
        color == :black and (blacks == 0 or blacks > 2) ->
          Map.put(newfloor, tile, :white)

        color == :white and blacks == 2 ->
          Map.put(newfloor, tile, :black)

        true ->
          if t in floor, do: Map.put(newfloor, tile, color), else: newfloor
      end
    end)
    |> dayflip(count - 1)
  end

  def expand(floor) do
    floor
    |> Enum.flat_map(fn {tile, _} -> adjtiles(tile) end)
    |> MapSet.new()
    |> Enum.reduce(%{}, fn tile, newfloor ->
      Map.put(newfloor, tile, Map.get(floor, tile, :white))
    end)
  end

  def adjtiles({ns, ew}) do
    [
      {ns + 0.5, ew + 0.5},
      {ns + 0.5, ew - 0.5},
      {ns - 0.5, ew + 0.5},
      {ns - 0.5, ew - 0.5},
      {ns, ew + 1},
      {ns, ew - 1}
    ]
  end
end

After a day full of eating it was nice to end the day with todays puzzle.

Not being fluent in hexagonal but knowing I only needed to go relative to the center tile I decided to store the tiles in a 2D grid. I used this function to decide where to store them, so every second row was skewed to the right, ever second skewed to the left.

#           {-1, 1}   { 0, 1} 
#      {-1, 0}   { 0, 0}   { 1, 0}
#           {-1,-1}   { 0,-1}
 def get_coordinate({x, y}, dir) do
    case dir do
      :ne -> {x + rem(abs(y), 2), y + 1}
      :se -> {x + rem(abs(y), 2), y - 1}
      :nw -> {x - 1 + rem(abs(y), 2), y + 1}
      :sw -> {x - 1 + rem(abs(y), 2), y - 1}
      :e -> {x + 1, y}
      :w -> {x - 1, y}
    end
  end

The rest of the code

Hi there, I’m a bit late but here is my take on day 24.

Part1 / Part2

It was quite easy in comparison of previous days, which caused me way more issues, damn crab! :crab: