Solved 2024/10 P1+P2 and added BFS to helper file

This commit is contained in:
FrederikBaerentsen 2024-12-10 19:29:10 +01:00
parent e7a1efc09a
commit 5cc0f4fabf
9 changed files with 259 additions and 198 deletions

View File

@ -100,7 +100,86 @@ The reindeer gleefully carries over a protractor and adds it to the
pile. *What is the sum of the scores of all trailheads on your pile. *What is the sum of the scores of all trailheads on your
topographic map?* topographic map?*
To begin, [get your puzzle input](10/input). Your puzzle answer was `754`.
Answer: ## \-\-- Part Two \-\-- {#part2}
The reindeer spends a few minutes reviewing your hiking trail map before
realizing something, disappearing for a few minutes, and finally
returning with yet another slightly-charred piece of paper.
The paper describes a second way to measure a trailhead called its
*rating*. A trailhead\'s rating is the *number of distinct hiking
trails* which begin at that trailhead. For example:
.....0.
..4321.
..5..2.
..6543.
..7..4.
..8765.
..9....
The above map has a single trailhead; its rating is `3` because there
are exactly three distinct hiking trails which begin at that position:
.....0. .....0. .....0.
..4321. .....1. .....1.
..5.... .....2. .....2.
..6.... ..6543. .....3.
..7.... ..7.... .....4.
..8.... ..8.... ..8765.
..9.... ..9.... ..9....
Here is a map containing a single trailhead with rating `13`:
..90..9
...1.98
...2..7
6543456
765.987
876....
987....
This map contains a single trailhead with rating `227` (because there
are `121` distinct hiking trails that lead to the `9` on the right edge
and `106` that lead to the `9` on the bottom edge):
012345
123456
234567
345678
4.6789
56789.
Here\'s the larger example from before:
89010123
78121874
87430965
96549874
45678903
32019012
01329801
10456732
Considering its trailheads in reading order, they have ratings of `20`,
`24`, `10`, `4`, `1`, `4`, `5`, `8`, and `5`. The sum of all trailhead
ratings in this larger example topographic map is `81`.
You\'re not sure how, but the reindeer seems to have crafted some tiny
flags out of toothpicks and bits of paper and is using them to mark
trailheads on your topographic map. *What is the sum of the ratings of
all trailheads?*
Your puzzle answer was `1609`.
Both parts of this puzzle are complete! They provide two gold stars:
\*\*
At this point, you should [return to your Advent calendar](/2024) and
try another puzzle.
If you still want to see it, you can [get your puzzle
input](10/input).

16
2024/10/README.md Normal file
View File

@ -0,0 +1,16 @@
This calls for some explanations.
solutions_P1.py solves the first part and solutions_P2.py solves the second part.
I first started out writing the code in solutions_P2.py but i kept getting the wrong answers.
Basically I find a zero in the grid, then check up, down, left and right if any of them are
a one. If any are, call find_path, which looks for two, then three and so on.
Returns 1 each time a nine is found. This means all valid paths are returned.
I decided to read the problem again and started from a fresh solutions_P1.py file.
The problem can be solved using BFS (Breadth First Search). I implemented a standard BFS
function in my fred.py helper file.
Then used that to corrently find the solution.
Reading part 2 i realized my first code would be useful and without modifying it, it gave me
the right answer.

View File

@ -1,31 +0,0 @@
#!/bin/python3
import sys,time,re
from pprint import pprint
sys.path.insert(0, '../../')
from fred import list2int,get_re,nprint,lprint,loadFile,toGrid,TSP,dijkstra
start_time = time.time()
input_f = 'test'
part = 1
#########################################
# #
# Part 1 #
# #
#########################################
if part == 1:
grid = toGrid(input_f,True)
nprint(grid)
print(dijkstra(grid,0,9))
#########################################
# #
# Part 2 #
# #
#########################################
if part == 2:
exit()
print("--- %s seconds ---" % (time.time() - start_time))

67
2024/10/solution_P1.py Normal file
View File

@ -0,0 +1,67 @@
#!/bin/python3
import sys,time,re
from pprint import pprint
sys.path.insert(0, '../../')
from fred import list2int,get_re,nprint,lprint,loadFile,toGrid,TSP,dijkstra,bfs,get_value_in_direction,addTuples
start_time = time.time()
input_f = 'input'
grid = toGrid(input_f,list2int)
directions = [(-1,0),(1,0),(0,1),(0,-1)]
starts = []
ends = []
# Get coordinates of all start points (0) and all end points (9)
for row in range(len(grid)):
for col in range(len(grid[row])):
if grid[row][col] == 0:
starts.append((row,col))
elif grid[row][col] == 9:
ends.append((row,col))
# If the current node is in the list of end nodes, return it.
def is_goal(node):
return node in ends
# Get the neighbors of the current node.
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:
t = get_value_in_direction(grid,node)
# If the direction is 1 more than the current nodes value,
# add it to the list of valid neighbors.
if get_value_in_direction(grid,node,d) == t+1:
neighbors.append(addTuples(offsets[d],node))
# Return the list of valid neighbors
return neighbors
result = 0
# Loop through all the starting points.
for s in starts:
# Call a standard BFS (Breadth First Search).
# Takes the following inputs:
# s: Starting node
# is_goal: function that returns true if the current node is the goal
# get_neighbors: returns a list of valid neighbors
# Returns a list of all visited goals and the path to the goals.
goal_nodes, paths_to_goal = bfs(s,is_goal,get_neighbors)
# We onlu care about how many goals each starting position reached.
result += len(goal_nodes)
print(result)
print("--- %s seconds ---" % (time.time() - start_time))

