Spaces:
Running
Running
import copy | |
def load_data(file): | |
with open(file) as f: | |
raw_data = f.readlines() | |
grid = [] | |
for line in raw_data: | |
line = line.strip("\n") | |
grid.append(list(line)) | |
return grid | |
def build_grid(M, N, coords): | |
grid = [] | |
for i in range(M): | |
row = [] | |
for j in range(N): | |
c = "." if (i,j) not in coords else "#" | |
row.append(c) | |
grid.append(row) | |
return grid | |
def pprint(grid): | |
grid_str = "\n".join(["".join(l) for l in grid]) | |
print(grid_str) | |
def pprint2(grid): | |
M = len(grid) | |
N = len(grid[0]) | |
new_grid = copy.deepcopy(grid) | |
for i in range(M): | |
for j in range(N): | |
if isinstance(grid[i][j], tuple): | |
new_grid[i][j] = "O" | |
print(new_grid) | |
# try: | |
grid_str = "\n".join(["".join(l) for l in new_grid]) | |
# except: | |
# import ipdb; ipdb.set_trace(); | |
print(grid_str) | |
def get_neighbours(pos, grid): | |
directions = [(0,1), (1,0), (-1,0), (0, -1)] | |
M = len(grid) | |
N = len(grid[0]) | |
ns = [] | |
i, j = pos | |
for dx, dy in directions: | |
ni, nj = (i+dx, j+dy) | |
if ni in range(M) and nj in range(N): | |
if grid[ni][nj] != "#": | |
ns.append((ni, nj)) | |
return ns | |
def get_symbol_pos(grid, s): | |
M = len(grid) | |
N = len(grid[0]) | |
for i in range(M): | |
for j in range(N): | |
if grid[i][j] == s: | |
return (i,j) | |
def bfs(grid): | |
parents = copy.deepcopy(grid) | |
# start = (0, 0) | |
start_pos = get_symbol_pos(grid, "S") | |
end_pos = get_symbol_pos(grid, "E") | |
q = [] | |
q.append(start_pos) | |
visited = set() | |
count = 0 | |
while len(q) > 0 and end_pos not in visited: | |
# # Visualize grid filling up | |
# # So much fun! | |
# if count % 500 == 0: | |
# print() | |
# pprint2(parents) | |
# print() | |
pos = q.pop(0) | |
if pos in visited: | |
continue | |
ns = get_neighbours(pos, grid) | |
# print(ns) | |
for n in ns: | |
if n not in visited: | |
q.append(n) | |
ni, nj = n | |
parents[ni][nj] = (pos) | |
visited.add(pos) | |
# print(len(visited)) | |
count += 1 | |
return parents | |
def get_len_shortest_path(grid): | |
# Run bfs, collect parents info | |
parents = bfs(grid) | |
# Build back the shortest path | |
shortest_grid = copy.deepcopy(grid) | |
shortest_path = [] | |
end_pos = get_symbol_pos(grid, "E") | |
start_pos = get_symbol_pos(grid, "S") | |
next_ = end_pos | |
while next_ != start_pos: | |
shortest_path.append(next_) | |
i, j = next_ | |
shortest_grid[i][j] = "O" | |
next_ = parents[i][j] | |
# print(len(shortest_path)) | |
return len(shortest_path), shortest_path | |
def get_all_shortest_paths(grid, shortest_path): | |
# We know that the cheat must be distance 2 and land back on the shortest path | |
# Iterate through all points on shortest path, compute 2 in each direction, and see which lands back on shortest | |
# path | |
directions = [(0, 1), (1,0), (0, -1), (-1, 0)] | |
valid_cheat_positions = set() | |
all_shortest_paths = [] | |
shortest_path = shortest_path[::-1] # Reverse it for easier logic, start_pos is now first | |
# shortest_path = [shortest_path[0]] + shortest_path # Add the start position so we can consider it too | |
start_idx = get_symbol_pos(grid, "S") | |
# print(start_idx) | |
# Start idx not included originally | |
shortest_path = [start_idx] + shortest_path | |
for pos in shortest_path: | |
for dx, dy in directions: | |
i, j = pos | |
cheat_1x, cheat_1y = i+dx, j+dy | |
cheat_2x, cheat_2y = i+2*dx, j+2*dy | |
cheat_1 = (cheat_1x, cheat_1y) | |
cheat_2 = (cheat_2x, cheat_2y) | |
if cheat_2 in shortest_path: # Check that we land back on the track | |
cheat_2_idx = shortest_path.index(cheat_2) | |
pos_idx = shortest_path.index(pos) | |
if cheat_2_idx > pos_idx: # Make sure we're ahead, not behind, otherwise doesn't make sense | |
grid_val1 = grid[cheat_1x][cheat_1y] | |
grid_val2 = grid[cheat_2x][cheat_2y] | |
if grid_val1 == "#" or grid_val2 == "#": # Make sure we're actually using the cheat | |
# if (cheat_1, cheat_2) and (cheat_2, cheat_1) not in valid_cheat_positions: # Avoid permutations, i don tthink this is necessary though | |
valid_cheat_positions.add((cheat_1, cheat_2)) | |
new_shortest_path = shortest_path[:pos_idx] + [cheat_1, cheat_2] + shortest_path[cheat_2_idx:] | |
all_shortest_paths.append(new_shortest_path[1:]) # Remove the added start pos for consistency | |
return all_shortest_paths, valid_cheat_positions | |
# Load data | |
# file = "test.txt" | |
file = "input.txt" | |
grid = load_data(file) | |
# First calculate the normal path length | |
normal_path_len, shortest_path = get_len_shortest_path(grid) | |
all_shortest_paths, cheat_positions = get_all_shortest_paths(grid, shortest_path) | |
# print(len(cheat_positions)) # Should be equal to 43 for test input | |
# Visualize all cheat positions on grid to see if we did it well | |
# for c1, c2 in cheat_positions: | |
# grid_copy = copy.deepcopy(grid) | |
# i, j = c1 | |
# grid_copy[i][j] = "1" | |
# i, j = c2 | |
# grid_copy[i][j] = "2" | |
# print() | |
# pprint2(grid_copy) | |
counts = {} | |
for idx, path in enumerate(all_shortest_paths): | |
shortest_path_len = len(path) | |
time_saved = normal_path_len - shortest_path_len | |
counts[time_saved] = counts.get(time_saved, 0) + 1 | |
total = 0 | |
for time_saved, count in counts.items(): | |
# print(f"There are {count} cheats that save {time_saved} picoseconds.") | |
if time_saved >= 100: | |
total += count | |
print(total) | |
## Part 2 | |
def get_all_shortest_paths(grid, shortest_path): | |
# We know that the cheat must be distance 2 and land back on the shortest path | |
# Iterate through all points on shortest path, compute 2 in each direction, and see which lands back on shortest | |
# path | |
directions = [(0, 1), (1,0), (0, -1), (-1, 0)] | |
valid_cheat_positions = set() | |
all_shortest_paths = [] | |
shortest_path = shortest_path[::-1] # Reverse it for easier logic, start_pos is now first | |
start_idx = get_symbol_pos(grid, "S") | |
# print(start_idx) | |
# Start idx not included originally | |
shortest_path = [start_idx] + shortest_path | |
c_len = 2 # Cheat length | |
for pos in shortest_path: | |
for dx, dy in directions: | |
i, j = pos | |
cheat_1x, cheat_1y = i+dx, j+dy | |
cheat_2x, cheat_2y = i+c_len*dx, j+c_len*dy | |
cheat_1 = (cheat_1x, cheat_1y) | |
cheat_2 = (cheat_2x, cheat_2y) | |
if cheat_2 in shortest_path: # Check that we land back on the track | |
cheat_2_idx = shortest_path.index(cheat_2) | |
pos_idx = shortest_path.index(pos) | |
if cheat_2_idx > pos_idx: # Make sure we're ahead, not behind, otherwise doesn't make sense | |
grid_val1 = grid[cheat_1x][cheat_1y] | |
grid_val2 = grid[cheat_2x][cheat_2y] | |
if grid_val1 == "#" or grid_val2 == "#": # Make sure we're actually using the cheat | |
# if (cheat_1, cheat_2) and (cheat_2, cheat_1) not in valid_cheat_positions: # Avoid permutations, i don tthink this is necessary though | |
valid_cheat_positions.add((cheat_1, cheat_2)) | |
new_shortest_path = shortest_path[:pos_idx] + [cheat_1, cheat_2] + shortest_path[cheat_2_idx:] | |
all_shortest_paths.append(new_shortest_path[1:]) # Remove the added start pos for consistency | |
return all_shortest_paths, valid_cheat_positions | |