Advent of Code 2024 Day 4

Once again, I’m using Python.

Link to problem statement.

Link to full code.

Setup

inp = open("input.txt", "r").read()
sample1 = """MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX
"""
sample2 = """..X...
.SAMX.
.A..A.
XMAS.S
.X....
......
"""
print(f"Part 1: {part1(inp)}")
print(f"Part 2: {part2(inp)}")

I read the input file, copy some of the sample inputs and then run both part 1 and part 2. For some reason, I chose to parse inside the function for each part. Also, this is at the bottom of the file after function declarations since Python doesn’t have a main function.

Parsing

def parse_input(inp):
    inp = inp.strip()

    grid = {}
    row = 0
    for line in inp.splitlines():
        col = 0
        for char in line:
            grid[(row, col)] = char
            col += 1
        row += 1
    width = col
    height = row
    return grid, width, height

The input is in the form of a grid of characters, which I parse into a dictionary with (row, col) tuples as the keys and length-1 strings as the values. I also find out and return the width and the height of the grid.

Part One

We need to do a word search on our grid of characters, looking for all instances of XMAS, horizontally, vertically, diagonally, forwards and backwards.

I did this by first finding all the spots on the grid that contain "X". Then, for all eight directions, I check if going in that direction gives us M, A, and S in that order. Note how I use the .get method in the helper function to deal with going out of the grid bounds. For each match in a direction, I increment the answer.

def part1(inp):
    grid, width, height = parse_input(inp)
    ans = 0

    for row in range(0, height):
        for col in range(0, width):
            if grid[(row, col)] == "X":
                ans += search_for_xmas(grid, row, col)
    return ans


def search_for_xmas(grid, row, col):
    neighbor_offsets = [
        [1, 0],
        [0, 1],
        [-1, 0],
        [0, -1],
        [1, 1],
        [1, -1],
        [-1, 1],
        [-1, -1],
    ]
    count = 0
    for neighbor in neighbor_offsets:
        current = [row, col]
        for i in range(1, 4):
            current[0] += neighbor[0]
            current[1] += neighbor[1]
            current_value = grid.get(tuple(current))
            if current_value == "M" and i == 1:
                pass
            elif current_value == "A" and i == 2:
                pass
            elif current_value == "S" and i == 3:
                count += 1
            else:
                break
    return count

Part Two

Now, it turns out, we needed to find all the X-MASs and not all the XMASs. An X-MAS is the letters MAS arranged in a cross.

M.S
.A.
M.S

To find all the X-MASs, my approach is very similar to part 1. I first find all the As in the grid, then for each such grid position, I see if both its diagonals give us the letters M and S.

def part2(inp):
    grid, width, height = parse_input(inp)
    ans = 0
    for row in range(0, height):
        for col in range(0, width):
            if grid[(row, col)] == "A":
                ans += search_for_cross_mas(grid, row, col)
    return ans


def search_for_cross_mas(grid, row, col):
    diagonals_1 = [[1, 1], [-1, -1]]
    diagonals_2 = [[-1, 1], [1, -1]]
    diagonals_1_values = [
        grid.get((row + offset[0], col + offset[1])) for offset in diagonals_1
    ]
    diagonals_2_values = [
        grid.get((row + offset[0], col + offset[1])) for offset in diagonals_2
    ]
    if (diagonals_1_values == ["S", "M"] or diagonals_1_values == ["M", "S"]) and (
        diagonals_2_values == ["S", "M"] or diagonals_2_values == ["M", "S"]
    ):
        return 1
    else:
        return 0