Advent of Code 2024 - Day 4

Here is my solution for day 4:

https://github.com/bjorng/advent-of-code/blob/main/2024/day04/lib/day04.ex

Today looked more intimidating than it actually was. General strategy is to bruteforce all directions. We can minimize bruteforce area by only searching when ā€œXā€ (for part 1) or ā€œAā€ (for part 2):

defmodule AOC.Y2024.Day4 do
  @moduledoc false

  use AOC.Solution

  @dirs [
    {-1, 0},
    {1, 0},
    {0, -1},
    {0, 1},
    {-1, -1},
    {-1, 1},
    {1, -1},
    {1, 1}
  ]

  @impl true
  def load_data() do
    Data.load_day_as_grid(2024, 4)
  end

  @impl true
  def part_one({grid, _, _}) do
    grid
    |> Enum.filter(fn {_, v} -> v == "X" end)
    |> General.map_sum(fn {coord, _} -> count_xmas(grid, coord) end)
  end

  @impl true
  def part_two({grid, _, _}) do
    grid
    |> Enum.filter(fn {_, v} -> v == "A" end)
    |> Enum.count(fn {coord, _} -> has_x_mas(grid, coord) end)
  end

  defp has_x_mas(grid, {r, c}) do
    [tl, tr, bl, br] =
      @dirs
      |> Enum.slice(4..-1//1)
      |> Enum.map(fn {dr, dc} -> {r + dr, c + dc} end)
      |> Enum.map(fn coord -> Map.get(grid, coord, ".") end)

    ([tl, br] == ["M", "S"] or [tl, br] == ["S", "M"]) and
      ([tr, bl] == ["M", "S"] or [tr, bl] == ["S", "M"])
  end

  defp count_xmas(grid, {r, c}) do
    for {dr, dc} <- @dirs do
      0..3
      |> Enum.map(fn j -> {r + dr * j, c + dc * j} end)
      |> Enum.map_join(fn coord -> Map.get(grid, coord, ".") end)
    end
    |> Enum.count(fn v -> v == "XMAS" end)
  end
end

Bruteforcing is fun, indeed!

https://github.com/sevenseacat/advent_of_code/blob/main/lib/y2024/day04.ex

I feel part 2 is actually easier.

defmodule AoC2024.Day04 do
  def part_1(grid) do
    directions =
      for di <- -1..1,
          dj <- -1..1,
          di != 0 or dj != 0,
          do: {di, dj}

    directions
    |> Enum.map(fn {di, dj} ->
      Enum.count(Map.keys(grid), fn coord ->
        coord
        |> Stream.iterate(fn {i, j} -> {i + di, j + dj} end)
        |> Stream.take(4)
        |> Enum.map(&grid[&1])
        |> Kernel.==(~c"XMAS")
      end)
    end)
    |> Enum.sum()
  end

  def part_2(grid) do
    Enum.count(grid, fn
      {{i, j}, ?A} ->
        [grid[{i - 1, j - 1}], grid[{i + 1, j + 1}]] in [~c"MS", ~c"SM"] and
        [grid[{i - 1, j + 1}], grid[{i + 1, j - 1}]] in [~c"MS", ~c"SM"]
        
      _ ->
        false
    end)
  end
end

where grid is built like this:

charlists = puzzle_input |> String.split() |> Enum.map(&String.to_charlist/1)

grid =
  for {row, i} <- Enum.with_index(charlists),
      {char, j} <- Enum.with_index(row),
      into: %{},
      do: {{i, j}, char}

I think there’s a subtle bug in your part 2 though - what if:

# this is "MS"
[grid[{i - 1, j - 1}], grid[{i + 1, j + 1}]] in [~c"MS", ~c"SM"]

# and this is "SM"?
[grid[{i - 1, j + 1}], grid[{i + 1, j - 1}]] in [~c"MS", ~c"SM"]

Or I may have misunderstood your algorithm here!

Actually, the line before and checks the cases like

M     S
 A     A
  S     M

And the line after and checks

  M     S
 A     A
S     M

My solution relies on my Grid module so it may not be very clear.

But I can see you guys have very short and contained solutions!

defmodule AdventOfCode.Solutions.Y24.Day04 do
  alias AdventOfCode.Grid
  alias AoC.Input

  def parse(input, _part) do
    input
    |> Input.stream!(trim: true)
    |> Grid.parse_stream(fn <<c>> -> {:ok, c} end)
  end

  def part_one(grid) do
    grid
    |> Enum.filter(fn {_, c} -> c == ?X end)
    |> Enum.map(&elem(&1, 0))
    |> Enum.map(&count_xmas(&1, grid))
    |> Enum.sum()
  end

  defp count_xmas(pos, grid) do
    for dir <- Grid.directions(8), step <- 0..3 do
      Grid.translate(pos, dir, step)
    end
    |> Enum.chunk_every(4)
    |> Enum.map(&to_word(&1, grid))
    |> Enum.filter(&(&1 == ~c"XMAS"))
    |> length()
  end

  defp to_word(coords, grid) do
    Enum.map(coords, &Map.get(grid, &1))
  end

  def part_two(grid) do
    grid
    |> Enum.filter(fn {_, c} -> c == ?A end)
    |> Enum.map(&elem(&1, 0))
    |> Enum.count(&is_cross(&1, grid))
  end

  defp is_cross(pos, grid) do
    nw = Map.get(grid, Grid.translate(pos, :nw))
    se = Map.get(grid, Grid.translate(pos, :se))
    sw = Map.get(grid, Grid.translate(pos, :sw))
    ne = Map.get(grid, Grid.translate(pos, :ne))

    ((nw == ?M and se == ?S) or (nw == ?S and se == ?M)) and
      ((ne == ?M and sw == ?S) or (ne == ?S and sw == ?M))
  end
end

Edit: filtering on start == X for part 1 for speed, thank you :slight_smile:

Oh I really like the idea of having helpers in the grid for moving in different directions! Much less magic -1 and 1 everywhere :smiley:

#!/usr/bin/env elixir

# Aoc 2024. day 4.

defmodule Part1 do
  @spec xmas(map(), non_neg_integer(), non_neg_integer()) :: non_neg_integer()
  def xmas(m, r, c) do
    b2i(unquote(:"xmas↑")(m, r, c)) +
    b2i(unquote(:"xmas↗")(m, r, c)) +
    b2i(unquote(:"xmas→")(m, r, c)) +
    b2i(unquote(:"xmasā†˜")(m, r, c)) +
    b2i(unquote(:"xmas↓")(m, r, c)) +
    b2i(unquote(:"xmas↙")(m, r, c)) +
    b2i(unquote(:"xmas←")(m, r, c)) +
    b2i(unquote(:"xmas↖")(m, r, c))
  end

  defp unquote(:"xmas↑")(m, r, c),
    do: m[{r,c}] == "X" && m[{r-1,c}] == "M" && m[{r-2,c}] == "A" && m[{r-3,c}] == "S"
  defp unquote(:"xmas↗")(m, r, c),
    do: m[{r,c}] == "X" && m[{r-1,c+1}] == "M" && m[{r-2,c+2}] == "A" && m[{r-3,c+3}] == "S"
  defp unquote(:"xmas→")(m, r, c),
    do: m[{r,c}] == "X" && m[{r,c+1}] == "M" && m[{r,c+2}] == "A" && m[{r,c+3}] == "S"
  defp unquote(:"xmasā†˜")(m, r, c),
    do: m[{r,c}] == "X" && m[{r+1,c+1}] == "M" && m[{r+2,c+2}] == "A" && m[{r+3,c+3}] == "S"
  defp unquote(:"xmas↓")(m, r, c),
    do: m[{r,c}] == "X" && m[{r+1,c}] == "M" && m[{r+2,c}] == "A" && m[{r+3,c}] == "S"
  defp unquote(:"xmas↙")(m, r, c),
    do: m[{r,c}] == "X" && m[{r+1,c-1}] == "M" && m[{r+2,c-2}] == "A" && m[{r+3,c-3}] == "S"
  defp unquote(:"xmas←")(m, r, c),
    do: m[{r,c}] == "X" && m[{r,c-1}] == "M" && m[{r,c-2}] == "A" && m[{r,c-3}] == "S"
  defp unquote(:"xmas↖")(m, r, c),
    do: m[{r,c}] == "X" && m[{r-1,c-1}] == "M" && m[{r-2,c-2}] == "A" && m[{r-3,c-3}] == "S"

    def b2i(true), do: 1
  def b2i(false), do: 0
end

map = File.stream!("../day04.txt")
  |> Stream.with_index()
  |> Enum.reduce(%{}, fn {line, row}, map ->
    line
    |> String.trim_trailing()
    |> String.codepoints()
    |> Enum.with_index()
    |> Enum.reduce(map, fn {c, col}, map -> Map.put(map, {row, col}, c) end)
  end)

# part 1
Enum.reduce(map, 0, fn {{r,c},_char}, n -> n+Part1.xmas(map, r, c) end)
|> IO.inspect(label: "part 1")

defmodule Part2 do
  @spec xmas(map(), non_neg_integer(), non_neg_integer()) :: boolean()
  def xmas(m, r, c) do
    unquote(:"xmas↗↙")(m, r, c) && unquote(:"xmasā†–ā†˜")(m, r, c)
  end
  defp unquote(:"xmas↗↙")(m, r, c),
    do:  m[{r,c}] == "A" && ((m[{r+1,c-1}] == "M" && m[{r-1,c+1}] == "S") || (m[{r+1,c-1}] == "S" && m[{r-1,c+1}] == "M"))
  defp unquote(:"xmasā†–ā†˜")(m, r, c),
    do:  m[{r,c}] == "A" && ((m[{r+1,c+1}] == "M" && m[{r-1,c-1}] == "S") || (m[{r+1,c+1}] == "S" && m[{r-1,c-1}] == "M"))
end

# part 2
Enum.reduce(map, 0, fn {{r,c},_char}, n -> n+Part1.b2i(Part2.xmas(map, r, c)) end)
|> IO.inspect(label: "part 2")

Part1

defmodule Advent.Y2024.Day04.Part1 do
  def run(puzzle) do
    grid = parse(puzzle)

    grid
    |> find_char("X")
    |> Enum.map(fn pos ->
      pos |> directions() |> Enum.count(&(multiple_get(grid, &1) == "MAS"))
    end)
    |> Enum.sum()
  end

  def parse(puzzle) do
    for {row, y} <- puzzle |> String.split("\n") |> Enum.with_index(),
        {char, x} <- row |> String.graphemes() |> Enum.with_index(),
        into: %{},
        do: {{x, y}, char}
  end

  def find_char(grid, match) do
    grid |> Enum.filter(fn {_, c} -> c == match end) |> Enum.map(&elem(&1, 0))
  end

  def directions({x, y}) do
    [
      [{x + 1, y}, {x + 2, y}, {x + 3, y}],
      [{x - 1, y}, {x - 2, y}, {x - 3, y}],
      [{x, y + 1}, {x, y + 2}, {x, y + 3}],
      [{x, y - 1}, {x, y - 2}, {x, y - 3}],
      [{x + 1, y + 1}, {x + 2, y + 2}, {x + 3, y + 3}],
      [{x - 1, y - 1}, {x - 2, y - 2}, {x - 3, y - 3}],
      [{x - 1, y + 1}, {x - 2, y + 2}, {x - 3, y + 3}],
      [{x + 1, y - 1}, {x + 2, y - 2}, {x + 3, y - 3}]
    ]
  end

  def multiple_get(grid, positions) do
    Enum.map_join(positions, &Map.get(grid, &1))
  end
end

Part2

defmodule Advent.Y2024.Day04.Part2 do
  alias Advent.Y2024.Day04.Part1

  def run(puzzle) do
    grid = Part1.parse(puzzle)
    a_positions = Part1.find_char(grid, "A")
    Enum.count(a_positions, &xmas?(grid, &1))
  end

  def xmas?(grid, {x, y}) do
    grid
    |> Part1.multiple_get([{x - 1, y - 1}, {x + 1, y - 1}, {x + 1, y + 1}, {x - 1, y + 1}])
    |> Kernel.in(["MMSS", "MSSM", "MSSM", "SSMM", "SMMS"])
  end
end

Using NIFs for the fun of it :slight_smile: Using macOS 14.7.1 and Homebrew.

Compilation.

export ERL_INCLUDE_PATH=/opt/homebrew/lib/erlang/usr/include
cc -fPIC -I $ERL_INCLUDE_PATH -dynamiclib -undefined dynamic_lookup -o day04_array.so day04_array.c
elixirc day04_array.ex

First timing with the map is pure Elixir (cfr. above my solution) and the other ones using the array is with C code. Since the array is stateful, we do not need to reduce using it as an accumulator. But I copy/pasted the Elixir solution and left it as is.

$ ./day04.exs
Set up the map in 7205 µs
Part 1 (map): 2427. Job done in 4360 µs
Part 2 (map): 1900. Job done in 2867 µs
Number of rows/cols: 140
Set up the array in 821 µs
Part 1 (array): 2427. Job done in 576 µs
Part 2 (array): 1900. Job done in 502 µs

Using C to solve day 4. The file is day04.ex.

# setting up the array
%File.Stat{size: size} = File.stat!("../day04.txt")
# same number of rows and columns + 1 \n per row
rows = trunc((-1 + :math.sqrt(1+4*size)) / 2) # positive solution of quadratic equation
IO.puts "Number of rows/cols: #{rows}"

start = System.monotonic_time(:microsecond)
array = File.stream!("../day04.txt")
  |> Stream.with_index()
  |> Enum.reduce(Day04Array.new(rows, rows), fn {line, row}, array ->
    line
    |> String.trim_trailing()
    |> String.to_charlist()
    |> Enum.with_index()
    |> Enum.reduce(array, fn {c, col}, array -> Day04Array.set(array, row, col, c) end)
  end)
elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Set up the array in #{elapsed} µs"

# part 1. array
start = System.monotonic_time(:microsecond)
n = for r <- 0..rows-1, c <- 0..rows-1, reduce: 0 do
  n -> n + Day04Array.count_xmas(array, r, c)
end
elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Part 1 (array): #{n}. Job done in #{elapsed} µs"

# part 2. array
start = System.monotonic_time(:microsecond)
n = for r <- 0..rows-1, c <- 0..rows-1, reduce: 0 do
  n -> n + Day04Array.is_xmas(array, r, c)
end
elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Part 2 (array): #{n}. Job done in #{elapsed} µs"

day04_array.ex

defmodule Day04Array do
  @on_load :load_nifs

  def load_nifs do
    :erlang.load_nif(~c"./day04_array", 0)
  end

  def new(_size1, _size2) do
    raise "NIF new/2 not implemented"
  end

  def new(_size1, _size2, _value) do
    raise "NIF new/3 not implemented"
  end

  def get(_array, _index1, _index2) do
    raise "NIF get/3 not implemented"
  end

  def set(_array, _index1, _index2, _value) do
    raise "NIF set/4 not implemented"
  end

  def count_xmas(_array, _row, _col) do
    raise "NIF count_xmas/3 not implemented"
  end

  def is_xmas(_array, _row, _col) do
    raise "NIF is_xmas/3 not implemented"
  end
end

day04_array.c

#include "erl_nif.h"
#include <limits.h>

ErlNifResourceType* RES_TYPE;

typedef struct {
  int* array;
  unsigned int size1;
  unsigned int size2;
} array_t;

void array_destructor(ErlNifEnv* env, void* res) { free(((array_t*)res)->array); }

int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) {
  int flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER;
  RES_TYPE = enif_open_resource_type(env, NULL, "day04_array", array_destructor, flags, NULL);
  if (RES_TYPE == NULL) return -1;
  return 0;
}

