hard回溯-N皇后+数独 2020-09-16(未允禁转)

2020-09-16  本文已影响0人  9_SooHyun

所谓leetcode hard级别的回溯问题

回溯问题其实就是一个模板题,都是通过【带状态恢复的dfs】实现
我们知道回溯的模板如下

def backTrack(visited_path, choice_list, global_result, target):
    # visited_path表示当前的问题阶段,choice_list表示当前问题面临的可选项,target出现表示问题无需继续向下搜索,这时result结算可行解,
    # 递归出口
    if visited_path meets target:
        update global_result
        return

    # 尝试每一个【当前可行】的选择
    for c in choice_list:
        # 2.更新问题状态: 必须包含3项,visited_path+choice_list+target
        visited_path.add(c)
        new_choice_list = choice_list.remove(c)
        update target
        # 3.递归
        backTrack(visited_path, new_choice_list, global_result, target)
        # 4.状态复原
        visited_path.remove(c)
        reset target

hard题难在什么地方呢?要说难,它就难在【当前可行】的选择需要满足更为复杂的限制条件,也仅此而已了
只要我们翻译好限制条件,将限制条件用code正确表示出来,其实就还是板子题

例1 leetcode51 N皇后

给定一个整数 n,返回所有不同的 n 皇后问题的解决方案,要求皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

示例:
输入:4
输出:[
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。

思路:
假定我们从上到下逐行放置一个皇后,现在我们在第i行要放置一个皇后,那么当前可行的选择有哪些呢?第i行的哪几个位置能放呢?解决了这个问题就好。我们知道,前i-1行已经放置的皇后所在的行、列、斜线都不能放,认为是前皇后势力覆盖区域
那么,我们维护一个dict来记录整个棋盘的每个方格的势力值,初始化为0。

这样,我们把【已放置皇后所在的行、列、斜线都不能放新皇后】这个限制条件很好地翻译成了代码形式来确定当前可行的选择,得解

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        # 设置好规则,【dfs】暴力尝试所有解。如果能够dfs到第n层的,说明整个路径上的皇后位置合法,纳入结果集。
        # 用queen_pos list记录可行的皇后位置;用字典invalid_pos记录当前不可放置的位置,0可放,非0不可放
        queen_pos = list()
        invalid_pos = dict()
        for i in range(n):
            for j in range(n):
                invalid_pos[(i,j)] = 0
        final_res = []

        def helper(queen_pos, invalid_pos, row_index, final_res):
            # 已经到整个棋盘的n行,说明0-n-1行都放好了,结算并返回
            if row_index == n:
                temp = []
                for p in queen_pos:
                    s = '.'*p[1] + 'Q' + '.'*(n-1-p[1])
                    temp.append(s)
                final_res.append(temp)
                return

            for i in range(n):
                pos = (row_index, i)
                if invalid_pos[pos] == 0:
                    # 尝试放一个皇后
                    queen_pos.append(pos)
                    ## 更新不可放的位置 ##
                    additional_invalid_pos = []
                    for j in range(n):
                        # 同一行不可再放
                        additional_invalid_pos.append((row_index, j))
                    for j in range(n):
                        # 同一列不可再放
                        additional_invalid_pos.append((j, i))
                    s = row_index + i
                    delta = i - row_index
                    for j in range(n):
                        # 双斜线不可再放
                        if n > s-j >= 0 : additional_invalid_pos.append((j, s-j)) 
                        if 0 <= j + delta < n: additional_invalid_pos.append((j, j+delta)) 
                    for ele in additional_invalid_pos:
                        invalid_pos[ele] += 1
                    ## 更新不可放的位置,完毕 ##
                    # dfs
                    helper(queen_pos, invalid_pos, row_index+1, final_res)
                    # 状态复原
                    queen_pos.pop()
                    for ele in additional_invalid_pos:
                        invalid_pos[ele] -= 1
        
        helper(queen_pos, invalid_pos, 0, final_res)
        return final_res

例2 leetcode 37. 解数独

编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 '.' 表示。
假设给定的数独只有唯一解,且数独棋盘为9 x 9规模

思路:
好了,又是翻译复杂限制条件来确定当前可行选择的回溯问题
维护3个set row_d, col_d, block_d,对应3个限制条件,记录每行、每列、每块当前含有的数字

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """

        # 维护3个set,记录每行、每列、每块当前含有的数字
        row_d, col_d, block_d = dict(), dict(), dict()
        for i in range(9):
            row_d[i] = set()
            col_d[i] = set()
            block_d[i] = set()
        # 初始化这3个set
        for i in range(9):
            for j in range(9):
                row_d[i].add(board[i][j])
                col_d[j].add(board[i][j])
                block_d[(i//3)*3+j//3].add(board[i][j])

        def backTracking(board, i, j, row_d, col_d, block_d) -> bool:
            # 递归出口,填到棋盘外的第九行,,,
            if i == 9:
                return True

            # 碰到数字格
            if board[i][j] != '.':
                if j == 8:
                    res = backTracking(board, i+1, 0, row_d, col_d, block_d)
                    if res:
                        return res
                else:
                    res = backTracking(board, i, j+1, row_d, col_d, block_d)
                    if res:
                        return res
            # 碰到空格
            else:
                for ii in range(1, 10):
                    ele = str(ii)
                    if ele not in row_d[i] and ele not in col_d[j] and ele not in block_d[(i//3)*3+j//3]:
                        # 修改状态
                        board[i][j] = ele
                        row_d[i].add(board[i][j])
                        col_d[j].add(board[i][j])
                        block_d[(i//3)*3+j//3].add(board[i][j])
                        # dfs
                        if j == 8:
                            res = backTracking(board, i+1, 0, row_d, col_d, block_d)
                            if res:
                                return res
                        else:
                            res = backTracking(board, i, j+1, row_d, col_d, block_d)
                            if res:
                                return res
                        # 恢复状态
                        row_d[i].remove(board[i][j])
                        col_d[j].remove(board[i][j])
                        block_d[(i//3)*3+j//3].remove(board[i][j])
                        board[i][j] = '.'
                return False
        
        backTracking(board, 0, 0, row_d, col_d, block_d)


上一篇下一篇

猜你喜欢

热点阅读