【问题标题】:Trying to understand recursion/backtracking, simple inelegant Sudoku example试图理解递归/回溯,简单不雅的数独示例
【发布时间】:2020-08-25 08:30:10
【问题描述】:

[这篇文章反响不佳,所以我提出了一些建议的修改,以尝试改进它以供后代使用。希望对以后发现它的人有所帮助!]

我一直在尝试使用一个简单的数独示例来理解递归/回溯/DFS。我对数独示例本身并不感兴趣,因此根据建议,我已将下面的示例最小化为仅 2x2 数独板,以便专注于让我对递归感到困惑的点(感谢@MisterMiyagi 的建议)。

在下面的代码中,我有一个辅助函数check_board,它接受一个 2x2 矩阵并检查是否有任何数字在其行和列中重复(即检查输入的 2x2 数独是否有效)。然后函数solve_sudoku是我理解的标准DFS/回溯算法,通过选择第一个空位置(由0表示)来解决数独,并尝试其中的值1和2,递归地寻找解决方案.

所以输入[[0,0], [0,2]] 输出应该是[[[2,1],[1,2]],但我收到的是输出False

@ThierryLathuille 通过注意到代码中的问题来提供帮助:在尝试了每个可能的后代节点之后(在这种情况下,通过尝试两个值 1、2),我错过了“回溯”步骤,需要添加一行重置值为 0,这意味着对于所有后续调用,矩阵中的方格将停留在 2 中(或者,在最初的 9x9 示例中,停留在 9 中):

当您看到您在正方形中尝试的最后一个值无法得出有效的解决方案时,您尝试下一个,直到达到 9。此时,您返回并返回递增前一个方块,但您当前的方块仍然包含值 9,因此它被认为不可用于下一次尝试。

您只需在返回之前将其恢复为原始值 0:

现在我仍然感到困惑和问题的重点:我正在尝试像一棵树一样思考递归。一旦其中一个节点尝试了正方形的每个值并且其后代节点都返回False,它不只是将False 报告给它的父节点吗?为什么其他人会再次查看带有 2 的板?

如果您能帮助我理解递归,我将不胜感激!

编辑:@ThierryLathuille 在评论中再次回答了这个问题!非常感谢!

请注意,当递归调用您的函数时,您不会传递棋盘的副本,而只会在任何地方操作同一个棋盘。当您的代码运行时,每次您探索树的一个分支时,您在探索过程中接触到的所有方块都会留下一个非零值,因此它们不再被视为空闲。

我有一个错误的想法,即每当以递归方式调用函数时,它都会获取其所有变量的新副本以进行操作。它当然可以,但不是输入!对代码的另一个修复是使用 Python 的 copy 模块和 copy_board = copy.deepcopy(board) 行,并在函数的每个实例化处操作并返回 copy_board,这是我错误地认为 Python 总是在递归中所做的。

也许这与按值传递与按引用传递有关,而 Python 总是按引用传递?任何关于这个主题的更多讨论仍然很感激!


这是带有修复它的行注释掉的损坏代码:

def check_board(board: list):
    for i in chars:
        for j in chars:
            for k in chars:
                if k == j:
                    continue
                if board[i][j] == board[i][k] and board[i][j] != 0:
                    return False
                if board[j][i] == board[k][i] and board[j][i] != 0:
                    return False
    return True

def solve_sudoku(board: list):
    chars = range(2)
    if not check_board(board):
        return False
    for i in chars:
        for j in chars:
            if board[i][j] == 0:
                for a in chars:
                    board[i][j] = a+1
                    if solve_sudoku(board):
                        return solve_sudoku(board)
                # uncommenting this next line fixes the algorithm
                #board[i][j] = 0
                return False
    return board


board = [[0,0],[0,2]]

if __name__ == "__main__":
    print(solve_sudoku(board))

输出: False

【问题讨论】:

  • 这是很多代码。请帮助我们通过删除无用的代码和 cmets 来帮助您,并更明确地描述您的问题——例如,哪个函数是递归的?如果你想知道递归,你能把这个例子减少到重现你的问题所需的最低限度吗?为什么您认为这是递归问题而不是众多检查之一?当您已经提供了一块您(希望)自己检查过的电路板时,我们是否真的需要 check_board 及其 6 个嵌套的 for 循环?
  • @MarcelBesixdouze 请不要编辑您的帖子说“已解决”。通过这样做,并删除已回答的初始问题的关键部分,该线程会为未来的访问者失去上下文,这些访问者将无法理解您最初遇到的问题。查看这些元线程:12
  • 非常感谢您做出这些更改。这些可能有助于理解 Python 如何传递对象:Are python variables pointersChanging an object pointed to by two variablesHow are arguments passes in Python

标签: python recursion depth-first-search backtracking sudoku


【解决方案1】:

当您看到您在正方形中尝试的最后一个值无法得出有效的解决方案时,您尝试下一个,直到达到 9。此时,您返回并返回递增前一个方块,但您当前的方块仍然包含值 9,因此它被认为不可用于下一次尝试。

您只需在返回之前将其恢复为原始值 0:

if board[3*a+c][3*b+d] == board[3*a+e][3*b+f] and board[3*a+c][3*b+d] != 0:
    board[i][j] = 0
    return False

输出:

[[4, 8, 6, 9, 1, 5, 7, 3, 2], 
 [7, 2, 5, 4, 6, 3, 1, 9, 8],
 [3, 9, 1, 7, 8, 2, 4, 5, 6], 
 [5, 6, 4, 1, 9, 7, 2, 8, 3],
 [9, 7, 3, 6, 2, 8, 5, 1, 4],
 [8, 1, 2, 5, 3, 4, 6, 7, 9],
 [2, 5, 7, 8, 4, 9, 3, 6, 1], 
 [1, 3, 8, 2, 5, 6, 9, 4, 7],
 [6, 4, 9, 3, 7, 1, 8, 2, 5]]

这是正确的答案。

虽然需要很长时间才能到达它(大约 30 秒左右......),因为您的代码的很多部分,特别是检查行/列/块中是否存在重复项非常效率低下。看看this question 了解有效方法的想法。

【讨论】:

  • 非常感谢!这真的很棒,但我仍然对正在发生的事情有点迷茫。我试图像一棵树一样思考递归。一旦其中一个节点尝试了正方形的每个值并且其后代节点都返回 False,它不只是将 False 报告给其父节点吗?为什么其他人会再看那个有 9 的棋盘?
  • 请注意,当递归调用您的函数时,您不会传递板的副本,而只会在任何地方操作同一个板。当您的代码运行时,每次您探索树的一个分支时,您在探索过程中接触到的所有方格都会留下一个非零值,因此它们不再被视为空闲。
  • 太棒了!我现在肯定明白了。这很有意义,我还将研究使用副本实现调用,看看它如何改变它以供我理解。非常感谢,这对您很有帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多