Skip to content

Commit dfaabb1

Browse files
Add Day 20: Part 2
Use Manhattan Distance
1 parent 9456964 commit dfaabb1

File tree

2 files changed

+33
-17
lines changed

2 files changed

+33
-17
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@
2323
| [Day 17: Chronospatial Computer](https://adventofcode.com/2024/day/17) ||| [day17.py](aoc/day17.py) |
2424
| [Day 18: RAM Run](https://adventofcode.com/2024/day/18) ||| [day18.py](aoc/day18.py) |
2525
| [Day 19: Linen Layout](https://adventofcode.com/2024/day/19) ||| [day19.py](aoc/day19.py) |
26-
| [Day 20: Race Condition](https://adventofcode.com/2024/day/20) || | [day20.py](aoc/day20.py) |
26+
| [Day 20: Race Condition](https://adventofcode.com/2024/day/20) || | [day20.py](aoc/day20.py) |

aoc/day20.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,45 +54,61 @@ def is_free(y, x, maze: Maze):
5454
return (y, x) not in maze.walls and y and x and y < maze.size_y and x < maze.size_x
5555

5656

57-
def shortest_path(m: Maze, limit=None) -> int | None:
57+
def shortest_path(m: Maze, limit=None) -> tuple[int | None, list[tuple[int, int]]]:
5858
distances = {
5959
(y, x): 0 if (y, x) == m.start else sys.maxsize
6060
for y in range(m.size_y)
6161
for x in range(m.size_x)
6262
if is_free(y, x, m)
6363
}
64-
64+
predecessor: dict[tuple[int, int], tuple[int, int]] = dict()
6565
while distances:
6666
min_pos, ucost = min(distances.items(), key=itemgetter(1))
6767
del distances[min_pos]
6868
if limit and ucost > limit:
69-
return
69+
return None, []
7070
if min_pos == m.end:
71-
return ucost
71+
pos = min_pos
72+
route = [pos]
73+
while True:
74+
pos = predecessor[pos]
75+
route = [pos, *route]
76+
if pos == m.start:
77+
return ucost, route
78+
7279
for pos in [(min_pos[0] + d[0], min_pos[1] + d[1]) for d in [(-1, 0), (1, 0), (0, -1), (0, 1)]]:
7380
if (d := distances.get(pos)) and d > ucost + 1:
7481
distances[pos] = ucost + 1
82+
predecessor[pos] = min_pos
83+
84+
85+
def manhattan_distance(pos1: tuple[int, int], pos2: tuple[int, int]):
86+
return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1])
87+
88+
89+
def cheat_paths(path, max_cheat_points=2):
90+
for i in range(len(path)):
91+
for j in range(i, len(path)):
92+
if (d := manhattan_distance(path[i], path[j])) <= max_cheat_points:
93+
yield (i + len(path) - j + d - 1)
7594

7695

7796
def part1(orig_maze: Maze):
78-
non_cheat_score = shortest_path(orig_maze)
97+
non_cheat_score, path = shortest_path(orig_maze)
98+
assert non_cheat_score
99+
return len([score for score in cheat_paths(path) if score <= non_cheat_score - 100])
100+
101+
102+
def part2(orig_maze: Maze):
103+
non_cheat_score, path = shortest_path(orig_maze)
79104
assert non_cheat_score
80-
count = 0
81-
to_check = len(orig_maze.walls)
82-
c = 0
83-
for remove_pos in orig_maze.walls:
84-
walls = orig_maze.walls - {remove_pos}
85-
m = dataclasses.replace(orig_maze, walls=walls)
86-
if shortest_path(m, non_cheat_score - 100):
87-
count += 1
88-
c += 1
89-
print("Done:", 100 * c / to_check)
90-
return count
105+
return len([score for score in cheat_paths(path, 20) if score <= non_cheat_score - 100])
91106

92107

93108
def main():
94109
m = parse_maze(Path(sys.argv[1]).read_text())
95110
print(f"Part1: {part1(m)}")
111+
print(f"Part2: {part2(m)}")
96112

97113

98114
if __name__ == "__main__":

0 commit comments

Comments
 (0)