import sys,re,heapq from itertools import permutations from termcolor import colored 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. """ 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}") return lines def convert_list(input_list): """ Converts a list of strings to integers where possible, leaving others as strings. 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. """ if not isinstance(input_list, list): raise TypeError("Input must be a list.") converted_list = [] for item in input_list: try: converted_list.append(int(item)) # Attempt to convert string to integer except ValueError: converted_list.append(item) # Leave as string if conversion fails 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. Raises: FileNotFoundError: If the file cannot be found (if input is a file path). ValueError: If the parser function is invalid. """ if parser is not None and not callable(parser): raise ValueError("The parser must be a callable function.") grid = [] try: 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}") return grid def swap(a: int, b: int, lst: list): """ 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. Returns: list: The list with swapped elements. Raises: IndexError: If any index is out of range. TypeError: If lst is not a list. """ if not isinstance(lst, list): raise TypeError("The provided object is not a list.") 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 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]) 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. 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.") 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 """ if log: print(x) 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.") num_rows = len(grid) num_cols = len(grid[0]) expanded_grid = [] # Add top and bottom borders expanded_grid.append(['.'] * (num_cols + 2)) # Add left and right borders for each row for row in grid: expanded_grid.append(['.'] + row + ['.']) expanded_grid.append(['.'] * (num_cols + 2)) # Bottom border 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)) 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. 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.") 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: if len(sign) > 1: print(sign[0] + grid[idx][jdx] + sign[1], end='') # Print with sign else: print(colored(sign,'blue'), end=' ') # Print sign else: 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 print() 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.") return list(map(int, x)) 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.") rows = len(grid) cols = len(grid[0]) return 0 <= x < rows and 0 <= y < cols 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. """ if not isinstance(line, str): raise TypeError("Input string must be of type str.") match = re.match(pattern, line) if match: return match return None 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='') print() def get_value_in_direction(grid:list, position:set, direction:str=None, length:int=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(1,length+1): 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 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]] 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 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)) 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)