44
2024/10/solution_P2.py Normal file
View File

@ -0,0 +1,44 @@
#!/bin/python3
import sys,time,re
from pprint import pprint
sys.path.insert(0, '../../')
from fred import list2int,get_re,nprint,lprint,loadFile,toGrid, get_value_in_direction,addTuples,grid_valid
start_time = time.time()
input_f = 'input'
result = 0
dir = {
'up': (-1, 0),
'down': (1, 0),
'left': (0, -1),
'right': (0, 1)
}
grid = toGrid(input_f,list2int)
def find_path(grid:list,pos:set,cur:set):
path = 0
if cur == 9:
return 1
cur+=1
for direction in ['up', 'down', 'left', 'right']:
next_pos = addTuples(pos, dir[direction])
if get_value_in_direction(grid,pos,direction)== cur:
path += find_path(grid, next_pos, cur)
return path
for r, row in enumerate(grid):
for c, col in enumerate(row):
if grid[r][c] == 0:
for direction in ['up', 'down', 'left', 'right']:
next_pos = addTuples((r,c), dir[direction])
if get_value_in_direction(grid,(r,c),direction) == 1:
result += find_path(grid,next_pos,1)
print(result)
print("--- %s seconds ---" % (time.time() - start_time))

View File

@ -1,162 +0,0 @@
#!/bin/python3
import sys,time,re
from pprint import pprint
sys.path.insert(0, '../../')
from fred import list2int,get_re,nprint,lprint,loadFile,toGrid, get_value_in_direction,addTuples
start_time = time.time()
input_f = 'test2'
part = 1
#########################################
# #
# Part 1 #
# #
#########################################
if part == 1:
def get_value_in_direction(grid, position, direction=None, length=1, type: str = None):
"""
Get the value(s) in a specified direction from a given position in a grid.
If no direction is provided, returns the value at the current position.
Args:
grid (list of list of int/float/str): The 2D grid.
position (set): A set containing x (row index) and y (column index) as integers.
direction (str, optional): The direction to check. Defaults to None.
length (int, optional): The number of steps to check in the given direction. Default is 1.
type (str, optional): The type of result to return ('list' or 'str'). Defaults to None.
Returns:
list or str: A list or string of values in the specified direction, or a single value.
Raises:
ValueError: If direction is invalid or position is not a set of two integers.
TypeError: If grid is not a list of lists.
"""
if not all(isinstance(row, list) for row in grid):
raise TypeError("Grid must be a list of lists.")
# Ensure position is a set of two integers
if len(position) != 2 or not all(isinstance(coord, int) for coord in position):
raise ValueError("Position must be a set containing two integers (x, y).")
x, y = position
offsets = {
'up': (-1, 0),
'down': (1, 0),
'left': (0, -1),
'right': (0, 1),
'up-left': (-1, -1),
'up-right': (-1, 1),
'down-left': (1, -1),
'down-right': (1, 1)
}
# If no direction is given, return the value at the current position
if direction is None:
if 0 <= x < len(grid) and 0 <= y < len(grid[x]):
return grid[x][y]
else:
return None
# Validate direction
if direction not in offsets:
raise ValueError(f"Invalid direction: {direction}. Choose from {list(offsets.keys())}")
dx, dy = offsets[direction]
new_x, new_y = x + dx, y + dy
values = []
if length == 1:
# Check for out-of-bounds
if 0 <= new_x < len(grid) and 0 <= new_y < len(grid[new_x]):
return grid[new_x][new_y]
else:
return None
else:
for step in range(length):
new_x, new_y = x + step * dx, y + step * dy
# Check for out-of-bounds
if 0 <= new_x < len(grid) and 0 <= new_y < len(grid[new_x]):
values.append(grid[new_x][new_y])
else:
return [] # Return empty list if any position is out of bounds
if type == 'list':
return values
elif type == 'str':
return ''.join(values)
else:
return values
grid = toGrid(input_f,True)
def extract_tuples(nested_list):
result = []
for item in nested_list:
if isinstance(item, tuple): # Check if the item is a tuple
result.append(item)
elif isinstance(item, list): # If it's a list, recurse into it
result.extend(extract_tuples(item))
return result
def find_path(grid,pos:set,cur):
path = 0
if cur == 9:
return 1
cur+=1
if get_value_in_direction(grid,pos,'up') == cur:
path += find_path(grid,addTuples(pos,(-1,0)),cur)
if get_value_in_direction(grid,pos,'down') == cur:
path += find_path(grid,addTuples(pos,(1,0)),cur)
if get_value_in_direction(grid,pos,'left') == cur:
path += find_path(grid,addTuples(pos,(0,-1)),cur)
if get_value_in_direction(grid,pos,'right') == cur:
path += find_path(grid,addTuples(pos,(0,1)),cur)
return path
result = 0
for r, row in enumerate(grid):
for c, col in enumerate(row):
#get_value_in_direction(grid, position, direction=None, length=1, type: str = None):
if grid[r][c] == 0:
if get_value_in_direction(grid,(r,c),'up') == 1:
#print(find_path(grid,(r-1,c),1))
result += find_path(grid,(r-1,c),1)
if get_value_in_direction(grid,(r,c),'down') == 1:
result += find_path(grid,(r+1,c),1)
#print(find_path(grid,(r+1,c),1,0))
if get_value_in_direction(grid,(r,c),'left') == 1:
result += find_path(grid,(r,c-1),1)
#print(find_path(grid,(r,c-1),1,0))
if get_value_in_direction(grid,(r,c),'right') == 1:
result += find_path(grid,(r,c+1),1)
#print(find_path(grid,(r,c+1),1,0))
print(result)
#########################################
# #
# Part 2 #
# #
#########################################
if part == 2:
exit()
print("--- %s seconds ---" % (time.time() - start_time))