static ERL_NIF_TERM new(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  if (argc != 2) return enif_make_badarg(env);

  unsigned int size1, size2;
  if (!enif_get_uint(env, argv[0], &size1)) return enif_make_badarg(env);
  if (!enif_get_uint(env, argv[1], &size2)) return enif_make_badarg(env);

  array_t* res = enif_alloc_resource(RES_TYPE, sizeof(array_t));

  res->size1 = size1;
  res->size2 = size2;
  res->array = malloc(size1*size2*sizeof(int));
  if (res->array == NULL) return enif_make_badarg(env);

  ERL_NIF_TERM term = enif_make_resource(env, res);
  enif_release_resource(res);

  return term;
}

static ERL_NIF_TERM get(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  if (argc != 3) return enif_make_badarg(env);
  unsigned int idx1, idx2;
  if (!enif_get_uint(env, argv[1], &idx1)) return enif_make_badarg(env);
  if (!enif_get_uint(env, argv[2], &idx2)) return enif_make_badarg(env);
  array_t* res;
  if (!enif_get_resource(env, argv[0], RES_TYPE, (void**) &res)) return enif_make_badarg(env);
  if (idx1 < 0 || idx1 >= res->size1 || idx2 < 0 || idx2 >= res->size2 ) return enif_make_badarg(env);
  return enif_make_int(env, *(res->array + idx1*res->size2 + idx2));
}

