AOC 23 - Day 03 - Gear Ratios

Link to Advent of Code day 3

For the third day, we require a gondola lift. Of course, it's broken. We need to help finding the missing mechanic part.

Part 1

Rules

For the first part, we need to find which number is a part or not. A number is a part if it's adjacent, even diagonally, to any symbol which is not ..

467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..

In this schematic, two numbers are not part numbers because they are not adjacent to a symbol: 114 (top right) and 58 (middle right). Every other number is adjacent to a symbol and so is a part number; their sum is 4361.

Code

To solve this, a simple flood-fill like algorithm is enough; we iterate from left to right, and up to bottom, and for each found digit, we go from left to right to gather the full number, and check if any of the digits is adjacent to a symbol. If the digit has a digit on its left, we ignore it, since it means its part of a number we already processed.

Schematic = List[List[str]]

def part_one(input_file: str) -> int:
    schematic = get_schematic(input_file)
    part_numbers = []
    for (y, x), value in iterate_on_schematic(schematic):
        if x > 0 and schematic[y][x - 1].isdigit():
            continue  # this part number already has been evaluated
        if is_part(schematic, y, x):
            part_numbers.append(get_part_number(schematic, y, x))
    return sum(part_numbers)

def is_part(schematic: Schematic, y: int, x: int) -> bool:
    while x < len(schematic[y]) and schematic[y][x].isdigit():
        if touch_symbol(schematic, y, x):
            return True
        x += 1
    return False

def get_part_number(
    schematic: Schematic, y: int, x: int
) -> int:
    nbr = ""
    while x < len(schematic[y]) and schematic[y][x].isdigit():
        nbr += schematic[y][x]
        x += 1
    return int(nbr)

Part 2

Rules

For the part 2, we need to find all the values * with exactly two part number adjacent to it, multiply these two number, and return the sum of this.

Code

I started trying to find a solution to keep track of all the part number processed by (y, x), but I then realize that a number touching the tile * is by definition a product number. With this new solution, I just have to:

  1. update the get_part_number function, so it can retrieve a number from any side, and delete the number so we count each number once
  2. for each tile *, retrieve all the adjacent numbers Then, if the tile has exactly 2 numbers, we add the product to the total. This give us:
def get_part_number(
    schematic: Schematic, y: int, x: int, delete=False
) -> Optional[int]:
    """Returns the complete number from this tile as an `int`, or `None`.

    A complete number is the concatenation of the digits linked to the initial tile on the x-axis.
    If the tile is not a digit, this function will return `None`
    If `delete` is set to `True`, each digit from the number will be replaced by `.` once read.
    """
    nbr = ""
    if not schematic[y][x].isdigit():
        return None
    while x >= 0 and schematic[y][x].isdigit():
        x -= 1
    x += 1
    while x < len(schematic[y]) and schematic[y][x].isdigit():
        nbr += schematic[y][x]
        if delete:
            schematic[y][x] = "."
        x += 1
    if not nbr:
        return None
    else:
        return int(nbr)


def part_two(input_file: str):
    schematic = get_schematic(input_file)
    total = 0
    for (y, x), value in iterate_on_schematic(schematic):
        if value == "*":
            numbers = get_surrounding_numbers(schematic, y, x)
            if len(numbers) == 2:
                total += numbers[0] * numbers[1]
    return total

def get_surrounding_numbers(schematic, y: int, x: int) -> List[int]:
    numbers = []
    for tmp_y, tmp_x in iterate_around_tile(y, x):
        value = get_part_number(schematic, tmp_y, tmp_x, delete=True)
        if value:
            numbers.append(value)
    return numbers

And the day is solved !


All the code is available on github