Advent of Code 2024 Day 4 – Ceres Search

Advent of Code 2024 Day 4 – Ceres Search

- 14 mins

Day 4: Ceres Search

Part 1

Today’s puzzle is called “Ceres Search”. The input for today’s puzzle is a file where it represents a word search where we have to find the word XMAS. This is the example input that was given.

MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX

XMAS can appear horizontal, vertical, diagonal, written backwards, or even overlapping with other words. From the example, there are 18 instances of XMAS. Here’s the example where all the letters not involved with XMAS words have been replaced with ..

....XXMAS.
.SAMXMS...
...S..A...
..A.A.MS.X
XMASAMX.MM
X.....XA.A
S.S.S.S.SS
.A.A.A.A.A
..M.M.M.MM
.X.X.XMASX

Our task is to find the number of instances of XMAS in the word search.

My Solution

For my solution, I went a bit overboard with the code needed, so there are a few functions that I will be explaining. But first let’s start with the pseudocode to solve the problem:

  1. Read the input file and store the word search in a 2D list. We will be treating this list as a grid.
  2. For every coordinate in the grid, check if the word XMAS can be formed in any direction (horizontal, vertical, diagonal, and reverse diagonal), starting from that coordinate.
  3. If the word XMAS can be formed, add the amount of times it can be formed to a counter.
  4. Return the counter as the result.

Starting with step 1, let’s read the input file and store the word search in a 2D list.

with open("input.txt") as f:
    lines = f.readlines()
grid = [list(line.strip()) for line in lines]

Easy enough, on to step 2. For every coordinate in the grid, we need to check if the word XMAS can be formed in any direction. To do this, I created a function called get_adjacent_letters that returns the letters adjacent to a given coordinate in the grid.

def get_adjacent_letters(
    coord: Coordinate, grid: list[list[str]]
) -> dict[Directions, tuple[str, Coordinate]]:
    x, y = coord.x, coord.y
    adjacent = {}
    if x > 0:
        adjacent[Directions.LEFT] = (grid[y][x - 1], Coordinate(x - 1, y))
    if x < len(grid[y]) - 1:
        adjacent[Directions.RIGHT] = (grid[y][x + 1], Coordinate(x + 1, y))
    if y > 0:
        adjacent[Directions.UP] = (grid[y - 1][x], Coordinate(x, y - 1))
    if y < len(grid) - 1:
        adjacent[Directions.DOWN] = (grid[y + 1][x], Coordinate(x, y + 1))
    if x > 0 and y > 0:
        adjacent[Directions.UP_LEFT] = (grid[y - 1][x - 1], Coordinate(x - 1, y - 1))
    if x < len(grid[y]) - 1 and y > 0:
        adjacent[Directions.UP_RIGHT] = (grid[y - 1][x + 1], Coordinate(x + 1, y - 1))
    if x > 0 and y < len(grid) - 1:
        adjacent[Directions.DOWN_LEFT] = (grid[y + 1][x - 1], Coordinate(x - 1, y + 1))
    if x < len(grid[y]) - 1 and y < len(grid) - 1:
        adjacent[Directions.DOWN_RIGHT] = (grid[y + 1][x + 1], Coordinate(x + 1, y + 1))
    return adjacent

This function takes a coordinate and the grid as input and returns a dictionary where the keys are the directions relative to the coordinate and the values are tuples containing the letter at the adjacent coordinate as well as the actual coordinate. If the coordinate is at the edge of the grid, the function will not return the adjacent coordinate that would be out of bounds. This function seems like overkill, and it is, but I think it ends up being more readable than if I just hardcoded the directions.

Now that I can get the adjacent letters, I created a function called is_xmas_match that checks if the word XMAS can be formed starting from a given coordinate in a given direction.