static ERL_NIF_TERM set(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  if (argc != 4) return enif_make_badarg(env);
  unsigned int idx1, idx2;
  if (!enif_get_uint(env, argv[1], &idx1)) return enif_make_badarg(env);
  if (!enif_get_uint(env, argv[2], &idx2)) return enif_make_badarg(env);
  int value;
  if (!enif_get_int(env, argv[3], &value)) return enif_make_badarg(env);
  array_t* res;
  if (!enif_get_resource(env, argv[0], RES_TYPE, (void**) &res)) return enif_make_badarg(env);
  if (idx1 < 0 || idx1 >= res->size1 || idx2 < 0 || idx2 >= res->size2 ) return enif_make_badarg(env);
  *(res->array + idx1*res->size2 + idx2) = value;
  return argv[0];
}

static ERL_NIF_TERM count_xmas(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  if (argc != 3) return enif_make_badarg(env);

  int r, c;
  if (!enif_get_int(env, argv[1], &r)) return enif_make_badarg(env);
  if (!enif_get_int(env, argv[2], &c)) return enif_make_badarg(env);

  array_t* res;
  if (!enif_get_resource(env, argv[0], RES_TYPE, (void**) &res)) return enif_make_badarg(env);

  if (*(res->array + r*res->size2 + c) != 'X') return enif_make_int(env, 0);
  int count = 0;
  if (r - 3 >= 0) count += *(res->array + (r-1)*res->size2 + c) == 'M' && *(res->array + (r-2)*res->size2 + c) == 'A' && *(res->array + (r-3)*res->size2 + c) == 'S';
  if (r - 3 >= 0 && c + 3 < res->size2) count += *(res->array + (r-1)*res->size2 + c+1) == 'M' && *(res->array + (r-2)*res->size2 + c+2) == 'A' && *(res->array + (r-3)*res->size2 + c+3) == 'S';
  if (c + 3 < res->size2) count += *(res->array + r*res->size2 + c+1) == 'M' && *(res->array + r*res->size2 + c+2) == 'A' && *(res->array + r*res->size2 + c+3) == 'S';
  if (r + 3 < res->size1 && c + 3 < res->size2) count += *(res->array + (r+1)*res->size2 + c+1) == 'M' && *(res->array + (r+2)*res->size2 + c+2) == 'A' && *(res->array + (r+3)*res->size2 + c+3) == 'S';
  if (r + 3 < res->size1) count += *(res->array + (r+1)*res->size2 + c) == 'M' && *(res->array + (r+2)*res->size2 + c) == 'A' && *(res->array + (r+3)*res->size2 + c) == 'S';
  if (r + 3 < res->size1 && c - 3 >= 0) count += *(res->array + (r+1)*res->size2 + c-1) == 'M' && *(res->array + (r+2)*res->size2 + c-2) == 'A' && *(res->array + (r+3)*res->size2 + c-3) == 'S';
  if (c - 3 >= 0) count += *(res->array + r*res->size2 + c-1) == 'M' && *(res->array + r*res->size2 + c-2) == 'A' && *(res->array + r*res->size2 + c-3) == 'S';
  if (r - 3 >= 0 && c - 3 >= 0) count += *(res->array + (r-1)*res->size2 + c-1) == 'M' && *(res->array + (r-2)*res->size2 + c-2) == 'A' && *(res->array + (r-3)*res->size2 + c-3) == 'S';
  return enif_make_int(env, count);
}

