2024-12-20 - Race Condition
(original .ipynb)
from collections import defaultdict
puzzle_input_str = open("./puzzle_input/day20.txt").read()
test_input_str = """###############
#...#...#.....#
#.#.#.#.#.###.#
#S#...#.#.#...#
#######.#.#.###
#######.#.#...#
#######.#.###.#
###..E#...#...#
###.#######.###
#...###...#...#
#.#####.#.###.#
#.#...#.#.#...#
#.#.#.#.#.#.###
#...#...#...###
###############"""
def neighbours(track, r, c):
max_r = len(track)
max_c = len(track[0])
candidates = (
(r-1, c), (r+1, c),
(r, c-1), (r, c+1)
)
for maybe_r, maybe_c in candidates:
if maybe_r == r and maybe_c == c:
continue
if maybe_r >= max_r or maybe_r < 0:
continue
if maybe_c >= max_c or maybe_c < 0:
continue
yield (maybe_r, maybe_c)
def next_move(track, r, c, visited):
for nr, nc in neighbours(track, r, c):
if (nr, nc) in visited:
continue
if track[nr][nc] == "." or track[nr][nc] == "E":
return (nr, nc)
def parse_input(input_str):
track = input_str.splitlines()
start, finish = None, None
for r, line in enumerate(track):
for c, section in enumerate(line):
if section == "S":
start = (r, c)
elif section == "E":
finish = (r, c)
return track, start, finish
def time_race(track, start, finish):
timed_track = [
[None for section in line]
for line in track
]
visited = set()
picoseconds = -1
r, c = start
while track[r][c] != "E":
visited.add((r, c))
picoseconds += 1
next_r, next_c = next_move(track, r, c, visited)
timed_track[r][c] = picoseconds
r, c = next_r, next_c
timed_track[r][c] = picoseconds + 1
return timed_track
def is_track(timed_track, r, c):
return timed_track[r][c] is not None
def find_cheats_from(timed_track, r, c, max_distance):
queue = [
(1, nr, nc)
for nr, nc
in neighbours(timed_track, r, c)
]
visited = {(r,c)}
while queue:
distance, nr, nc = queue.pop(0)
if (nr, nc) not in visited and distance <= max_distance:
visited.add((nr,nc))
if is_track(timed_track, nr, nc):
saving = timed_track[nr][nc] - timed_track[r][c] - distance
if saving > 0:
yield saving
queue += [
(distance+1, nr2, nc2)
for nr2, nc2
in neighbours(timed_track, nr, nc)
]
def find_cheats(timed_track, distance):
for r, row in enumerate(timed_track):
for c, col in enumerate(row):
if col == None:
continue
yield from find_cheats_from(timed_track, r, c, distance)
def part_one(input_str, min_saving=100, distance=2):
track, start, finish = parse_input(input_str)
timed_track = time_race(track, start, finish)
cheats = find_cheats(timed_track, distance)
cheat_counts = defaultdict(lambda:0)
for time_saved in cheats:
cheat_counts[time_saved] += 1
return sum(
cheat_counts[cheat]
for cheat
in cheat_counts
if cheat >= min_saving
)
assert 1 == part_one(test_input_str, 64)
print("part one:", part_one(puzzle_input_str))
part one: 1363
def part_two(input_str, min_saving=100):
return part_one(input_str, min_saving, 20)
assert 3 == part_two(test_input_str, 76)
# 980343 is too low >:-(
print("part two:", part_two(puzzle_input_str))
part two: 1007186