回溯算法 15. N皇后(难)

回溯算法 15. N皇后(难)

51. N 皇后 - 力扣(LeetCode)

代码随想录

难度 6 - 困难

  • 题目理解:

    n皇后问题,其实可以看成:按行顺序依次摆放皇后,每一行只能放一个皇后

    那么只要保证每一行新放置的皇后与之前放置的皇后之间,列不重叠且不在之前皇后的斜线上即可

  • 要点:

    下面是我的做法,但我的做法不如后面代码随想录的题解方便和快速,不过大体思想是相通的。

    1. 创建一个空棋盘used = [[None for _ in range(n)] for _ in range(n)]

      形如: [[None,None,...]...]

      每次放置一个皇后,就在对应位置放上'Q',然后将后续不允许使用的格子设置为'.'

    2. 定义回溯函数的传入参数def backTracking(self, n, row_idx, used):

      row_idx​记录当前放置的是第几行的皇后,从0开始

      n​就是不变的n皇后的n

      used​为传入的当前棋盘格的状态,既是当前回溯树的路径,也可以辅助后续放置新皇后(这里就和代码随想录的不太一样了)

    3. 定义回溯函数的终止条件if row_idx == n

      row_idx​等于n​的时候,说明前一层已经是叶子结点

      此时传入的used​其实就是最终棋盘,直接处理path = [''.join(row) for row in used]​,然后录入

    4. 回溯函数的设计:

      • 根据棋盘used​获得当前行所有允许使用的col_idx​(为None​的格子就是允许使用的格子)

      • 横向拓展: 当前合法row_idx​下,拓展不同的col_idx​作为子结点

        • 拷贝usedtemp_used

          这样做,一方面可以防止影响上一层递归函数;

          另一方面,同层的每一次拓展结点时都复制一次,相当于起到回溯的作用(回溯到没有放置过皇后的棋盘)

          因为我的写法中,每次放置皇后,要把皇后的位置以及防止皇后之后的无效位置全部填入占位符,只有None​的格子才是合法的,

          这就会导致撤销操作非常难实现,所以就直接用同层横向拓展时每次都深度拷贝棋盘used​这样的办法来解决回溯的需求了。(所以这就是为什么代码随想录的做法会更好)

        • 放置新皇后(修改棋盘格temp_used​),然后递归纵向拓展

    5. 放置新皇后的函数设计:def putQueen(self, n, used, row_idx, col_idx)

      用于修改放置皇后之后的used

      放置一个皇后'Q'​在used[row_idx][col_idx]​,然后将后续不允许使用的格子设置为'.'

      不允许使用的格子如下:

      • 同行同列格子放置'.'

      • 斜向格子放置'.'

        用下面的方法来直接一次性处理斜向(左下,左上,右下,右上)

        directions = [(-1,-1), (-1,1), (1,-1), (1,1)]
        for dx, dy in directions:
        	r, c = row_idx + dx, col_idx + dy  # 先移动一次位置,避免标记出发点
        	while 0 <= r < n and 0 <= c < n:
        		if not used[r][c]: 
        			used[r][c] = '.'
        		r += dx
        		c += dy
        
  • 个人代码:

    import copy
    
    class Solution:
        def solveNQueens(self, n: int) -> List[List[str]]:
            self.result = []
    
            # 创建一个n*n 列表[[None,None,...]...],
            # 每次放置一个皇后,就在该位置放上'Q',然后将后续不允许使用的格子设置为'.'
            used = [[None for _ in range(n)] for _ in range(n)]
    
            self.backTracking(n, 0, used)
            return self.result
    
        def backTracking(self, n, row_idx, used):
            # row_idx记录当前放置的是第几行的皇后,从0开始
            # 因为n皇后问题,其实可以看成,按行顺序依次摆放皇后,每一行只能放一个皇后,
            # 那么只要保证每一行的皇后之间,列不重叠且不在之前皇后的斜线上即可
    
            # 终止:row_idx等于n的时候,说明前一层已经是叶子结点
            if row_idx == n: 
                path = [''.join(row) for row in used]
                self.result.append(path)
                return
    
            # 获得当前行所有允许使用的col_idx
            valid_cols = [col for col in range(n) if not used[row_idx][col]]
    
            for col_idx in valid_cols: # 横向拓展:当前row_idx下,拓展不同的col_idx作为子结点
                # 拷贝,防止影响上一层递归函数;同时,同层的每一个拓展结点都复制一次,相当于起到回溯的作用
                temp_used = copy.deepcopy(used) 
                self.putQueen(n, temp_used, row_idx, col_idx) # temp_used会被修改
                self.backTracking(n, row_idx + 1, temp_used) # 纵向拓展下一行
    
    
        def putQueen(self, n, used, row_idx, col_idx):
            # 用于修改放置皇后之后的used
            # 放置一个皇后'Q'在used[row_idx][col_idx],然后将后续不允许使用的格子设置为'.'
            used[row_idx][col_idx] = 'Q'
            # 同行同列格子放置'.'
            for col in range(n):
                if not used[row_idx][col]:
                    used[row_idx][col] = '.'
            for row in range(n):
                if not used[row][col_idx]:
                    used[row][col_idx] = '.'
            # 斜向格子放置'.'
            directions = [(-1,-1), (-1,1), (1,-1), (1,1)]
            for dx, dy in directions:
                r, c = row_idx + dx, col_idx + dy  # 先移动一次位置,避免标记出发点
                while 0 <= r < n and 0 <= c < n:
                    if not used[r][c]: 
                        used[r][c] = '.'
                    r += dx
                    c += dy
    
    
  • 时间复杂度: O(n!)

  • 空间复杂度: O(n)

  • 看了下代码随想录中的做法和我的大致差不多,

    只是代码随想录中是在放置皇后的时候,先验证当前位置是否合法,然后再放置皇后,

    但是运行速度比我的快很多,我估计是我在横向拓展(for循环内)每一次都要复制一次棋盘的原因。

    他的做法还是要更好一些的,不过核心思想还是对棋盘的处理,大致思路还是相通的。

    摘录代码如下,供对比反思:

    class Solution:
        def solveNQueens(self, n: int) -> List[List[str]]:
            result = []  # 存储最终结果的二维字符串数组
    
            chessboard = ['.' * n for _ in range(n)]  # 初始化棋盘
            self.backtracking(n, 0, chessboard, result)  # 回溯求解
            return [[''.join(row) for row in solution] for solution in result]  # 返回结果集
    
        def backtracking(self, n: int, row: int, chessboard: List[str], result: List[List[str]]) -> None:
            if row == n:
                result.append(chessboard[:])  # 棋盘填满,将当前解加入结果集
                return
    
            for col in range(n):
                if self.isValid(row, col, chessboard):
                    chessboard[row] = chessboard[row][:col] + 'Q' + chessboard[row][col+1:]  # 放置皇后
                    self.backtracking(n, row + 1, chessboard, result)  # 递归到下一行
                    chessboard[row] = chessboard[row][:col] + '.' + chessboard[row][col+1:]  # 回溯,撤销当前位置的皇后
    
        def isValid(self, row: int, col: int, chessboard: List[str]) -> bool:
            # 检查列
            for i in range(row):
                if chessboard[i][col] == 'Q':
                    return False  # 当前列已经存在皇后,不合法
    
            # 检查 45 度角是否有皇后
            i, j = row - 1, col - 1
            while i >= 0 and j >= 0:
                if chessboard[i][j] == 'Q':
                    return False  # 左上方向已经存在皇后,不合法
                i -= 1
                j -= 1
    
            # 检查 135 度角是否有皇后
            i, j = row - 1, col + 1
            while i >= 0 and j < len(chessboard):
                if chessboard[i][j] == 'Q':
                    return False  # 右上方向已经存在皇后,不合法
                i -= 1
                j += 1
    
            return True  # 当前位置合法
    

你可能感兴趣的:(Mophead的小白刷题笔记,leetcode,python,代码随想录,回溯算法)