def is_xmas_match(
    grid: list[list[str]],
    coord: Coordinate,
    current_match: list[Coordinate],
    current_direction: Directions,
) -> list[Coordinate]:
    xmas = "XMAS"

    adjacent = get_adjacent_letters(coord, grid)
    adjacent_letter = adjacent.get(current_direction, None)
    if adjacent_letter is None:
        return []

    potential_xmas = (
        "".join(grid[c.y][c.x] for c in current_match)
        + grid[coord.y][coord.x]
        + adjacent_letter[0]
    )

    if xmas.startswith(potential_xmas):
        if len(potential_xmas) == len(xmas):
            return current_match + [coord, adjacent_letter[1]]
        return is_xmas_match(
            grid,
            adjacent_letter[1],
            current_match + [coord],
            current_direction,
        )
    return []

This function takes the grid, a coordinate, the current match, and the current direction as input. Let’s break down the function:

Again, I know there’s a lot of overhead in this function, sorry!

Now that we can check if the word XMAS can be formed starting from a given coordinate in a given direction, we can use it to go through every coordinate in the grid in every direction and keep track of the matches.

def xmas_matches1(grid: list[list[str]]) -> int:
    matches = 0

    for y in range(len(grid)):
        for x in range(len(grid[y])):
            for direction in Directions:
                match = is_xmas_match(grid, Coordinate(x, y), [], direction)
                if match:
                    matches += 1

    return matches

Breaking down the function:

Finally, we can call the xmas_matches1 function with the grid we read from the input file to get the number of instances of XMAS.

print(xmas_matches1(grid))

I’ve only included the relevant parts of the code here, but to see my full solution, you can check out my Advent of Code GitHub repository.

Part 2

For part 2, instead of finding the word XMAS, we have to find instances of two MAS words in the shape of an X. For example,

M.S
.A.
M.S

Looking at the example input again with the irrelevant letters replaced with ..

.M.S......
..A..MSMS.
.M.S.MAA..
..A.ASMSM.
.M.S.M....
..........
S.S.S.S.S.
.A.A.A.A..
M.M.M.M.M.
..........

There are 9 instances of X-MAS appearing in the word search. Notice that the instances can overlap, and the X can be formed in any direction.

My Solution

To solve part 2, my solution is less complex than part 1. Again, let’s start with the pseudocode:

  1. Read the input file and store the word search in a 2D list. It’s treated as a grid.
  2. An X-MAS instance can only be formed with an A in the middle, so iterate over all the coordinates in the grid, and if the letter is A, check if an X-MAS instance can be formed starting from that coordinate.
  3. Keep track of the number of instances of X-MAS found.

The file is read in the same way as part 1.

To check if the A at a coordinate can be part of an X-MAS instance, I created a function called is_x_mas_match.

def is_x_mas_match(grid: list[list[str]], coord: Coordinate) -> bool:
    corners = {
        Directions.UP_LEFT,
        Directions.UP_RIGHT,
        Directions.DOWN_LEFT,
        Directions.DOWN_RIGHT,
    }
    adjacent = get_adjacent_letters(coord, grid)
    if (corners).intersection(adjacent.keys()) != corners:
        return False
    return (
        {adjacent[Directions.UP_LEFT][0], adjacent[Directions.DOWN_RIGHT][0]}
        == {"M", "S"}
    ) and (
        {adjacent[Directions.UP_RIGHT][0], adjacent[Directions.DOWN_LEFT][0]}
        == {"M", "S"}
    )

Breaking down the function:

Now that we can check if the A at a coordinate can be part of an X-MAS instance, we can iterate over all the coordinates in the grid and keep track of the number of instances of X-MAS found.

def xmas_matches2(grid: list[list[str]]) -> int:
    matches = 0
    for y in range(len(grid)):
        for x in range(len(grid[y])):
            if grid[y][x] == "A":
                match = is_x_mas_match(grid, Coordinate(x, y))
                if match:
                    matches += 1
    return matches

The only difference between this function and the one for part 1 is that we only do more checks if the letter at the current coordinate is A. Finally, we can call the xmas_matches2 function with the grid we read from the input file to get the number of instances of X-MAS.

print(xmas_matches2(grid))

That’s it for day 4 of Advent of Code 2024! I hope you enjoyed reading my solution and let’s see how the rest of the month goes!

Vinesh Benny

Vinesh Benny

A Software Engineer learning about different things in life and otherwise