static ERL_NIF_TERM is_xmas(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  if (argc != 3) return enif_make_badarg(env);

  int r, c;
  if (!enif_get_int(env, argv[1], &r)) return enif_make_badarg(env);
  if (!enif_get_int(env, argv[2], &c)) return enif_make_badarg(env);

  array_t* res;
  if (!enif_get_resource(env, argv[0], RES_TYPE, (void**) &res)) return enif_make_badarg(env);

  if (*(res->array + r*res->size2 + c) != 'A') return enif_make_int(env, 0);
  int b = 0;
  if (r-1>=0 && r+1<res->size1 && c-1>=0 && c+1<res->size2) {
    b = ((*(res->array + (r+1)*res->size2 + c-1) == 'M' && *(res->array + (r-1)*res->size2 + c+1) == 'S') ||
         (*(res->array + (r+1)*res->size2 + c-1) == 'S' && *(res->array + (r-1)*res->size2 + c+1) == 'M')) &&
        ((*(res->array + (r+1)*res->size2 + c+1) == 'M' && *(res->array + (r-1)*res->size2 + c-1) == 'S') ||
         (*(res->array + (r+1)*res->size2 + c+1) == 'S' && *(res->array + (r-1)*res->size2 + c-1) == 'M'));
  }
  return enif_make_int(env, b);
}

