bjorng

bjorng

Erlang Core Team

Advent of Code 2025 - Day 4

defmodule Day04 do
  def part1(input) do
    grid = parse(input)
    Enum.count(removable(grid))
  end

  def part2(input) do
    grid = parse(input)
    remove(grid, 0)
  end

  defp remove(grid, num_removed) do
    case removable(grid) do
      [] ->
        num_removed
      [_|_]=ps ->
        grid = Enum.reduce(ps, grid, &(Map.delete(&2, &1)))
        remove(grid, num_removed + length(ps))
    end
  end

  defp removable(grid) do
    grid
    |> Enum.filter(fn {_position, what} -> what === ?@ end)
    |> Enum.filter(fn {position, _} ->
      adjacent_squares(position)
      |> Enum.count(fn position->
        get_grid(grid, position) === ?@
      end)
      |> then(&(&1 < 4))
    end)
    |> Enum.map(&elem(&1, 0))
  end

  defp get_grid(grid, position) do
    Map.get(grid, position, ?.)
  end

  defp adjacent_squares({row, col}) do
    [{row - 1, col - 1}, {row - 1, col}, {row - 1, col + 1},
     {row, col - 1}, {row, col + 1},
     {row + 1, col - 1}, {row + 1, col}, {row + 1, col + 1}]
  end

  defp parse(input) do
    parse_grid(input)
  end

  defp parse_grid(grid) do
    grid
    |> Enum.with_index
    |> Enum.flat_map(fn {line, row} ->
      String.to_charlist(line)
      |> Enum.with_index
      |> Enum.flat_map(fn {char, col} ->
        position = {row, col}
        [{position, char}]
      end)
    end)
    |> Map.new
  end
end

Most Liked Switch mode

lud

lud

I love immutability because you can just compare a term against its previous version without managing the copy yourself:

case Map.drop(grid, Map.keys(accessibles(grid))) do
  ^grid -> grid
  new_grid -> loop_remove(new_grid)
end

In context:

defmodule AdventOfCode.Solutions.Y25.Day04 do
  alias AoC.Input

  def parse(input, _part) do
    lines = Input.stream!(input, trim: true)

    for {row, y} <- Enum.with_index(lines),
        {"@", x} <- Enum.with_index(String.graphemes(row)),
        reduce: %{},
        do: (grid -> Map.put(grid, {x, y}, true))
  end

  def part_one(grid) do
    map_size(accessibles(grid))
  end

  def part_two(grid) do
    clear_grid = loop_remove(grid)
    map_size(grid) - map_size(clear_grid)
  end

  defp loop_remove(grid) do
    case Map.drop(grid, Map.keys(accessibles(grid))) do
      ^grid -> grid
      new_grid -> loop_remove(new_grid)
    end
  end

  defp accessibles(grid) do
    Map.filter(grid, &less_than_four_neighbors?(&1, grid))
  end

  defp less_than_four_neighbors?({{x, y}, _}, grid) do
    for(nx <- (x - 1)..(x + 1), ny <- (y - 1)..(y + 1), {nx, ny} != {x, y}, do: {nx, ny})
    |> Enum.count(&Map.has_key?(grid, &1))
    |> Kernel.<(4)
  end
end
dompdv

dompdv

This one was pretty straightforward. Using MapSet (and MapSet operations) and the “for” construct, which is more concise than reduce. No optimization at all


