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 | |