171 lines
4.7 KiB
Python
171 lines
4.7 KiB
Python
# --- Day 3: Spiral Memory ---
|
|
|
|
# You come across an experimental new kind of memory stored on an infinite
|
|
# two-dimensional grid.
|
|
|
|
# Each square on the grid is allocated in a spiral pattern starting at a
|
|
# location marked 1 and then counting up while spiraling outward. For example,
|
|
# the first few squares are allocated like this:
|
|
|
|
# 17 16 15 14 13
|
|
# 18 5 4 3 12
|
|
# 19 6 1 2 11
|
|
# 20 7 8 9 10
|
|
# 21 22 23---> ...
|
|
|
|
# While this is very space-efficient (no squares are skipped), requested data
|
|
# must be carried back to square 1 (the location of the only access port for
|
|
# this memory system) by programs that can only move up, down, left, or right.
|
|
# They always take the shortest path: the Manhattan Distance between the
|
|
# location of the data and square 1.
|
|
|
|
# For example:
|
|
|
|
# Data from square 1 is carried 0 steps, since it's at the access port.
|
|
# Data from square 12 is carried 3 steps, such as: down, left, left.
|
|
# Data from square 23 is carried only 2 steps: up twice.
|
|
# Data from square 1024 must be carried 31 steps.
|
|
|
|
# How many steps are required to carry the data from the square identified in
|
|
# your puzzle input all the way to the access port?
|
|
|
|
# Your puzzle input is 347991.
|
|
|
|
from typing import Generator, Tuple
|
|
|
|
from scipy.spatial.distance import cityblock
|
|
|
|
|
|
# https://stackoverflow.com/questions/23706690/how-do-i-make-make-spiral-in-python
|
|
def move_right(x: int, y: int) -> Tuple[int, int]:
|
|
return x + 1, y
|
|
|
|
|
|
def move_down(x: int, y: int) -> Tuple[int, int]:
|
|
return x, y - 1
|
|
|
|
|
|
def move_left(x: int, y: int) -> Tuple[int, int]:
|
|
return x - 1, y
|
|
|
|
|
|
def move_up(x: int, y: int) -> Tuple[int, int]:
|
|
return x, y + 1
|
|
|
|
|
|
moves = [move_right, move_up, move_left, move_down]
|
|
|
|
|
|
def gen_points(end: int) -> Generator[Tuple[int, Tuple[int, int]], int, None]:
|
|
from itertools import cycle
|
|
|
|
_moves = cycle(moves)
|
|
n = 1
|
|
pos = 0, 0
|
|
times_to_move = 1
|
|
|
|
yield n, pos
|
|
|
|
while True:
|
|
for _ in range(2):
|
|
move = next(_moves)
|
|
for _ in range(times_to_move):
|
|
if n >= end:
|
|
return
|
|
pos = move(*pos)
|
|
n += 1
|
|
yield n, pos
|
|
|
|
times_to_move += 1
|
|
|
|
|
|
def part_1() -> None:
|
|
my_lst = list(gen_points(347991))
|
|
|
|
init = my_lst[0][1]
|
|
target = my_lst[347990][1]
|
|
|
|
print(f"We need {cityblock(init, target)} steps")
|
|
|
|
|
|
# --- Part Two ---
|
|
|
|
# As a stress test on the system, the programs here clear the grid and then
|
|
# store the value 1 in square 1. Then, in the same allocation order as shown
|
|
# above, they store the sum of the values in all adjacent squares, including
|
|
# diagonals.
|
|
|
|
# So, the first few squares' values are chosen as follows:
|
|
|
|
# Square 1 starts with the value 1.
|
|
# Square 2 has only one adjacent filled square (with value 1), so it also
|
|
# stores 1.
|
|
# Square 3 has both of the above squares as neighbors and stores the sum of
|
|
# their values, 2.
|
|
# Square 4 has all three of the aforementioned squares as neighbors and
|
|
# stores the sum of their values, 4.
|
|
# Square 5 only has the first and fourth squares as neighbors, so it gets
|
|
# the value 5.
|
|
|
|
# Once a square is written, its value does not change. Therefore, the first few
|
|
# squares would receive the following values:
|
|
|
|
# 147 142 133 122 59
|
|
# 304 5 4 2 57
|
|
# 330 10 1 1 54
|
|
# 351 11 23 25 26
|
|
# 362 747 806---> ...
|
|
|
|
# What is the first value written that is larger than your puzzle input?
|
|
|
|
|
|
def is_adjacent(p1: Tuple[int, int], p2: Tuple[int, int]) -> bool:
|
|
if (abs(p1[0] - p2[0]) == 0) and (abs(p1[1] - p2[1]) == 1):
|
|
return True
|
|
elif (abs(p1[0] - p2[0]) == 1) and (abs(p1[1] - p2[1]) == 0):
|
|
return True
|
|
elif (abs(p1[0] - p2[0]) == 1) and (abs(p1[1] - p2[1]) == 1):
|
|
return True
|
|
return False
|
|
|
|
|
|
def gen_points_adjacents(
|
|
end: int,
|
|
) -> Generator[Tuple[int, Tuple[int, int]], int, None]:
|
|
from itertools import cycle
|
|
|
|
_moves = cycle(moves)
|
|
n = 1
|
|
pos = 0, 0
|
|
times_to_move = 1
|
|
|
|
yield n, pos
|
|
|
|
saved_items = [(n, pos)]
|
|
while True:
|
|
for _ in range(2):
|
|
move = next(_moves)
|
|
for _ in range(times_to_move):
|
|
if n >= end:
|
|
return
|
|
pos = move(*pos)
|
|
adjacent_elements = []
|
|
for el in saved_items:
|
|
if is_adjacent(el[1], pos):
|
|
adjacent_elements.append(el[0])
|
|
n = sum(adjacent_elements)
|
|
saved_items.append((n, pos))
|
|
yield n, pos
|
|
|
|
times_to_move += 1
|
|
|
|
|
|
def part_2() -> None:
|
|
my_lst = list(gen_points_adjacents(347991))
|
|
print(f"The value we are looking for is {my_lst[-1:][0][0]}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
part_1()
|
|
part_2()
|