View File

@ -1,7 +1,7 @@
# AdventOfCode # AdventOfCode
## 2024 ## 2024
.-----. .------------------.
.--'~ ~ ~| .-' * \ / '-. 1 ** .--'~ ~ ~| .-' * \ / '-. 1 **
.--'~ ,* ~ | | >o< \_\_\|_/__/ | 2 ** .--'~ ,* ~ | | >o< \_\_\|_/__/ | 2 **
.---': ~ '(~), ~| | >@>O< o-_/.()__------| 3 ** .---': ~ '(~), ~| | >@>O< o-_/.()__------| 3 **
@ -11,6 +11,7 @@
|#~~~@@@@ @ | |/\ ''. | | -/ :| 7 ** |#~~~@@@@ @ | |/\ ''. | | -/ :| 7 **
|~~..--. _____ | |* /~\ '.| | - / .'| 8 ** |~~..--. _____ | |* /~\ '.| | - / .'| 8 **
'---' ||[][]_\-| |~/ * \ :| | *..' | 9 ** '---' ||[][]_\-| |~/ * \ :| | *..' | 9 **
|------- | | /\ .'| |'''~~~~~| 10 **
## 2023 ## 2023

Binary file not shown.

49
fred.py
View File

@ -71,7 +71,7 @@ def toGrid(input, parser=None):
try: try:
with open(input) as file: with open(input) as file:
for line in file: for line in file:
grid.append(list2int(list(line.rstrip())) if parser else list(line.rstrip())) # Use parser or default processing grid.append(parser(list(line.rstrip())) if parser else list(line.rstrip())) # Use parser or default processing
except FileNotFoundError: except FileNotFoundError:
raise FileNotFoundError(f"The file '{input}' was not found.") raise FileNotFoundError(f"The file '{input}' was not found.")
except IOError as e: except IOError as e:
@ -540,3 +540,50 @@ def TSP(graph, start, path_type='shortest'):
print(f"No valid path found starting at '{start}'. Ensure all cities are connected.") print(f"No valid path found starting at '{start}'. Ensure all cities are connected.")
return best_path, best_distance if best_path else None return best_path, best_distance if best_path else None
def bfs(start, is_goal, get_neighbors, max_depth=float('inf')):
"""
Generic Breadth-First Search (BFS) function.
Args:
start: The starting node/state
is_goal: A function that checks if the current node is a goal state
get_neighbors: A function that returns valid neighboring nodes/states
max_depth: Maximum search depth to prevent infinite loops (default: no limit)
Returns:
tuple: (goal_nodes, paths_to_goal)
- goal_nodes: List of nodes that satisfy the goal condition
- paths_to_goal: Dictionary mapping goal nodes to their paths from start
"""
# Queue for BFS: each element is (current_node, path_to_node, depth)
queue = [(start, [start], 0)]
# Track visited nodes to prevent cycles
visited = set([start])
# Store goal nodes and their paths
goal_nodes = []
paths_to_goal = {}
while queue:
current, path, depth = queue.pop(0)
# Check if current node is a goal
if is_goal(current):
goal_nodes.append(current)
paths_to_goal[current] = path
# Stop if max depth reached
if depth >= max_depth:
continue
# Explore neighbors
for neighbor in get_neighbors(current):
# Prevent revisiting nodes
if neighbor not in visited:
visited.add(neighbor)
new_path = path + [neighbor]
queue.append((neighbor, new_path, depth + 1))
return goal_nodes, paths_to_goal