mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-05-19 03:23:27 +02:00
171 lines
4.7 KiB
Python
171 lines
4.7 KiB
Python
from collections import defaultdict
|
|
|
|
|
|
def day20(input: str, cheats: int) -> dict[int, int]:
|
|
# get minimum without cheats, do this backwards by propagating from E to S
|
|
# to populate a grid of the distance from each cell to E
|
|
grid: list[list[str]] = [list(line) for line in input.splitlines()]
|
|
|
|
row: int = 0
|
|
col: int = 0
|
|
|
|
for r in range(len(grid)):
|
|
for c in range(len(grid[0])):
|
|
if grid[r][c] == "E":
|
|
row = r
|
|
col = c
|
|
|
|
# default value set to 100_000 which is bigger than entire grid's area so
|
|
# walls will not be usable as paths to finish
|
|
dp_steps_to_end: list[list[int]] = [
|
|
[100_000 for _ in range(len(grid[0]))] for _ in range(len(grid))
|
|
]
|
|
|
|
diffs = [(0, 1), (0, -1), (-1, 0), (1, 0)]
|
|
queue: list[tuple[int, int, int]] = [(row, col, 0)]
|
|
visited: set[tuple[int, int]] = set()
|
|
while len(queue) > 0:
|
|
row, col, steps = queue.pop(0)
|
|
dp_steps_to_end[row][col] = steps
|
|
visited.add((row, col))
|
|
|
|
for diff in diffs:
|
|
next_row, next_col = row + diff[0], col + diff[1]
|
|
if 0 <= next_row < len(grid) and 0 <= next_col < len(grid[0]):
|
|
if grid[next_row][next_col] != "#":
|
|
if (next_row, next_col) not in visited:
|
|
queue.append((next_row, next_col, steps + 1))
|
|
|
|
# then starting from S, BFS but from each cell, view all cells <cheats> cells away
|
|
# if it's a valid shortcut, record it
|
|
|
|
second_saved_freqs: dict[int, int] = defaultdict(int)
|
|
|
|
reachable_diffs = get_reachable_coords_within_x_steps(cheats)
|
|
|
|
# visited contains coords of the entire path, so just use that...
|
|
for path_coord in visited:
|
|
r, c = path_coord
|
|
for diff in reachable_diffs:
|
|
rr = path_coord[0] + diff[0]
|
|
cc = path_coord[1] + diff[1]
|
|
|
|
if 0 <= rr < len(grid) and 0 <= cc < len(grid[0]):
|
|
if grid[rr][cc] != "#":
|
|
savings = dp_steps_to_end[r][c] - (
|
|
dp_steps_to_end[rr][cc] + abs(diff[0]) + abs(diff[1])
|
|
)
|
|
if savings > 0:
|
|
second_saved_freqs[savings] += 1
|
|
|
|
return second_saved_freqs
|
|
|
|
|
|
# helper function to get all coords that are reachable within allotted cheats
|
|
def get_reachable_coords_within_x_steps(x: int) -> list[tuple[int, int]]:
|
|
coords_map: dict[tuple[int, int], bool] = {}
|
|
|
|
queue: list[tuple[int, int]] = [(0, 0)]
|
|
for _ in range(x):
|
|
next_queue: list[tuple[int, int]] = []
|
|
while len(queue) > 0:
|
|
front = queue.pop(0)
|
|
for diff in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
|
coord = (front[0] + diff[0], front[1] + diff[1])
|
|
if coord not in coords_map:
|
|
coords_map[coord] = True
|
|
next_queue.append(coord)
|
|
queue = next_queue
|
|
|
|
return list(coords_map.keys())
|
|
|
|
|
|
example = """###############
|
|
#...#...#.....#
|
|
#.#.#.#.#.###.#
|
|
#S#...#.#.#...#
|
|
#######.#.#.###
|
|
#######.#.#...#
|
|
#######.#.###.#
|
|
###..E#...#...#
|
|
###.#######.###
|
|
#...###...#...#
|
|
#.#####.#.###.#
|
|
#.#...#.#.#...#
|
|
#.#.#.#.#.#.###
|
|
#...#...#...###
|
|
###############"""
|
|
|
|
# python is weird and we can use == to compare 2 dicts for the same keys and values...
|
|
example_seconds_saved_to_freq: dict[int, int] = {
|
|
2: 14,
|
|
4: 14,
|
|
6: 2,
|
|
8: 4,
|
|
10: 2,
|
|
12: 3,
|
|
20: 1,
|
|
36: 1,
|
|
38: 1,
|
|
40: 1,
|
|
64: 1,
|
|
}
|
|
|
|
part1_example_result = day20(example, 2)
|
|
print(
|
|
f"part1 example: {part1_example_result == example_seconds_saved_to_freq} want True"
|
|
)
|
|
if part1_example_result != example_seconds_saved_to_freq:
|
|
print(
|
|
f"\tDEBUG part1 example got: {part1_example_result} want {(example_seconds_saved_to_freq)}"
|
|
)
|
|
|
|
input = open("input.txt").read().strip()
|
|
|
|
|
|
def sum_keys_over_x(d: dict[int, int], x: int) -> int:
|
|
total: int = 0
|
|
for k, v in d.items():
|
|
if k >= x:
|
|
total += v
|
|
return total
|
|
|
|
|
|
part1_result = day20(input, 2)
|
|
|
|
print(
|
|
f"part1 actual: {sum_keys_over_x(part1_result, 100)} cheats save 100+ picosecs want 1422"
|
|
)
|
|
|
|
part2_example_result: dict[int, int] = {
|
|
50: 32,
|
|
52: 31,
|
|
54: 29,
|
|
56: 39,
|
|
58: 25,
|
|
60: 23,
|
|
62: 20,
|
|
64: 19,
|
|
66: 12,
|
|
68: 14,
|
|
70: 12,
|
|
72: 22,
|
|
74: 4,
|
|
76: 3,
|
|
}
|
|
|
|
part2_result = day20(example, 20)
|
|
part2_result_50 = {}
|
|
for k, v in part2_example_result.items():
|
|
if k >= 50:
|
|
part2_result_50[k] = v
|
|
|
|
print(f"part2 example: {part2_result_50 == part2_example_result} want True")
|
|
if part2_result_50 != part2_example_result:
|
|
print(
|
|
f"DEBUG part2 example 50+ saved: {part2_result_50} want {part2_example_result}"
|
|
)
|
|
|
|
part2_result = day20(input, 20)
|
|
print(f"part2 actual: {sum_keys_over_x(part2_result, 100)} want 1009299")
|