AdventOfCode/fred.py

664 lines
21 KiB
Python
Raw Normal View History

import sys,re,heapq
from itertools import permutations
2024-12-14 10:46:57 +01:00
from termcolor import colored
2024-11-28 18:47:06 +01:00
2024-12-05 19:47:18 +01:00
def loadFile(input_f):
"""
Loads a file and returns its lines as a list.
Args:
input_f (str): The file path to read from.
Returns:
list: A list of lines from the file, with each line stripped of trailing newlines.
Raises:
FileNotFoundError: If the file cannot be found.
IOError: If there is an error reading the file.
"""
2024-12-05 19:47:18 +01:00
lines = []
try:
with open(input_f) as file:
for line in file:
lines.append(line.rstrip()) # Removes trailing newline from each line
except FileNotFoundError:
raise FileNotFoundError(f"The file '{input_f}' was not found.")
except IOError as e:
raise IOError(f"Error reading file '{input_f}': {e}")
2024-12-05 19:47:18 +01:00
return lines
def convert_list(input_list):
"""
Converts a list of strings to integers where possible, leaving others as strings.
2024-12-05 19:47:18 +01:00
Args:
input_list (list): A list of strings to be converted.
Returns:
list: A list with integers or strings based on the input.
Raises:
TypeError: If the input is not a list.
2024-12-05 19:47:18 +01:00
"""
if not isinstance(input_list, list):
raise TypeError("Input must be a list.")
2024-12-05 19:47:18 +01:00
converted_list = []
for item in input_list:
try:
converted_list.append(int(item)) # Attempt to convert string to integer
2024-12-05 19:47:18 +01:00
except ValueError:
converted_list.append(item) # Leave as string if conversion fails
2024-12-05 19:47:18 +01:00
return converted_list
def toGrid(input, parser=None):
"""
Converts input (file or data) into a grid (list of lists).
Args:
input (str): The file path or data to be converted into a grid.
parser (function, optional): A parser function to process each line before adding to the grid.
Defaults to None.
Returns:
list: A 2D list (grid) representation of the input.
2024-12-05 19:47:18 +01:00
Raises:
FileNotFoundError: If the file cannot be found (if input is a file path).
ValueError: If the parser function is invalid.
"""
2024-12-11 18:48:33 +01:00
if parser is not None and not callable(parser):
raise ValueError("The parser must be a callable function.")
2024-12-10 15:14:29 +01:00
2024-11-28 18:47:06 +01:00
grid = []
try:
2024-11-28 18:47:06 +01:00
with open(input) as file:
for line in file:
grid.append(parser(list(line.rstrip())) if parser else list(line.rstrip())) # Use parser or default processing
except FileNotFoundError:
raise FileNotFoundError(f"The file '{input}' was not found.")
except IOError as e:
raise IOError(f"Error reading file '{input}': {e}")
2024-11-28 18:47:06 +01:00
return grid
def swap(a: int, b: int, lst: list):
2024-12-05 18:10:12 +01:00
"""
Swaps two elements in a list based on their indices.
Args:
a (int): Index of the first element.
b (int): Index of the second element.
lst (list): The list in which to swap elements.
2024-12-05 18:10:12 +01:00
Returns:
list: The list with swapped elements.
Raises:
IndexError: If any index is out of range.
TypeError: If lst is not a list.
2024-12-05 18:10:12 +01:00
"""
if not isinstance(lst, list):
raise TypeError("The provided object is not a list.")
2024-12-05 18:10:12 +01:00
if a < 0 or b < 0 or a >= len(lst) or b >= len(lst):
raise IndexError("Index out of range.") # Ensure indices are valid
lst[a], lst[b] = lst[b], lst[a] # Swap the elements
2024-12-05 18:10:12 +01:00
return lst
def addTuples(x: tuple, y: tuple):
"""
Adds two tuples element-wise.
Args:
x (tuple): The first tuple.
y (tuple): The second tuple.
Returns:
tuple: A tuple with the sum of corresponding elements of x and y.
Raises:
TypeError: If x or y are not tuples.
"""
if not isinstance(x, tuple) or not isinstance(y, tuple):
raise TypeError("Both inputs must be tuples.")
return (x[0] + y[0], x[1] + y[1])
2024-12-08 09:12:53 +01:00
def subTuples(x: tuple, y: tuple):
"""
Substracts two tuples element-wise.
Args:
x (tuple): The first tuple.
y (tuple): The second tuple.
Returns:
tuple: A tuple with the sum of corresponding elements of x and y.
Raises:
TypeError: If x or y are not tuples.
"""
if not isinstance(x, tuple) or not isinstance(y, tuple):
raise TypeError("Both inputs must be tuples.")
return (x[0] - y[0], x[1] - y[1])
def findDupes(input: list):
"""
Finds duplicate elements in a list and returns their values.
Args:
input (list): The list to check for duplicates.
2024-12-01 12:58:38 +01:00
Returns:
list: A list of duplicate values in the input.
Raises:
TypeError: If the input is not a list.
"""
if not isinstance(input, list):
raise TypeError("Input must be a list.")
2024-12-01 12:58:38 +01:00
return [item for item in set(input) if input.count(item) > 1]
def dprint(graph: dict):
"""
Prints a dictionary in a formatted way, with each key-value pair on a new line.
Args:
graph (dict): The dictionary to be printed.
Returns:
None
Raises:
TypeError: If the input is not a dictionary.
"""
if not isinstance(graph, dict):
raise TypeError("Input must be a dictionary.")
try:
print("\n".join("{}\t\t{}".format(k, v) for k, v in graph.items()))
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.
Args:
x (str): The string to print.
log (bool): A flag to control logging.
Returns:
None
"""
2024-11-30 20:13:48 +01:00
if log:
print(x)
2024-11-29 19:02:00 +01:00
def expand_grid(grid):
"""
Expands the grid by adding a border of '.' characters around it.
Args:
grid (list): A 2D grid to expand.
Returns:
list: A new 2D grid with a border added.
Raises:
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.")
2024-11-29 19:02:00 +01:00
num_rows = len(grid)
num_cols = len(grid[0])
expanded_grid = []
# Add top and bottom borders
2024-11-29 19:02:00 +01:00
expanded_grid.append(['.'] * (num_cols + 2))
# Add left and right borders for each row
2024-11-29 19:02:00 +01:00
for row in grid:
expanded_grid.append(['.'] + row + ['.'])
expanded_grid.append(['.'] * (num_cols + 2)) # Bottom border
2024-11-29 19:02:00 +01:00
return expanded_grid
def getCenter(grid):
"""
Gets the center position of a grid (middle of the grid).
Args:
grid (list): A 2D grid.
Returns:
tuple: A tuple containing the row and column index of the center.
Raises:
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.")
return (int(len(grid) / 2), int(len(grid[0]) / 2))
2024-12-14 10:46:57 +01:00
def nprint(grid, cur: set = None, sign: str = None, positions:list = None):
"""
Prints a grid, highlighting the current position if specified.
Args:
grid (list): A 2D grid to print.
cur (set, optional): A set containing the (row, col) indices of the current position.
Defaults to None.
sign (str, optional): The sign to highlight the current position with. Defaults to None.
2024-12-14 10:46:57 +01:00
positions (list, optional): A list of sets containing the (row, col) indices of positions to color.
Defaults to None.
Returns:
None
Raises:
TypeError: If cur is not a set or sign is not a string.
"""
if cur is not None and not isinstance(cur, tuple):
raise TypeError("Cur must be a tuple with (row, column) indices.")
if sign is not None and not isinstance(sign, str):
raise TypeError("Sign must be a string.")
2024-12-14 10:46:57 +01:00
if positions is not None and not isinstance(positions, list):
raise TypeError("Positions must be a list.")
for idx, i in enumerate(grid):
for jdx, j in enumerate(i):
if (idx, jdx) == cur:
2024-11-29 19:02:00 +01:00
if len(sign) > 1:
print(sign[0] + grid[idx][jdx] + sign[1], end='') # Print with sign
2024-11-29 19:02:00 +01:00
else:
2024-12-14 10:46:57 +01:00
print(colored(sign,'blue'), end=' ') # Print sign
else:
2024-12-14 10:46:57 +01:00
if positions is not None:
if (idx,jdx) in positions:
print(colored(grid[idx][jdx],'red'),end=' ')
else:
print(grid[idx][jdx], end=' ')
else:
print(grid[idx][jdx], end=' ') # Regular grid element
2024-11-28 18:47:06 +01:00
print()
2024-11-28 15:10:22 +01:00
2024-11-24 19:14:52 +01:00
def list2int(x):
"""
Converts a list of strings to a list of integers.
Args:
x (list): A list of strings to convert.
Returns:
list: A list of integers.
Raises:
TypeError: If x is not a list.
"""
if not isinstance(x, list):
raise TypeError("Input must be a list.")
2024-11-24 19:14:52 +01:00
return list(map(int, x))
2024-11-25 12:27:11 +01:00
def grid_valid(x, y, grid):
"""
Checks if a grid position is valid (within bounds).
Args:
x (int): The row index.
y (int): The column index.
grid (list): The 2D grid to check against.
Returns:
bool: True if the position is within bounds, False otherwise.
Raises:
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.")
2024-12-04 20:53:21 +01:00
rows = len(grid)
cols = len(grid[0])
return 0 <= x < rows and 0 <= y < cols
2024-12-13 21:42:53 +01:00
def get_re(pattern, line):
"""
Returns a match object if the pattern matches the string, else None.
Args:
pattern (str): The regular expression pattern to match.
str (str): The string to match against.
Returns:
match or None: A match object if a match is found, otherwise None.
Raises:
TypeError: If str is not a string.
"""
2024-12-13 21:42:53 +01:00
if not isinstance(line, str):
raise TypeError("Input string must be of type str.")
2024-12-13 21:42:53 +01:00
match = re.match(pattern, line)
2024-11-28 15:10:22 +01:00
if match:
return match
return None
2024-11-25 12:27:11 +01:00
def ppprint(x):
"""
Pretty prints a 2D grid or matrix.
Args:
x (list): A 2D grid to print.
Returns:
None
Raises:
TypeError: If x is not a list of lists.
"""
if not all(isinstance(row, list) for row in x):
raise TypeError("Input must be a list of lists.")
for idx, i in enumerate(x):
for jdx, j in enumerate(i):
print(x[idx][jdx], end='')
2024-11-25 12:27:11 +01:00
print()
2024-11-28 13:58:15 +01:00
2024-12-12 20:02:54 +01:00
def get_value_in_direction(grid:list, position:set, direction:str=None, length:int=1, type: str = None):
2024-11-28 13:58:15 +01:00
"""
2024-12-04 20:53:21 +01:00
Get the value(s) in a specified direction from a given position in a grid.
2024-11-28 13:58:15 +01:00
If no direction is provided, returns the value at the current position.
Args:
2024-12-04 20:53:21 +01:00
grid (list of list of int/float/str): The 2D grid.
2024-11-28 13:58:15 +01:00
position (set): A set containing x (row index) and y (column index) as integers.
direction (str, optional): The direction to check. Defaults to None.
2024-12-04 20:53:21 +01:00
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.
2024-12-04 20:53:21 +01:00
2024-11-28 13:58:15 +01:00
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.
2024-11-28 13:58:15 +01:00
"""
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
2024-11-28 13:58:15 +01:00
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).")
2024-11-28 13:58:15 +01:00
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
2024-12-04 20:53:21 +01:00
values = []
if length == 1:
# Check for out-of-bounds
2024-12-04 20:53:21 +01:00
if 0 <= new_x < len(grid) and 0 <= new_y < len(grid[new_x]):
return grid[new_x][new_y]
else:
return None
else:
2024-12-14 10:46:57 +01:00
for step in range(1,length+1):
2024-12-04 20:53:21 +01:00
new_x, new_y = x + step * dx, y + step * dy
# Check for out-of-bounds
2024-12-04 20:53:21 +01:00
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)
2024-11-28 13:58:15 +01:00
else:
2024-12-04 20:53:21 +01:00
return values
def dijkstra(graph, start, end):
"""
Dijkstra's 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.
- end: The destination node.
Returns:
- path: A list of nodes that represent the shortest path from start to end.
- total_distance: The total distance of the shortest path.
"""
priority_queue = [(0, start)] # Initially, we start with the starting node, with distance 0.
distances = {node: float('inf') for node in graph}
distances[start] = 0 # The distance to the start node is 0 (we're already there).
previous_nodes = {node: None for node in graph}
while priority_queue:
current_distance, current_node = heapq.heappop(priority_queue) # heapq.heappop() removes and returns the node with the smallest distance.
# If we're at the destination node, we can stop. We've found the shortest path.
if current_node == end:
break
for neighbor, weight in graph[current_node]:
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
previous_nodes[neighbor] = current_node
heapq.heappush(priority_queue, (distance, neighbor))
path = [] # This will store the cities in the shortest path.
while end is not None: # Start from the destination and trace backward.
path.append(end) # Add the current node to the path.
end = previous_nodes[end] # Move to the previous node on the path.
path.reverse()
return path, distances[path[-1]]
2024-12-11 23:40:08 +01:00
def TSP(graph, start, path_type='shortest',circle=None):
"""
Solves TSP (Traveling Salesperson Problem) using brute force by generating all possible paths.
Handles missing edges by skipping invalid paths.
Parameters:
- graph: A dictionary where each key is a node, and the value is a list of tuples (neighbor, distance).
- start: The starting node.
- path_type: Either 'shortest' or 'longest' to find the shortest or longest path.
Returns:
- shortest_path: The shortest path visiting all nodes exactly once and returning to the start.
- shortest_distance: The total distance of this path.
"""
# Validate path_type
if path_type not in ['shortest', 'longest']:
raise ValueError("path_type must be either 'shortest' or 'longest'.")
# Extract all unique cities (keys + neighbors)
cities = set(graph.keys())
for neighbors in graph.values():
for neighbor, _ in neighbors:
cities.add(neighbor)
# Ensure the start node is included
if start not in cities:
raise ValueError(f"Start location '{start}' not found in the graph.")
# Remove the start city from the set for permutation
cities.remove(start)
# Set the initial best_distance to the appropriate extreme (inf for shortest, -inf for longest)
best_distance = float('inf') if path_type == 'shortest' else -float('inf')
best_path = None
# Generate all permutations of the remaining cities
for perm in permutations(cities):
# Create a complete path starting and ending at the start node
2024-12-11 23:40:08 +01:00
if circle is not None:
path = [start] + list(perm) + [start]
else:
path = [start] + list(perm)
# Calculate the total distance of this path
total_distance = 0
valid_path = True
for i in range(len(path) - 1):
# Check if the edge exists in the graph
neighbors = dict(graph.get(path[i], []))
if path[i + 1] in neighbors:
total_distance += neighbors[path[i + 1]]
else:
valid_path = False
break # Stop checking this path if it's invalid
# If the path is valid, update the best_path based on the required path_type
if valid_path:
if (path_type == 'shortest' and total_distance < best_distance) or \
(path_type == 'longest' and total_distance > best_distance):
best_distance = total_distance
best_path = path
if not best_path:
print(f"No valid path found starting at '{start}'. Ensure all cities are connected.")
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))
2024-12-14 13:07:16 +01:00
return goal_nodes, paths_to_goal
def dfs(grid:list, pos:set) -> list:
"""
Perform a flood fill/dfs starting from the given position in the grid.
Args:
grid (list): 2D list representing the grid.
pos (tuple): Starting position (row, col) in the grid.
Returns:
list: List of visited positions.
"""
rows, cols = len(grid), len(grid[0])
start_value = grid[pos[0]][pos[1]]
visited = set()
print(rows,cols,start_value)
def is_valid(r, c):
return 0 <= r < rows and 0 <= c < cols and (r, c) not in visited and grid[r][c] == start_value
def dfs(r, c):
visited.add((r, c))
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nr, nc = r + dr, c + dc
if is_valid(nr, nc):
dfs(nr, nc)
dfs(pos[0], pos[1])
return list(visited)
# Should probably be added to the regular dfs.
def flood_fill(cells, pos):
"""
Perform a flood fill starting from the given position in a grid defined by a list of occupied cells.
Args:
cells (list): List of sets representing occupied positions in the grid.
pos (tuple): Starting position (row, col) in the grid.
Returns:
list: List of visited positions.
"""
occupied = set(cells) # Set of all occupied positions.
if pos not in occupied:
return 0, []
visited = set()
def is_valid(cell):
return cell in occupied and cell not in visited
def dfs(cell):
visited.add(cell)
r, c = cell
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
neighbor = (r + dr, c + dc)
if is_valid(neighbor):
dfs(neighbor)
dfs(pos)
return list(visited)