static ErlNifFunc nif_funcs[] =
{
  {"new", 2, new},
  {"new", 3, new},
  {"get", 3, get},
  {"set", 4, set},
  {"count_xmas", 3, count_xmas},
  {"is_xmas", 3, is_xmas}
};

ERL_NIF_INIT(Elixir.Day04Array, nif_funcs, load, NULL, NULL, NULL);

EDIT: changed the Elixir code to check for X or A first and avoid doing extra work if absent. It is more fair regarding the timing comparison with C.

Nothing too fancy. I did some basic optimizations like checking only from X (in p1) and A (in p2), plus reduce_while in p1 to stop early if there’s no matches.

https://github.com/mexicat/aoc-2024/blob/main/lib/aoc/day_04.ex

Added some padding and made sub matrixes. Not optimal at all. :weary:

defmodule Aoc2024.Solutions.Y24.Day04 do
  alias AoC.Input

  def parse(input, part) do
    input =
      input
      |> Input.stream!(trim: true)
      |> Enum.map(&String.codepoints(&1 <> "YYY"))
      |> (fn input -> input ++ padding(length(hd(input))) end).()

    n = if part == :part_one, do: 4, else: 3

    for x <- 0..(length(input) - n), y <- 0..(length(hd(input)) - n) do
      {_, rest_x} = Enum.split(input, x)
      rest_rows = Enum.take(rest_x, n)

      Enum.map(rest_rows, fn rest_row ->
        {_, rest_y} = Enum.split(rest_row, y)
        Enum.take(rest_y, n)
      end)
    end
  end

  def part_one(problem), do: Enum.reduce(problem, 0, &(&2 + count_xmas(&1)))

  def part_two(problem), do: Enum.reduce(problem, 0, &(&2 + count_mas(&1)))

  defp count_xmas([[a1, a2, a3, a4], [b1, b2, b3, _b4], [c1, c2, c3, _c4], [d1, _d2, _d3, d4]]) do
    list = [
      a1 <> a2 <> a3 <> a4,
      a1 <> b1 <> c1 <> d1,
      a1 <> b2 <> c3 <> d4,
      a4 <> b3 <> c2 <> d1
    ]

    Enum.reduce(list, 0, fn e, acc ->
      case e do
        "XMAS" -> acc + 1
        "SAMX" -> acc + 1
        _ -> acc
      end
    end)
  end

  defp count_mas([[a1, _a2, a3], [_b1, b2, _b3], [c1, _c3, c3]]) do
    case b2 <> a1 <> a3 <> c1 <> c3 do
      "AMMSS" -> 1
      "ASSMM" -> 1
      "ASMSM" -> 1
      "AMSMS" -> 1
      _ -> 0
    end
  end

  defp padding(n) do
    for _ <- 0..2 do
      for _ <- 0..(n - 1) do
        "Y"
      end
    end
  end
