diff --git a/2024/16/16.md b/2024/16/16.md new file mode 100644 index 0000000..353327a --- /dev/null +++ b/2024/16/16.md @@ -0,0 +1,170 @@ +## \-\-- Day 16: Reindeer Maze \-\-- + +It\'s time again for the [Reindeer Olympics](/2015/day/14)! This year, +the big event is the *Reindeer Maze*, where the Reindeer compete for the +*[lowest +score]{title="I would say it's like Reindeer Golf, but knowing Reindeer, it's almost certainly nothing like Reindeer Golf."}*. + +You and The Historians arrive to search for the Chief right as the event +is about to start. It wouldn\'t hurt to watch a little, right? + +The Reindeer start on the Start Tile (marked `S`) facing *East* and need +to reach the End Tile (marked `E`). They can move forward one tile at a +time (increasing their score by `1` point), but never into a wall (`#`). +They can also rotate clockwise or counterclockwise 90 degrees at a time +(increasing their score by `1000` points). + +To figure out the best place to sit, you start by grabbing a map (your +puzzle input) from a nearby kiosk. For example: + + ############### + #.......#....E# + #.#.###.#.###.# + #.....#.#...#.# + #.###.#####.#.# + #.#.#.......#.# + #.#.#####.###.# + #...........#.# + ###.#.#####.#.# + #...#.....#.#.# + #.#.#.###.#.#.# + #.....#...#.#.# + #.###.#.#.#.#.# + #S..#.....#...# + ############### + +There are many paths through this maze, but taking any of the best paths +would incur a score of only `7036`. This can be achieved by taking a +total of `36` steps forward and turning 90 degrees a total of `7` times: + + ############### + #.......#....E# + #.#.###.#.###^# + #.....#.#...#^# + #.###.#####.#^# + #.#.#.......#^# + #.#.#####.###^# + #..>>>>>>>>v#^# + ###^#.#####v#^# + #>>^#.....#v#^# + #^#.#.###.#v#^# + #^....#...#v#^# + #^###.#.#.#v#^# + #S..#.....#>>^# + ############### + +Here\'s a second example: + + ################# + #...#...#...#..E# + #.#.#.#.#.#.#.#.# + #.#.#.#...#...#.# + #.#.#.#.###.#.#.# + #...#.#.#.....#.# + #.#.#.#.#.#####.# + #.#...#.#.#.....# + #.#.#####.#.###.# + #.#.#.......#...# + #.#.###.#####.### + #.#.#...#.....#.# + #.#.#.#####.###.# + #.#.#.........#.# + #.#.#.#########.# + #S#.............# + ################# + +In this maze, the best paths cost `11048` points; following one such +path would look like this: + + ################# + #...#...#...#..E# + #.#.#.#.#.#.#.#^# + #.#.#.#...#...#^# + #.#.#.#.###.#.#^# + #>>v#.#.#.....#^# + #^#v#.#.#.#####^# + #^#v..#.#.#>>>>^# + #^#v#####.#^###.# + #^#v#..>>>>^#...# + #^#v###^#####.### + #^#v#>>^#.....#.# + #^#v#^#####.###.# + #^#v#^........#.# + #^#v#^#########.# + #S#>>^..........# + ################# + +Note that the path shown above includes one 90 degree turn as the very +first move, rotating the Reindeer from facing East to facing North. + +Analyze your map carefully. *What is the lowest score a Reindeer could +possibly get?* + +Your puzzle answer was `108504`. + +The first half of this puzzle is complete! It provides one gold star: \* + +## \-\-- Part Two \-\-- {#part2} + +Now that you know what the best paths look like, you can figure out the +best spot to sit. + +Every non-wall tile (`S`, `.`, or `E`) is equipped with places to sit +along the edges of the tile. While determining which of these tiles +would be the best spot to sit depends on a whole bunch of factors (how +comfortable the seats are, how far away the bathrooms are, whether +there\'s a pillar blocking your view, etc.), the most important factor +is *whether the tile is on one of the best paths through the maze*. If +you sit somewhere else, you\'d miss all the action! + +So, you\'ll need to determine which tiles are part of *any* best path +through the maze, including the `S` and `E` tiles. + +In the first example, there are `45` tiles (marked `O`) that are part of +at least one of the various best paths through the maze: + + ############### + #.......#....O# + #.#.###.#.###O# + #.....#.#...#O# + #.###.#####.#O# + #.#.#.......#O# + #.#.#####.###O# + #..OOOOOOOOO#O# + ###O#O#####O#O# + #OOO#O....#O#O# + #O#O#O###.#O#O# + #OOOOO#...#O#O# + #O###.#.#.#O#O# + #O..#.....#OOO# + ############### + +In the second example, there are `64` tiles that are part of at least +one of the best paths: + + ################# + #...#...#...#..O# + #.#.#.#.#.#.#.#O# + #.#.#.#...#...#O# + #.#.#.#.###.#.#O# + #OOO#.#.#.....#O# + #O#O#.#.#.#####O# + #O#O..#.#.#OOOOO# + #O#O#####.#O###O# + #O#O#..OOOOO#OOO# + #O#O###O#####O### + #O#O#OOO#..OOO#.# + #O#O#O#####O###.# + #O#O#OOOOOOO..#.# + #O#O#O#########.# + #O#OOO..........# + ################# + +Analyze your map further. *How many tiles are part of at least one of +the best paths through the maze?* + +Answer: + +Although it hasn\'t changed, you can still [get your puzzle +input](16/input). + diff --git a/2024/16/solution.py b/2024/16/solution.py new file mode 100644 index 0000000..6e766c5 --- /dev/null +++ b/2024/16/solution.py @@ -0,0 +1,87 @@ +#!/bin/python3 +import sys,time,re +from pprint import pprint +sys.path.insert(0, '../../') +from fred import list2int,get_re,nprint,lprint,loadFile,dijkstra,toGrid,dfs,bfs,findInGrid,get_value_in_direction,addTuples,subTuples,create_graph_from_grid,manhattan_distance +start_time = time.time() + +input_f = 'input' + +######################################### +# # +# Part 1 # +# # +######################################### + +def part1(): + grid = toGrid(input_f) + + start = findInGrid(grid,'S') + end = findInGrid(grid,'E') + + print(start,end) + + def a_star(graph, start, end, heuristic): + + import heapq + + # Priority queue stores (f_score, current_node, current_direction) + priority_queue = [(0, start, None)] + g_scores = {node: float('inf') for node in graph} # Cost from start to each node + g_scores[start] = 0 + + f_scores = {node: float('inf') for node in graph} + f_scores[start] = heuristic(start, end) + + previous_nodes = {node: None for node in graph} + + while priority_queue: + current_f_score, current_node, previous_direction = heapq.heappop(priority_queue) + + if current_node == end: + break + + for neighbor, weight in graph[current_node]: + current_direction = ( + neighbor[0] - current_node[0], # Row direction (-1, 0, 1) + neighbor[1] - current_node[1] # Col direction (-1, 0, 1) + ) + + turn_cost = 1000 if previous_direction and current_direction != previous_direction else 0 + tentative_g_score = g_scores[current_node] + weight + turn_cost + + if tentative_g_score < g_scores[neighbor]: + g_scores[neighbor] = tentative_g_score + f_scores[neighbor] = tentative_g_score + heuristic(neighbor, end) + previous_nodes[neighbor] = current_node + heapq.heappush(priority_queue, (f_scores[neighbor], neighbor, current_direction)) + + path = [] + while end is not None: + path.append(end) + end = previous_nodes[end] + path.reverse() + + return path, g_scores[path[-1]] + + + graph = create_graph_from_grid(grid,start,end,'#') + + path, score = a_star(graph,start,end,manhattan_distance) + #print(path) + return score #Instead of implementing edge cases, just try with turning (adding 1000) to the result, if that's wrong, just don't add it. I got it right by not turning right away. + +start_time = time.time() +print('Part 1:',part1(), '\t\t', round((time.time() - start_time)*1000), 'ms') + + +######################################### +# # +# Part 2 # +# # +######################################### +def part2(): + return + +start_time = time.time() +print('Part 2:',part2(), '\t\t', round((time.time() - start_time)*1000), 'ms') \ No newline at end of file diff --git a/2024/18/18.md b/2024/18/18.md new file mode 100644 index 0000000..ec66fde --- /dev/null +++ b/2024/18/18.md @@ -0,0 +1,144 @@ +## \-\-- Day 18: RAM Run \-\-- + +You and The Historians look a lot more pixelated than you remember. +You\'re [inside a computer](/2017/day/2) at the North Pole! + +Just as you\'re about to check out your surroundings, a program runs up +to you. \"This region of memory isn\'t safe! The User misunderstood what +a [pushdown +automaton](https://en.wikipedia.org/wiki/Pushdown_automaton) +is and their algorithm is pushing whole *bytes* down on top of us! +Run!\" + +The algorithm is fast - it\'s going to cause a byte to fall into your +memory space once every +[nanosecond](https://www.youtube.com/watch?v=9eyFDBPk4Yw)! +Fortunately, you\'re *faster*, and by quickly scanning the algorithm, +you create a *list of which bytes will fall* (your puzzle input) in the +order they\'ll land in your memory space. + +Your memory space is a two-dimensional grid with coordinates that range +from `0` to `70` both horizontally and vertically. However, for the sake +of example, suppose you\'re on a smaller grid with coordinates that +range from `0` to `6` and the following list of incoming byte positions: + + 5,4 + 4,2 + 4,5 + 3,0 + 2,1 + 6,3 + 2,4 + 1,5 + 0,6 + 3,3 + 2,6 + 5,1 + 1,2 + 5,5 + 2,5 + 6,5 + 1,4 + 0,4 + 6,4 + 1,1 + 6,1 + 1,0 + 0,5 + 1,6 + 2,0 + +Each byte position is given as an `X,Y` coordinate, where `X` is the +distance from the left edge of your memory space and `Y` is the distance +from the top edge of your memory space. + +You and The Historians are currently in the top left corner of the +memory space (at `0,0`) and need to reach the exit in the bottom right +corner (at `70,70` in your memory space, but at `6,6` in this example). +You\'ll need to simulate the falling bytes to plan out where it will be +safe to run; for now, simulate just the first few bytes falling into +your memory space. + +As bytes fall into your memory space, they make that coordinate +*corrupted*. Corrupted memory coordinates cannot be entered by you or +The Historians, so you\'ll need to plan your route carefully. You also +cannot leave the boundaries of the memory space; your only hope is to +reach the exit. + +In the above example, if you were to draw the memory space after the +first `12` bytes have fallen (using `.` for safe and `#` for corrupted), +it would look like this: + + ...#... + ..#..#. + ....#.. + ...#..# + ..#..#. + .#..#.. + #.#.... + +You can take steps up, down, left, or right. After just 12 bytes have +corrupted locations in your memory space, the shortest path from the top +left corner to the exit would take `22` steps. Here (marked with `O`) is +one such path: + + OO.#OOO + .O#OO#O + .OOO#OO + ...#OO# + ..#OO#. + .#.O#.. + #.#OOOO + +Simulate the first kilobyte (`1024` bytes) falling onto your memory +space. Afterward, *what is the minimum number of steps needed to reach +the exit?* + +Your puzzle answer was `294`. + +The first half of this puzzle is complete! It provides one gold star: \* + +## \-\-- Part Two \-\-- {#part2} + +The Historians aren\'t as used to moving around in this pixelated +universe as you are. You\'re afraid they\'re not going to be fast enough +to make it to the exit before the path is completely blocked. + +To determine how fast everyone needs to go, you need to determine *the +first byte that will cut off the path to the exit*. + +In the above example, after the byte at `1,1` falls, there is still a +path to the exit: + + O..#OOO + O##OO#O + O#OO#OO + OOO#OO# + ###OO## + .##O### + #.#OOOO + +However, after adding the very next byte (at `6,1`), there is no longer +a path to the exit: + + ...#... + .##..## + .#..#.. + ...#..# + ###..## + .##.### + #.#.... + +So, in this example, the coordinates of the first byte that prevents the +exit from being reachable are `6,1`. + +Simulate more of the bytes that are about to corrupt your memory space. +*What are the coordinates of the first byte that will prevent the exit +from being reachable from your starting position?* (Provide the answer +as two integers separated by a comma with no other characters.) + +Answer: + +Although it hasn\'t changed, you can still [get your puzzle +input](18/input). + diff --git a/2024/18/solution.py b/2024/18/solution.py new file mode 100644 index 0000000..75bfcea --- /dev/null +++ b/2024/18/solution.py @@ -0,0 +1,76 @@ +#!/bin/python3 +import sys,time,re +from pprint import pprint +sys.path.insert(0, '../../') +from fred import list2int,get_re,nprint,lprint,loadFile,bfs,get_value_in_direction,addTuples,grid_valid +start_time = time.time() + +input_f = 'input' + +######################################### +# # +# Part 1 # +# # +######################################### +def part1(): + instructions = [] + grid = [] + w = 70 + h = 70 + end = (h,w) + start = (0,0) + with open(input_f) as file: + for line in file: + l = list2int(line.rstrip().split(',')) + instructions.append((l[0],l[1])) + #print(instructions) + + grid = [[ '.' for x in range(0,w+1)] for y in range(0,h+1)] + + for i in range(1024): + x = instructions[i] + grid[x[0]][x[1]] = '#' + + + + def is_goal(node): + #print(node) + + return True if node == end else False + + def get_neighbors(node): + directions = ['up','down','left','right'] + offsets = { + 'up': (-1, 0), + 'down': (1, 0), + 'left': (0, -1), + 'right': (0, 1), + } + neighbors = [] + + # Loop through all the directions + for d in directions: + tmp = addTuples(offsets[d],node) + if get_value_in_direction(grid,node,d) != '#' and grid_valid(tmp[0],tmp[1],grid): + neighbors.append((tmp[0],tmp[1])) + # Return the list of valid neighbors + return neighbors + + goal_nodes, path = bfs((0,0),is_goal,get_neighbors) + print(goal_nodes) + return len(path[goal_nodes[0]])-1 + +start_time = time.time() +print('Part 1:',part1(), '\t\t', round((time.time() - start_time)*1000), 'ms') + + +######################################### +# # +# Part 2 # +# # +######################################### +def part2(): + return + +start_time = time.time() +print('Part 2:',part2(), '\t\t', round((time.time() - start_time)*1000), 'ms') \ No newline at end of file diff --git a/__pycache__/fred.cpython-311.pyc b/__pycache__/fred.cpython-311.pyc index 88f68b7..0f304cb 100644 Binary files a/__pycache__/fred.cpython-311.pyc and b/__pycache__/fred.cpython-311.pyc differ diff --git a/fred.py b/fred.py index cd59ccf..35f1b70 100644 --- a/fred.py +++ b/fred.py @@ -2,6 +2,17 @@ import sys,re,heapq from itertools import permutations from termcolor import colored +def findInGrid(grid:list,x:str) -> set: + """Find the location of a character in a grid. + + + """ + + for r,row in enumerate(grid): + for c, char in enumerate(row): + if char == x: + return (r,c) + def loadFile(input_f): """ Loads a file and returns its lines as a list. @@ -179,7 +190,6 @@ def dprint(graph: dict): except Exception as e: raise RuntimeError(f"An error occurred while printing the dictionary: {e}") - def lprint(x: str, log: bool): """ Prints a string if logging is enabled. @@ -271,7 +281,10 @@ def nprint(grid, cur: set = None, sign: str = None, positions:list = None): for idx, i in enumerate(grid): for jdx, j in enumerate(i): if jdx == 0: - prepend = str(idx)+' ' + if idx < 10: + prepend = ' '+str(idx)+' ' + else: + prepend = ''+str(idx)+' ' else: prepend = '' @@ -286,7 +299,7 @@ def nprint(grid, cur: set = None, sign: str = None, positions:list = None): else: if positions is not None: if (idx,jdx) in positions: - print(prepend+colored(grid[idx][jdx],'red'),end=' ') + print(prepend+colored(grid[idx][jdx],'green'),end=' ') else: print(prepend+grid[idx][jdx], end=' ') else: @@ -296,8 +309,26 @@ def nprint(grid, cur: set = None, sign: str = None, positions:list = None): for r in range(len(grid[0])): if r == 0: - print(' ',end='') - print(r,end=' ') + print(' 0',end='') + + else: + if r%2 == 0: + print(r,end='') + else: + print(' ',end='') + print() + for r in range(len(grid[0])): + if r == 0: + print(' ',end='') + + else: + if r%2 != 0: + print(r,end='') + else: + if r != 9: + print(' ',end='') + else: + print('',end='') print() def list2int(x): @@ -647,6 +678,9 @@ def dfs(grid:list, pos:set) -> list: dfs(pos[0], pos[1]) return list(visited) +def manhattan_distance(node, goal): + return abs(node[0] - goal[0]) + abs(node[1] - goal[1]) + # Should probably be added to the regular dfs. def flood_fill(cells, pos): """ @@ -677,4 +711,83 @@ def flood_fill(cells, pos): dfs(neighbor) dfs(pos) - return list(visited) \ No newline at end of file + return list(visited) + +def create_graph_from_grid(grid, start, end,wall): + """ + Converts a grid into a graph representation for use in pathfinding algorithms. + + Parameters: + - grid: A 2D list representing the grid, where '#' is a wall, and '.' is a path. + - start: The coordinates of the start node (row, col). + - end: The coordinates of the end node (row, col). + - wall: The string of the wall node. + + Returns: + - graph: A dictionary where each key is a node (row, col), and the value is a list of tuples. + Each tuple represents (neighbor, weight). + """ + rows, cols = len(grid), len(grid[0]) + graph = {} + + # Helper to get neighbors + def neighbors(r, c): + for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: # Up, Down, Left, Right + nr, nc = r + dr, c + dc + if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != '#': + yield (nr, nc) + + for r in range(rows): + for c in range(cols): + if grid[r][c] != '#': # Only process valid cells + graph[(r, c)] = [(neighbor, 1) for neighbor in neighbors(r, c)] # Weight is 1 + + return graph + +def a_star(graph, start, end, heuristic): + """ + A* Algorithm to find the shortest path between two nodes in a graph. + + Parameters: + - graph: A dictionary where each key is a node, and the value is a list of tuples. + Each tuple represents (neighbor, distance). + - start: The starting node (row, col). + - end: The destination node (row, col). + - heuristic: A function that estimates the cost from a node to the end. + + Returns: + - path: A list of nodes that represent the shortest path from start to end. + - total_cost: The total cost of the shortest path. + """ + priority_queue = [(0, start)] # (f_score, current_node) + g_scores = {node: float('inf') for node in graph} # Cost from start to each node + g_scores[start] = 0 + + f_scores = {node: float('inf') for node in graph} # Estimated total cost (g + h) + f_scores[start] = heuristic(start, end) + + previous_nodes = {node: None for node in graph} + + while priority_queue: + current_f_score, current_node = heapq.heappop(priority_queue) + + if current_node == end: + break + + for neighbor, weight in graph[current_node]: + tentative_g_score = g_scores[current_node] + weight + + if tentative_g_score < g_scores[neighbor]: + g_scores[neighbor] = tentative_g_score + f_scores[neighbor] = tentative_g_score + heuristic(neighbor, end) + previous_nodes[neighbor] = current_node + heapq.heappush(priority_queue, (f_scores[neighbor], neighbor)) + + # Reconstruct the path + path = [] + while end is not None: + path.append(end) + end = previous_nodes[end] + path.reverse() + + return path, g_scores[path[-1]] \ No newline at end of file