Solved 2024/18 P1

This commit is contained in:
FrederikBaerentsen 2024-12-18 07:09:36 +01:00
parent d119956812
commit 2674f6e723
6 changed files with 596 additions and 6 deletions

170
2024/16/16.md Normal file
View File

@ -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).

87
2024/16/solution.py Normal file
View File

@ -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')

144
2024/18/18.md Normal file
View File

@ -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).

76
2024/18/solution.py Normal file
View File

@ -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')

Binary file not shown.

123
fred.py
View File

@ -2,6 +2,17 @@ import sys,re,heapq
from itertools import permutations from itertools import permutations
from termcolor import colored 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): def loadFile(input_f):
""" """
Loads a file and returns its lines as a list. Loads a file and returns its lines as a list.
@ -179,7 +190,6 @@ def dprint(graph: dict):
except Exception as e: except Exception as e:
raise RuntimeError(f"An error occurred while printing the dictionary: {e}") raise RuntimeError(f"An error occurred while printing the dictionary: {e}")
def lprint(x: str, log: bool): def lprint(x: str, log: bool):
""" """
Prints a string if logging is enabled. 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 idx, i in enumerate(grid):
for jdx, j in enumerate(i): for jdx, j in enumerate(i):
if jdx == 0: if jdx == 0:
prepend = str(idx)+' ' if idx < 10:
prepend = ' '+str(idx)+' '
else:
prepend = ''+str(idx)+' '
else: else:
prepend = '' prepend = ''
@ -286,7 +299,7 @@ def nprint(grid, cur: set = None, sign: str = None, positions:list = None):
else: else:
if positions is not None: if positions is not None:
if (idx,jdx) in positions: if (idx,jdx) in positions:
print(prepend+colored(grid[idx][jdx],'red'),end=' ') print(prepend+colored(grid[idx][jdx],'green'),end=' ')
else: else:
print(prepend+grid[idx][jdx], end=' ') print(prepend+grid[idx][jdx], end=' ')
else: else:
@ -296,8 +309,26 @@ def nprint(grid, cur: set = None, sign: str = None, positions:list = None):
for r in range(len(grid[0])): for r in range(len(grid[0])):
if r == 0: if r == 0:
print(' ',end='') print(' 0',end='')
print(r,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() print()
def list2int(x): def list2int(x):
@ -647,6 +678,9 @@ def dfs(grid:list, pos:set) -> list:
dfs(pos[0], pos[1]) dfs(pos[0], pos[1])
return list(visited) 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. # Should probably be added to the regular dfs.
def flood_fill(cells, pos): def flood_fill(cells, pos):
""" """
@ -678,3 +712,82 @@ def flood_fill(cells, pos):
dfs(pos) dfs(pos)
return list(visited) 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]]