end
Solution for 2024 day 4
part_one: 2427 in 82.75ms
part_two: 1900 in 49.76ms

I could try nx now that I thought about it… :thinking:

Ugh. Grids. Just grunt work.

I decided to use :array to represent the grid as I know the O(log n) lookup of maps are not ideal. Combined with for, it’s not very elixir-like. I agree with @lkuty that native code is probably the way to go for this kind of thing. I might come back to this when I have more free time next year to try it with Zigler.

Four of these for part 1:

for y <- 0..(n - 1), x <- 0..(n - 4), reduce: 0 do
  count ->
    for x_offset <- 0..(4 - 1), into: "" do
      :array.get(x + x_offset, :array.get(y, grid))
    end
    |> consider(count)
end

and some similar stuff in part 2.

On my M1 the times were 15ms and 4ms. Not sure how that compares to the Map solutions.

https://git.adamu.jp/adam/AdventOfCode/src/branch/main/2024/day4.exs

It took me some time to get to the solution today. I solved part 2 by counting the diagonal occurrences of ā€œMASā€ in the 3x3 grid around the current position. If there are more than one then it is a valid X. I wondered if it was possible to have more than two ā€œMASā€ occurrences, but it is not because if two edges start with ā€œMā€ then the other two edges must end with ā€œSā€ obviously :sweat_smile:

