From f3ba9b6c508a24cd0e10fb08d0235c1f838fb73a Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 24 Dec 2020 18:16:21 +0530 Subject: [PATCH] [mypy] Add/fix type annotations for backtracking algorithms (#4055) * Fix mypy errors for backtracking algorithms * Fix CI failure --- backtracking/all_subsequences.py | 28 ++++++-------- backtracking/coloring.py | 8 ++-- backtracking/hamiltonian_cycle.py | 8 ++-- backtracking/knight_tour.py | 12 +++--- backtracking/minimax.py | 26 ++++++------- backtracking/n_queens_math.py | 63 ++++++++++++++----------------- backtracking/sudoku.py | 59 ++++++++++++++++------------- 7 files changed, 98 insertions(+), 106 deletions(-) diff --git a/backtracking/all_subsequences.py b/backtracking/all_subsequences.py index 9086e3a3d659..99db4ea46589 100644 --- a/backtracking/all_subsequences.py +++ b/backtracking/all_subsequences.py @@ -1,12 +1,11 @@ -from typing import Any, List - """ - In this problem, we want to determine all possible subsequences - of the given sequence. We use backtracking to solve this problem. +In this problem, we want to determine all possible subsequences +of the given sequence. We use backtracking to solve this problem. - Time complexity: O(2^n), - where n denotes the length of the given sequence. +Time complexity: O(2^n), +where n denotes the length of the given sequence. """ +from typing import Any, List def generate_all_subsequences(sequence: List[Any]) -> None: @@ -32,15 +31,10 @@ def create_state_space_tree( current_subsequence.pop() -""" -remove the comment to take an input from the user - -print("Enter the elements") -sequence = list(map(int, input().split())) -""" - -sequence = [3, 1, 2, 4] -generate_all_subsequences(sequence) +if __name__ == "__main__": + seq: List[Any] = [3, 1, 2, 4] + generate_all_subsequences(seq) -sequence = ["A", "B", "C"] -generate_all_subsequences(sequence) + seq.clear() + seq.extend(["A", "B", "C"]) + generate_all_subsequences(seq) diff --git a/backtracking/coloring.py b/backtracking/coloring.py index ceaffe3fae76..3956b21a9182 100644 --- a/backtracking/coloring.py +++ b/backtracking/coloring.py @@ -5,11 +5,11 @@ Wikipedia: https://en.wikipedia.org/wiki/Graph_coloring """ -from __future__ import annotations +from typing import List def valid_coloring( - neighbours: list[int], colored_vertices: list[int], color: int + neighbours: List[int], colored_vertices: List[int], color: int ) -> bool: """ For each neighbour check if coloring constraint is satisfied @@ -35,7 +35,7 @@ def valid_coloring( def util_color( - graph: list[list[int]], max_colors: int, colored_vertices: list[int], index: int + graph: List[List[int]], max_colors: int, colored_vertices: List[int], index: int ) -> bool: """ Pseudo-Code @@ -86,7 +86,7 @@ def util_color( return False -def color(graph: list[list[int]], max_colors: int) -> list[int]: +def color(graph: List[List[int]], max_colors: int) -> List[int]: """ Wrapper function to call subroutine called util_color which will either return True or False. diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index bf15cce4aca4..7be1ea350d7c 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -6,11 +6,11 @@ Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path """ -from __future__ import annotations +from typing import List def valid_connection( - graph: list[list[int]], next_ver: int, curr_ind: int, path: list[int] + graph: List[List[int]], next_ver: int, curr_ind: int, path: List[int] ) -> bool: """ Checks whether it is possible to add next into path by validating 2 statements @@ -47,7 +47,7 @@ def valid_connection( return not any(vertex == next_ver for vertex in path) -def util_hamilton_cycle(graph: list[list[int]], path: list[int], curr_ind: int) -> bool: +def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr_ind: int) -> bool: """ Pseudo-Code Base Case: @@ -108,7 +108,7 @@ def util_hamilton_cycle(graph: list[list[int]], path: list[int], curr_ind: int) return False -def hamilton_cycle(graph: list[list[int]], start_index: int = 0) -> list[int]: +def hamilton_cycle(graph: List[List[int]], start_index: int = 0) -> List[int]: r""" Wrapper function to call subroutine called util_hamilton_cycle, which will either return array of vertices indicating hamiltonian cycle diff --git a/backtracking/knight_tour.py b/backtracking/knight_tour.py index 2413ba468838..8e6613e07d8b 100644 --- a/backtracking/knight_tour.py +++ b/backtracking/knight_tour.py @@ -1,9 +1,9 @@ # Knight Tour Intro: https://www.youtube.com/watch?v=ab_dY3dZFHM -from __future__ import annotations +from typing import List, Tuple -def get_valid_pos(position: tuple[int], n: int) -> list[tuple[int]]: +def get_valid_pos(position: Tuple[int, int], n: int) -> List[Tuple[int, int]]: """ Find all the valid positions a knight can move to from the current position. @@ -32,7 +32,7 @@ def get_valid_pos(position: tuple[int], n: int) -> list[tuple[int]]: return permissible_positions -def is_complete(board: list[list[int]]) -> bool: +def is_complete(board: List[List[int]]) -> bool: """ Check if the board (matrix) has been completely filled with non-zero values. @@ -46,7 +46,9 @@ def is_complete(board: list[list[int]]) -> bool: return not any(elem == 0 for row in board for elem in row) -def open_knight_tour_helper(board: list[list[int]], pos: tuple[int], curr: int) -> bool: +def open_knight_tour_helper( + board: List[List[int]], pos: Tuple[int, int], curr: int +) -> bool: """ Helper function to solve knight tour problem. """ @@ -66,7 +68,7 @@ def open_knight_tour_helper(board: list[list[int]], pos: tuple[int], curr: int) return False -def open_knight_tour(n: int) -> list[list[int]]: +def open_knight_tour(n: int) -> List[List[int]]: """ Find the solution for the knight tour problem for a board of size n. Raises ValueError if the tour cannot be performed for the given size. diff --git a/backtracking/minimax.py b/backtracking/minimax.py index 91188090c899..dda29b47d6cc 100644 --- a/backtracking/minimax.py +++ b/backtracking/minimax.py @@ -1,18 +1,18 @@ -from __future__ import annotations - -import math +""" +Minimax helps to achieve maximum score in a game by checking all possible moves +depth is current depth in game tree. -""" Minimax helps to achieve maximum score in a game by checking all possible moves - depth is current depth in game tree. - nodeIndex is index of current node in scores[]. - if move is of maximizer return true else false - leaves of game tree is stored in scores[] - height is maximum height of Game tree +nodeIndex is index of current node in scores[]. +if move is of maximizer return true else false +leaves of game tree is stored in scores[] +height is maximum height of Game tree """ +import math +from typing import List def minimax( - depth: int, node_index: int, is_max: bool, scores: list[int], height: float + depth: int, node_index: int, is_max: bool, scores: List[int], height: float ) -> int: """ >>> import math @@ -32,10 +32,6 @@ def minimax( >>> height = math.log(len(scores), 2) >>> minimax(0, 0, True, scores, height) 12 - >>> minimax('1', 2, True, [], 2 ) - Traceback (most recent call last): - ... - TypeError: '<' not supported between instances of 'str' and 'int' """ if depth < 0: @@ -59,7 +55,7 @@ def minimax( ) -def main(): +def main() -> None: scores = [90, 23, 6, 33, 21, 65, 123, 34423] height = math.log(len(scores), 2) print("Optimal value : ", end="") diff --git a/backtracking/n_queens_math.py b/backtracking/n_queens_math.py index 811611971616..a8651c5c362e 100644 --- a/backtracking/n_queens_math.py +++ b/backtracking/n_queens_math.py @@ -75,14 +75,14 @@ for another one or vice versa. """ -from __future__ import annotations +from typing import List def depth_first_search( - possible_board: list[int], - diagonal_right_collisions: list[int], - diagonal_left_collisions: list[int], - boards: list[list[str]], + possible_board: List[int], + diagonal_right_collisions: List[int], + diagonal_left_collisions: List[int], + boards: List[List[str]], n: int, ) -> None: """ @@ -94,40 +94,33 @@ def depth_first_search( ['. . Q . ', 'Q . . . ', '. . . Q ', '. Q . . '] """ - """ Get next row in the current board (possible_board) to fill it with a queen """ + # Get next row in the current board (possible_board) to fill it with a queen row = len(possible_board) - """ - If row is equal to the size of the board it means there are a queen in each row in - the current board (possible_board) - """ + # If row is equal to the size of the board it means there are a queen in each row in + # the current board (possible_board) if row == n: - """ - We convert the variable possible_board that looks like this: [1, 3, 0, 2] to - this: ['. Q . . ', '. . . Q ', 'Q . . . ', '. . Q . '] - """ - possible_board = [". " * i + "Q " + ". " * (n - 1 - i) for i in possible_board] - boards.append(possible_board) + # We convert the variable possible_board that looks like this: [1, 3, 0, 2] to + # this: ['. Q . . ', '. . . Q ', 'Q . . . ', '. . Q . '] + boards.append([". " * i + "Q " + ". " * (n - 1 - i) for i in possible_board]) return - """ We iterate each column in the row to find all possible results in each row """ + # We iterate each column in the row to find all possible results in each row for col in range(n): - """ - We apply that we learned previously. First we check that in the current board - (possible_board) there are not other same value because if there is it means - that there are a collision in vertical. Then we apply the two formulas we - learned before: - - 45º: y - x = b or 45: row - col = b - 135º: y + x = b or row + col = b. - - And we verify if the results of this two formulas not exist in their variables - respectively. (diagonal_right_collisions, diagonal_left_collisions) - - If any or these are True it means there is a collision so we continue to the - next value in the for loop. - """ + # We apply that we learned previously. First we check that in the current board + # (possible_board) there are not other same value because if there is it means + # that there are a collision in vertical. Then we apply the two formulas we + # learned before: + # + # 45º: y - x = b or 45: row - col = b + # 135º: y + x = b or row + col = b. + # + # And we verify if the results of this two formulas not exist in their variables + # respectively. (diagonal_right_collisions, diagonal_left_collisions) + # + # If any or these are True it means there is a collision so we continue to the + # next value in the for loop. if ( col in possible_board or row - col in diagonal_right_collisions @@ -135,7 +128,7 @@ def depth_first_search( ): continue - """ If it is False we call dfs function again and we update the inputs """ + # If it is False we call dfs function again and we update the inputs depth_first_search( possible_board + [col], diagonal_right_collisions + [row - col], @@ -146,10 +139,10 @@ def depth_first_search( def n_queens_solution(n: int) -> None: - boards = [] + boards: List[List[str]] = [] depth_first_search([], [], [], boards, n) - """ Print all the boards """ + # Print all the boards for board in boards: for column in board: print(column) diff --git a/backtracking/sudoku.py b/backtracking/sudoku.py index 614bdb8530ac..3bfaddd6e56f 100644 --- a/backtracking/sudoku.py +++ b/backtracking/sudoku.py @@ -1,20 +1,20 @@ -from typing import List, Tuple, Union +""" +Given a partially filled 9×9 2D array, the objective is to fill a 9×9 +square grid with digits numbered 1 to 9, so that every row, column, and +and each of the nine 3×3 sub-grids contains all of the digits. + +This can be solved using Backtracking and is similar to n-queens. +We check to see if a cell is safe or not and recursively call the +function on the next column to see if it returns True. if yes, we +have solved the puzzle. else, we backtrack and place another number +in that cell and repeat this process. +""" +from typing import List, Optional, Tuple Matrix = List[List[int]] -""" - Given a partially filled 9×9 2D array, the objective is to fill a 9×9 - square grid with digits numbered 1 to 9, so that every row, column, and - and each of the nine 3×3 sub-grids contains all of the digits. - - This can be solved using Backtracking and is similar to n-queens. - We check to see if a cell is safe or not and recursively call the - function on the next column to see if it returns True. if yes, we - have solved the puzzle. else, we backtrack and place another number - in that cell and repeat this process. -""" # assigning initial values to the grid -initial_grid = [ +initial_grid: Matrix = [ [3, 0, 6, 5, 0, 8, 4, 0, 0], [5, 2, 0, 0, 0, 0, 0, 0, 0], [0, 8, 7, 0, 0, 0, 0, 3, 1], @@ -27,7 +27,7 @@ ] # a grid with no solution -no_solution = [ +no_solution: Matrix = [ [5, 0, 6, 5, 0, 8, 4, 0, 3], [5, 2, 0, 0, 0, 0, 0, 0, 2], [1, 8, 7, 0, 0, 0, 0, 3, 1], @@ -80,7 +80,7 @@ def is_completed(grid: Matrix) -> bool: return all(all(cell != 0 for cell in row) for row in grid) -def find_empty_location(grid: Matrix) -> Tuple[int, int]: +def find_empty_location(grid: Matrix) -> Optional[Tuple[int, int]]: """ This function finds an empty location so that we can assign a number for that particular row and column. @@ -89,9 +89,10 @@ def find_empty_location(grid: Matrix) -> Tuple[int, int]: for j in range(9): if grid[i][j] == 0: return i, j + return None -def sudoku(grid: Matrix) -> Union[Matrix, bool]: +def sudoku(grid: Matrix) -> Optional[Matrix]: """ Takes a partially filled-in grid and attempts to assign values to all unassigned locations in such a way to meet the requirements @@ -107,25 +108,30 @@ def sudoku(grid: Matrix) -> Union[Matrix, bool]: [1, 3, 8, 9, 4, 7, 2, 5, 6], [6, 9, 2, 3, 5, 1, 8, 7, 4], [7, 4, 5, 2, 8, 6, 3, 1, 9]] - >>> sudoku(no_solution) - False + >>> sudoku(no_solution) is None + True """ if is_completed(grid): return grid - row, column = find_empty_location(grid) + location = find_empty_location(grid) + if location is not None: + row, column = location + else: + # If the location is ``None``, then the grid is solved. + return grid for digit in range(1, 10): if is_safe(grid, row, column, digit): grid[row][column] = digit - if sudoku(grid): + if sudoku(grid) is not None: return grid grid[row][column] = 0 - return False + return None def print_solution(grid: Matrix) -> None: @@ -141,11 +147,12 @@ def print_solution(grid: Matrix) -> None: if __name__ == "__main__": # make a copy of grid so that you can compare with the unmodified grid - for grid in (initial_grid, no_solution): - grid = list(map(list, grid)) - solution = sudoku(grid) - if solution: - print("grid after solving:") + for example_grid in (initial_grid, no_solution): + print("\nExample grid:\n" + "=" * 20) + print_solution(example_grid) + print("\nExample grid solution:") + solution = sudoku(example_grid) + if solution is not None: print_solution(solution) else: print("Cannot find a solution.")