import numpy as np
import pandas as pd
2023: Day 3 - Gear Ratios
Setup
Part 1
Notes:
- A part is missing from an engine; we have to figure out which one using the numbers in the engine schematic (puzzle input).
- Any number adjacent to a symbol, even diagonally, is a “part number” and should be included in the sum.
- Periods do not count as a symbol.
Example
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.
Problems I see:
- The input is a sort of matrix/grid, but we’re working with multi-digit numbers that occupy several slots/cells of the matrix.
- Computations need to span both vertical and horizontal axes (so dataframe-oriented methods and functions may not be suitable here).
At this moment, I have no idea which data structure to use for this problem.
The only thing that comes to mind is to put the data in a numpy
array, then use a nested for
loop to check certain conditions.
Importing the data in a numpy array where each element is a character of the input file.
# Inspiration for this: https://stackoverflow.com/a/75643841/7221164
with open('input', 'r') as f:
input = np.stack([np.fromiter(list(line.strip()), dtype="object") for line in f])
print(input)
[['.' '.' '.' ... '.' '.' '.']
['.' '.' '.' ... '.' '.' '.']
['.' '.' '1' ... '.' '.' '.']
...
['.' '.' '.' ... '1' '3' '.']
['.' '9' '6' ... '.' '.' '.']
['.' '.' '.' ... '.' '.' '.']]
input.shape
(140, 140)
Implementing my for
loop
- In
current_number
, I’ll add the digits I find as I iterate (will reset when a period or a line break is found). is_valid
is a flag indicating whether thecurrent_number
should be counted in the sum (i.e., if any of its digits is adjacent to a symbol).running_sum
keeps track of the total of all valid numbers.
= []
debugging_array = None
current_number = False
is_valid = 0
running_sum
= input.shape[0] -1
max_y = input.shape[1] -1
max_x
# Inspiration for this: https://stackoverflow.com/a/49360371
for iy, ix in np.ndindex(input.shape):
= input[iy, ix]
value
if not value.isdigit() or (ix == 0 and iy > 0):
# case when a sequence of digits is ending
if current_number is not None:
if is_valid:
str(current_number) + " is valid")
debugging_array.append(+= current_number
running_sum = False
is_valid else:
str(current_number) + " is NOT valid")
debugging_array.append(
# reset current_numner
= None
current_number
if value.isdigit():
if current_number is None:
# case when a sequence of digits is starting
= int(value)
current_number else:
# case when we're in the middle of a sequence of digits
= current_number*10 + int(value)
current_number
# check the surrounding elements to know if there is a symbol (only if is_valid = False, otherwise it's not necessary)
if is_valid is False:
for offset_y, offset_x in np.ndindex((3,3)):
=iy+offset_y-1
curr_y=ix+offset_x-1
curr_x
# Exception when the "neighbour" would be out of the array
if curr_y < 0 or curr_x < 0 or curr_y > max_y or curr_x > max_x:
continue
# Exception when we're on the same number
if curr_y == iy and curr_x == ix:
continue
# The check itself
# If one of the surrounding elements is not a dot or a digit, the flag switches to True and the for loop ends
if input[curr_y, curr_x] != "." and not input[curr_y, curr_x].isdigit():
= True
is_valid break
Inspecting how the first numbers from the input were classified:
10] debugging_array[:
['401 is valid',
'425 is NOT valid',
'323 is NOT valid',
'791 is valid',
'697 is valid',
'963 is NOT valid',
'420 is NOT valid',
'290 is valid',
'492 is NOT valid',
'656 is valid']
Checking my solution:
print(running_sum)
543867
The code is very ugly (for my taste), but the solution is correct!!
Part 2
One of the gears in the engine is malfunctioning. A gear is represented by a ’*’ symbol, and it must be adjacent to exactly two part numbers. The gear ratio is calculated by multiplying these two numbers together.
Task is to determine the gear ratio for each gear and then add them up.
Example:
467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..
Here there are two gears. The first one is located in the top left corner, with part numbers 467 and 35, resulting in a gear ratio of 16345. The second gear is in the lower right corner, and its gear ratio is 451490. (Note that the ’*’ adjacent to 617 is not a gear because it’s only adjacent to one part number.)
To find the total sum of all the gear ratios in your engine schematic, simply add them up.
Idea to solve this part: each time a number is valid, put it in the corresponding position of a new array that has the same dimensions as the input array.
Then, iterate over the input array with a new for loop that searches for “*” and counts the N of valid numbers surrounding it.
= np.zeros(input.shape)
arr_pt2
= None
current_number
= input.shape[0] -1
max_y = input.shape[1] -1
max_x
for iy, ix in np.ndindex(input.shape):
= input[iy, ix]
value
# Case when the element is a * (potentially a gear)
if value == "*":
= -1
arr_pt2[iy, ix]
# Checking if a number just ended
if not value.isdigit() or (ix == 0 and iy > 0):
# case when a sequence of digits is ending
if current_number is not None:
# I need to write the current_number on the previous positions of arr_pt2
# Things to take into account:
# 1. overflowing across rows
# 2. I need to write the number on as many cells as digits the number has
for i in range(len(str(current_number))):
# ex: for a 3 digit number, offset will take values -1, -2 and -3
= -1 - i
offset # we first try to go back one cell in the x axis
# (move to the left across the same row)
= ix + offset
ix_offset = iy
iy_offset
# case when we overflow the beginning of the row
if ix_offset < 0:
# we move up in the y-axis
= iy_offset-1
iy_offset
# when ix_offset == -1, we add 140 to end up with 139
=ix_offset+input.shape[1]
ix_offset
# example
# if iy_offset, ix_offset == 10, -1
# we end up in 9, 139
= current_number
arr_pt2[iy_offset, ix_offset]
# reset current_numner
= None
current_number
if value.isdigit():
if current_number is None:
# case when a sequence of digits is starting
= int(value)
current_number else:
# case when we're in the middle of a sequence of digits
= current_number*10 + int(value) current_number
After this rather messy for
loop, I should be able to identify gears by looking at the surrounding cells of all the ‘-1’ values in the array, which represent ’*’.
= arr_pt2.shape[0] -1
max_y = arr_pt2.shape[1] -1
max_x
= 0
running_sum
for iy, ix in np.ndindex(arr_pt2.shape):
= arr_pt2[iy, ix]
value
= set()
neighbours
if value == -1:
for offset_y, offset_x in np.ndindex((3,3)):
=iy+offset_y-1
curr_y=ix+offset_x-1
curr_x
# Exception when the "neighbour" would be out of the array
if curr_y < 0 or curr_x < 0 or curr_y > max_y or curr_x > max_x:
continue
# Exception when we're on the same number
if curr_y == iy and curr_x == ix:
continue
# The check itself
# if a surrounding cell is a number, then I'll add it to the `neighbours` set (this data structure handles duplicated numbers automatically)
if arr_pt2[curr_y, curr_x] > 0:
neighbours.add(arr_pt2[curr_y, curr_x])
if len(neighbours) == 2:
# multiply the neighbours
+= neighbours.pop() * neighbours.pop() running_sum
Checking my solution:
print(running_sum)
79613331.0
It was correct!