For both solutions, I construct the full word / sequence before comparing it with ā€œMASā€ or "XMAS. An optimization would be to stop earlier, e.g., if the word doesn’t start with X or M at all…

Source Code:

https://github.com/Flo0807/adventofcode/blob/main/2024/04.livemd

It looks like Erlang arrays use a tree (trie maybe?) data structure and have O(log n) time complexity. See otp/lib/stdlib/src/array.erl at 44ffe8811dfcf3d2fe04d530c6e8fac5ca384e02 Ā· erlang/otp Ā· GitHub

I ā€œcheatedā€ a little bit because the C implementation is not just for 2D arrays of ints but it is specifically made for that problem. It allows to avoid many round-trips between Elixir and C as we check if we have an XMAS candidate. I could have gone all-in with C but then the Elixir code would have just been some sort of a driver for the C code. Would have been faster for sure.

Haha, oh well. I wonder why that info hidden in a comment instead of being in the moduledocs (which I read before starting, but didn’t find any information about performance…)

Yes, it should be in the doc of the arrays module. This is an important info.

For part 1 I rotates the matrix instead of looking in differente directions.
And for part 2, I went with a sliding window using Enum,chunks =)


defmodule Smaoc.Solution.Day4 do
  def solve(:part1, input) do
    matrix = parse_input(input)

    [
      [],
      [:reverse],
      [:transpose],
      [:transpose, :reverse],
      [:rotate45],
      [:rotate45, :reverse],
      [:reverse, :rotate45],
      [:reverse, :rotate45, :reverse]
    ]
    |> Enum.map(
      &(matrix
        |> apply_operations(&1)
        |> count_XMAS)
    )
    |> Enum.sum()
  end

  def solve(:part2, input) do
    valid_patterns = [
      ~r/M.M.A.S.S/,
      ~r/M.S.A.M.S/,
      ~r/S.M.A.S.M/,
      ~r/S.S.A.M.M/
    ]

    input
    |> parse_input()
    |> Enum.chunk_every(3, 1, :discard)
    |> Enum.map(fn row ->
      row
      |> transpose()
      |> Enum.chunk_every(3, 1, :discard)
      |> Enum.count(fn chunk ->
        Enum.any?(valid_patterns, &Regex.match?(&1, to_string(chunk)))
      end)
    end)
    |> Enum.sum()
  end

  defp parse_input(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(&String.split(&1, "", trim: true))
  end

  def reverse(matrix), do: Enum.map(matrix, &Enum.reverse/1)

  def transpose(matrix) do
    matrix
    |> Enum.zip()
    |> Enum.map(&Tuple.to_list/1)
  end

  def rotate45(matrix) do
    for(
      {row, i} <- Enum.with_index(matrix),
      {v, j} <- Enum.with_index(row),
      do: {i + j, v}
    )
    |> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
    |> Enum.sort()
    |> Enum.map(&elem(&1, 1))
  end

  def count_XMAS(matrix) do
    matrix
    |> Enum.map(fn l ->
      ~r/XMAS/
      |> Regex.scan(to_string(l))
      |> Enum.count()
    end)
    |> Enum.sum()
  end

  defp apply_operations(matrix, ops) do
    Enum.reduce(ops, matrix, fn op, m -> apply(__MODULE__, op, [m]) end)
  end
end

Here’s mine for today, it’s a bit verbose, so could be tidied up significantly, agreed with others that Part 2 seemed easier than Part 1 in some ways, really waiting for the hammer to drop as the first 4 days have seemed worryingly easy compared to last year.

    defmodule Aoc2024.Solutions.Y24.Day04 do
      alias AoC.Input

      def parse(input, _part) do
        Input.read!(input) |> String.split("\n", trim: true) |> pad_grid()
      end

      def part_one(problem) do
        matrix = problem |> build_matrix()
        matrix |> Enum.to_list() |> Enum.filter(fn {_, v} -> v == "X" end) |> find_xmas(matrix)
      end

      def find_xmas(xs, matrix) do
        Enum.reduce(xs, [], fn {x_coords, _}, acc ->
          acc ++
            [
              find_next(x_coords, :up, ["M", "A", "S"], matrix),
              find_next(x_coords, :down, ["M", "A", "S"], matrix),
              find_next(x_coords, :left, ["M", "A", "S"], matrix),
              find_next(x_coords, :right, ["M", "A", "S"], matrix),
              find_next(x_coords, :diag_up_right, ["M", "A", "S"], matrix),
              find_next(x_coords, :diag_down_right, ["M", "A", "S"], matrix),
              find_next(x_coords, :diag_up_left, ["M", "A", "S"], matrix),
              find_next(x_coords, :diag_down_left, ["M", "A", "S"], matrix)
            ]
        end)
        |> Enum.filter(&(&1 == 1))
        |> Enum.count()
      end

      def find_next(_, _, [], _), do: 1

      def find_next({current_x, current_y}, direction, [next_letter | tail], matrix) do
        next_coords = get_next_coord({current_x, current_y}, direction)
        next_match = Map.get(matrix, next_coords)

        if next_letter == next_match do
          find_next(next_coords, direction, tail, matrix)
        else
          0
        end
      end

      def pad_grid(problem) do
        [first | _] = problem
        width = String.length(first)
        pad_row = 1..width |> Enum.map(fn _ -> "." end) |> Enum.join()
        [pad_row] ++ Enum.map(problem, &".#{&1}.") ++ [pad_row]
      end

      #

      def build_matrix(grid) do
        Enum.with_index(grid)
        |> Enum.reduce(%{}, fn row, acc ->
          build_matrix_row(row, acc)
        end)
      end

      def build_matrix_row({row, row_index}, acc) do
        row
        |> String.graphemes()
        |> Enum.with_index()
        |> Enum.reduce(acc, fn {char, col_index}, acc ->
          Map.put(acc, {col_index, row_index}, char)
        end)
      end

      def get_next_coord({current_x, current_y}, direction) do
        case direction do
          :up -> {current_x, current_y - 1}
          :down -> {current_x, current_y + 1}
          :left -> {current_x - 1, current_y}
          :right -> {current_x + 1, current_y}
          :diag_up_right -> {current_x + 1, current_y - 1}
          :diag_down_right -> {current_x + 1, current_y + 1}
          :diag_up_left -> {current_x - 1, current_y - 1}
          :diag_down_left -> {current_x - 1, current_y + 1}
        end
      end

      def part_two(problem) do
        matrix = problem |> build_matrix()
        matrix |> Enum.to_list() |> Enum.filter(fn {_, v} -> v == "A" end) |> find_x_mases(matrix)
      end

      def find_x_mases(as, matrix) do
        Enum.reduce(as, [], fn a, acc ->
          acc ++ [find_x_mas(a, matrix)]
        end)
        |> Enum.filter(& &1)
        |> Enum.count()
      end

      def find_x_mas({{coord_x, coord_y}, _}, matrix) do
        cross_1 =
          [
            Map.get(matrix, {coord_x - 1, coord_y - 1}),
            Map.get(matrix, {coord_x, coord_y}),
            Map.get(matrix, {coord_x + 1, coord_y + 1})
          ]
          |> List.to_string()

        cross_2 =
          [
            Map.get(matrix, {coord_x + 1, coord_y - 1}),
            Map.get(matrix, {coord_x, coord_y}),
            Map.get(matrix, {coord_x - 1, coord_y + 1})
          ]
          |> List.to_string()

        cross_1 in ["MAS", "SAM"] && cross_2 in ["MAS", "SAM"]
      end
    end

Solution for 2024 day 4
part_one: 2370 in 143.32ms
part_two: 1908 in 26.88ms