defmodule AdventOfCode.Solution.Year2025.Day04 do
  import Enum, only: [flat_map: 2, with_index: 1, sum: 1, filter: 2, count: 1]

  def parse(input) do
    # Create a MapSet of the {row,col} of the rolls
    with_index(String.split(input, "\n", trim: true))
    |> flat_map(fn {line, row} ->
      for {c, col} <- with_index(to_charlist(line)), c == ?@, do: {row, col}
    end)
    |> MapSet.new()
  end

  def vec_sum({a, b}, {c, d}), do: {a + c, b + d}

  @around for r <- -1..1, c <- -1..1, {r, c} != {0, 0}, do: {r, c}
  def liftable(roll, rolls) do
    sum(for delta <- @around, vec_sum(roll, delta) in rolls, do: 1) < 4
  end

  def part1(input) do
    rolls = parse(input)
    rolls |> filter(&liftable(&1, rolls)) |> count()
  end

  def remove_all(rolls, counter \\ 0) do
    # Identify the removable rolls
    removable = for roll <- rolls, liftable(roll, rolls), into: MapSet.new(), do: roll
    # If nothing to remove, we're done, else remove the removable rolls and increment the counter
    if MapSet.size(removable) == 0,
      do: counter,
      else: remove_all(MapSet.difference(rolls, removable), counter + MapSet.size(removable))
  end

  def part2(input), do: input |> parse() |> remove_all()
end
mudasobwa

mudasobwa

Creator of Cure

I did my best to never construct a matrix, binaries rock. Also, Stream.iterate(&calc/1) is a lovely approach to code reuse.

  defmodule Day4 do
    @input "day4_1.input" |> File.read!() |> String.trim()

    defp read_input(input) do
      input
      |> String.split(["\s", "\n"], trim: true)
      |> then(fn [h | t] ->
        stub = "." |> List.duplicate(byte_size(h)) |> Enum.join()
        [stub, h | t] ++ [stub]
      end)
      |> Enum.map(&("." <> &1 <> "."))
    end

    def calc({input, acc} \\ {@input, 0}) do
      input
      |> read_input()
      |> Enum.chunk_every(3, 1, :discard)
      |> Enum.reduce({0, []}, fn [h, l, t], {count, res} ->
        res = ["" | res]

        Enum.reduce(1..(byte_size(l) - 2), {count, res}, fn idx, {count, [hres | tres]} ->
          with <<_::binary-size(idx - 1), r1::binary-size(3), _::binary>> <- h,
               <<_::binary-size(idx - 1), r21::binary-size(1), r22::binary-size(1),
                 r23::binary-size(1), _::binary>> <- l,
               <<_::binary-size(idx - 1), r3::binary-size(3), _::binary>> <- t,
               false <- r22 == "@" and String.count(r1 <> r21 <> r23 <> r3, "@") < 4,
               do: {count, [r22 <> hres | tres]},
               else: (_ -> {count + 1, ["x" <> hres | tres]})
        end)
      end)
      |> then(fn {count, data} -> {Enum.join(data, "\n"), acc + count} end)
    end

    def calc_2(input \\ @input) do
      {input, 0}
      |> Stream.iterate(&calc/1)
      |> Enum.reduce_while({0, -1}, fn
        {_, prev}, {acc, prev} -> {:halt, acc}
        {_, count}, {acc, _} -> {:cont, {count, acc}}
      end)
    end
  end

Last Post!

billylanchantin

billylanchantin

defmodule Day04 do
  def part1(file) when is_binary(file), do: file |> to_grid() |> part1()
  def part1(%{} = grid), do: Enum.count(grid, &accessible?(grid, &1))

  def to_grid(file), do: file |> Util.file_to_char_map() |> Map.filter(fn {_, v} -> v == ?@ end)
  def accessible?(grid, {pos, _}), do: Enum.count(adj(pos), &(grid[&1] == ?@)) < 4
  def adj({x, y}), do: for(dx <- -1..1, dy <- -1..1, {dx, dy} != {0, 0}, do: {x + dx, y + dy})

  def part2(file) do
    to_grid(file)
    |> Stream.unfold(fn grid ->
      {count, new_grid} = remove_accessible(grid)
      if count > 0, do: {count, new_grid}, else: nil
    end)
    |> Enum.sum()
  end

  def remove_accessible(grid) do
    removable = grid |> Enum.filter(&accessible?(grid, &1)) |> Enum.map(&elem(&1, 0))
    {length(removable), Enum.reduce(removable, grid, &Map.delete(&2, &1))}
  end
end

Where Next?

Trending in Challenges Top

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement