Leetcode-剪枝
51. N皇后 https://leetcode-cn.com/problems/n-queens/
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
解:
dfs + 剪枝,枚举每个行。注意在做回溯之前,要把当前放置的皇后拿掉,把其造成影响的标识位都消除。
class Solution: def solveNQueens(self, n: int) -> List[List[str]]: def could_place(row, col): # row这一行是没有放置过的行,要检查col这一列、(row,col)所占两条对角线有没有被放置过,如果都没有,(row,col)可以放皇后 return not (cols[col]+hill_diagonals[row-col]+\ dale_diagonals[row+col]) def place_queen(row, col): queens.add((row, col)) # 放皇后,记录位置,标记列和两对角线 cols[col] = 1 hill_diagonals[row-col] = 1 dale_diagonals[row+col] = 1 def remove_queen(row, col): queens.remove((row, col)) # 移除皇后,清空列和两对角线的标记 cols[col] = 0 hill_diagonals[row-col] = 0 dale_diagonals[row+col] = 0 def add_solution(): # 如果找到一个解,按要求记录下来 solution = [] for _, col in sorted(queens): solution.append('.'*col + 'Q' + '.'*(n-col-1)) output.append(solution) def dfs(row): # 从第一行row=0开始放置皇后,放到n-1行 for col in range(n): # 对于确定的row,遍历所有列col if could_place(row, col): place_queen(row, col) # 如果(row, col)可以放皇后,就放 if row == n-1: # 如果已经放了最后一个,说明找到一个解 add_solution() else: # 没有放到最后一个的话 dfs(row+1) # 去找row行之后所有可能的放置解法 remove_queen(row, col) # 不管是哪种情况都要回溯,移除当前皇后,进入(row, col+1) 的情况 cols = [0] * n hill_diagonals = [0] * (2 * n -1) dale_diagonals = [0] * (2 * n -1) queens = set() output = [] dfs(0) return output
52. N皇后ii https://leetcode-cn.com/problems/n-queens-ii/
给定一个整数 n,返回 n 皇后不同的解决方案的数量。
解:
跟#51基本相同,修改一下最后返回值的处理即可
class Solution: def totalNQueens(self, n: int) -> int: def could_place(row, col): # row这一行是没有放置过的行,要检查col这一列、(row,col)所占两条对角线有没有被放置过,如果都没有,(row,col)可以放皇后 return not (cols[col]+hill_diagonals[row-col]+\ dale_diagonals[row+col]) def place_queen(row, col): queens.add((row, col)) # 放皇后,记录位置,标记列和两对角线 cols[col] = 1 hill_diagonals[row-col] = 1 dale_diagonals[row+col] = 1 def remove_queen(row, col): queens.remove((row, col)) # 移除皇后,清空列和两对角线的标记 cols[col] = 0 hill_diagonals[row-col] = 0 dale_diagonals[row+col] = 0 def dfs(row): nonlocal res for col in range(n): if could_place(row, col): place_queen(row, col) if row == n-1: res += 1 else: dfs(row+1) remove_queen(row, col) # 把刚才放置的拿掉才能回溯 cols = [0] * n hill_diagonals = [0] * (2 * n - 1) dale_diagonals = [0] * (2 * n - 1) queens = set() res = 0 dfs(0) return res
36. 有效的数独 https://leetcode-cn.com/problems/valid-sudoku/
判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
数独部分空格内已填入了数字,空白格用 '.'
表示。
说明:
一个有效的数独(部分已被填充)不一定是可解的。
只需要根据以上规则,验证已经填入的数字是否有效即可。
给定数独序列只包含数字 1-9 和字符 '.' 。
给定数独永远是 9x9 形式的。
解:
两层嵌套循环遍历即可,分别对行、列、子数独用 n=9 个哈希表(其中key为1~9)来存所有已经遇到过的值,如果某个key的value大于1的话说明数独无效。
class Solution: def isValidSudoku(self, board: List[List[str]]) -> bool: # 9行,9列,9个子数独 rows = [{} for _ in range(9)] # 每个哈希表存,列表下标对应行的,已填数字情况 cols = [{} for _ in range(9)] boxes = [{} for _ in range(9)] # 子数独编号为boxes的下标,从上到下从左到右索引 for i in range(9): for j in range(9): num = board[i][j] if num != '.': # 如果某个位置已经填入数 num = int(num) box_idx = (i//3)*3 + j//3 # 当前位置所处的子数独索引 rows[i][num] = rows[i].get(num, 0) + 1 # 先保留当前的数,在哈希表中保存 cols[j][num] = cols[j].get(num, 0) + 1 boxes[box_idx][num] = boxes[box_idx].get(num, 0) + 1 # 检查如果保留当前数的话,是否合法 if rows[i][num] > 1 or cols[j][num] > 1 or boxes[box_idx][num] > 1: return False return True
37. 解数独 https://leetcode-cn.com/problems/sudoku-solver/
编写一个程序,通过已填充的空格来解决数独问题。
解:
dfs, 枚举每个空格。
from collections import defaultdict class Solution: def solveSudoku(self, board: List[List[str]]) -> None: """ Do not return anything, modify board in-place instead. """ def could_place(d, row, col): """ Check if one could place a number d in (row, col) cell """ return not (d in rows[row] or d in columns[col] or \ d in boxes[box_index(row, col)]) def place_number(d, row, col): """ Place a number d in (row, col) cell """ rows[row][d] += 1 columns[col][d] += 1 boxes[box_index(row, col)][d] += 1 board[row][col] = str(d) def remove_number(d, row, col): """ Remove a number which didn't lead to a solution """ del rows[row][d] del columns[col][d] del boxes[box_index(row, col)][d] board[row][col] = '.' def place_next_numbers(row, col): """ Call backtrack function in recursion to continue to place numbers till the moment we have a solution """ # if we're in the last cell # that means we have the solution if col == N - 1 and row == N - 1: nonlocal sudoku_solved sudoku_solved = True #if not yet else: # if we're in the end of the row # go to the next row if col == N - 1: backtrack(row + 1, 0) # go to the next column else: backtrack(row, col + 1) def backtrack(row = 0, col = 0): """ Backtracking """ # if the cell is empty if board[row][col] == '.': # iterate over all numbers from 1 to 9 for d in range(1, 10): if could_place(d, row, col): place_number(d, row, col) place_next_numbers(row, col) # if sudoku is solved, there is no need to backtrack # since the single unique solution is promised if not sudoku_solved: remove_number(d, row, col) else: place_next_numbers(row, col) # box size n = 3 # row size N = n * n # lambda function to compute box index box_index = lambda row, col: (row // n ) * n + col // n # init rows, columns and boxes rows = [defaultdict(int) for i in range(N)] columns = [defaultdict(int) for i in range(N)] boxes = [defaultdict(int) for i in range(N)] for i in range(N): for j in range(N): if board[i][j] != '.': d = int(board[i][j]) place_number(d, i, j) sudoku_solved = False backtrack()
除了普通的dfs,还可以加一些剪枝的条件来加速。先枚举可选项少的空格:预处理,先遍历一边格子,找出每个空格的可选数,并排序,dfs搜索时就从可选数最少的空格开始。