“自我”如何正确更新原始变量? N皇后问题中的递归/回溯(Python)

Posted

技术标签:

【中文标题】“自我”如何正确更新原始变量? N皇后问题中的递归/回溯(Python)【英文标题】:How can "self" update original variable correctly? Recursion/Backtracking in the N-queens problem (Python) 【发布时间】:2019-11-26 20:54:37 【问题描述】:

这是我解决 8-queens 问题的 python 程序。除了打印已解决的电路板的最后一步外,一切正常。我使用递归/回溯来用皇后填充棋盘,直到找到解决方案。包含解决方案的板对象是self,它是对b1 的引用,所以我假设我初始化的原始板b1 将被更新以包含最终解决的板,并将打印解决方案使用printBoard。但是,b1 没有更新,当我出于某种未知原因打印它时,它持有一个故障板。

编辑:在solve 中添加了placeQueen

EMPTY = 0
QUEEN = 1
RESTRICTED = 2

class Board:

    # initializes a 8x8 array
    def __init__ (self):
        self.board = [[EMPTY for x in range(8)] for y in range(8)]

    # pretty prints board
    def printBoard(self):
        for row in self.board:
            print(row)

    # places a queen on a board
    def placeQueen(self, x, y):
        # restricts row
        self.board[y] = [RESTRICTED for i in range(8)]

        # restricts column
        for row in self.board:
            row[x] = RESTRICTED

        # places queen
        self.board[y][x] = QUEEN

        self.fillDiagonal(x, y, 0, 0, -1, -1)   # restricts top left diagonal
        self.fillDiagonal(x, y, 7, 0, 1, -1)    # restructs top right diagonal
        self.fillDiagonal(x, y, 0, 7, -1, 1)    # restricts bottom left diagonal
        self.fillDiagonal(x, y, 7, 7, 1, 1)     # restricts bottom right diagonal

    # restricts a diagonal in a specified direction
    def fillDiagonal(self, x, y, xlim, ylim, xadd, yadd):
        if x != xlim and y != ylim:
            self.board[y + yadd][x + xadd] = RESTRICTED
            self.fillDiagonal(x + xadd, y + yadd, xlim, ylim, xadd, yadd)

    # recursively places queens such that no queen shares a row or
    # column with another queen, or in other words, no queen sits on a
    # restricted square. Should solve by backtracking until solution is found.
    def solve(self, col):

        if col == -1:
            return True

        for i in range(8):
            if self.board[i][col] == EMPTY:
                temp = self.copy()

                self.placeQueen(col, i)
                if self.solve(col - 1):
                    return True

                temp.board[i][col] = RESTRICTED
                self = temp.copy()

        return False

    # deep copies a board onto another board
    def copy(self):
        copy = Board()

        for i in range(8):
            for j in range (8):
                copy.board[j][i] = self.board[j][i]

        return copy


b1 = Board()
b1.solve(7)
b1.printBoard()

我知道我的实际求解器正在工作,因为当我像这样添加printBoard 时:

if col == -1:
    self.printBoard()
    return True

solve 方法中,打印了已解决的板。简而言之,为什么板子的self 实例没有更新b1

【问题讨论】:

您认为copy 会做什么?你是怎么用的? 我建议将copy()更改为仅复制8x8数组,然后不要尝试设置self,而是将8x8数组重置为保存的数组。 我认为错误是您将函数内部的 self 变量重新分配给 temp.copy() 的返回结果。 Python 创建了一个新变量 self ,仅此而已。所以你对原始对象的引用丢失了。如果要更改 self 对象,请更新其属性之一,而不是重新分配结果。 感谢您的建议。我在解决问题时错过了placeQueen 调用,我只是在复制代码和删除一些 cmets 时出错。 我使用复制作为一种将 Board 对象的板“复制”到另一个 Board 对象的方式。我这样做是为了保存一个棋盘,以防我想在回溯过程中撤消皇后的位置,如果这有意义的话。 【参考方案1】:

我相信您的问题与在解决方法中重新定义自我有关,我什至不确定您为什么要这样做。

查看这个问题了解更多详情:Is it safe to replace a self object by another object of the same type in a method?

像您正在做的那样重新分配 self 并不是重新分配“b1”引用。因此,当您再次引用 b1 并执行 printBoard 时,您引用的对象与求解完成时“self.printBoard()”将引用的对象不同。

我会退后一步,问问自己,为什么要从一开始就替换 self,以及这对你有什么好处。您可能也不需要,也不应该这样做。

【讨论】:

谢谢!这就是问题所在。我更改了我的代码,以便使用新信息更新自我,而不是完全重新分配。【参考方案2】:

我不确定这是如何工作的,因为从未调用过 placeQueen。因此,我没有看到按照建议添加印刷品会呈现成品板(我认为它是空的)。 [注意:最新的更新解决了这个问题]

使用受限方块的想法可能会奏效,但这里的实现方式(没有撤消选项)效率低下;为每个内部循环复制一个全新的 Board 对象非常昂贵。对于所有的麻烦,我们也可以在每次移动时执行迭代冲突检查,这至少节省了新堆对象的分配和垃圾收集成本。

至于返回完整的棋盘结果,在失败时使用selfself.boardNone 的返回值,而不是TrueFalse

其他几点:

由于解决难题不需要状态,我们可以(希望)同意复制板是低效的,我不确定允许__init__ 方法是否有什么意义。该类作为封装结构很好,我们应该在Board 类中隐藏EMPTYQUEEN 等静态变量,无论该类是静态的还是实例化的。 如果您决定保持类有状态,printBoard 不应生成 side effects--override __str__。 不要在整个代码中将hardcode 大小文字(例如8);这使得类变得僵化、难以维护并且容易出现拼写错误和一个错误。请改用len(self.board),并大量提供参数。 fillDiagonal 不需要递归。考虑使用列表推导或 numpy 来简化此矩阵遍历逻辑。 使用snake_case 变量名和docstrings 代替PEP-8 的主题标签cmets。如果您觉得有必要写 # restricts column 之类的评论,请考虑将相关块移动到名为 restrict_column(...) 的函数中并跳过该评论。

这是实现其中一些要点的初始重写:

class Board:
    EMPTY = 0
    QUEEN = 1
    DIRS = [(x, y) for x in range(-1, 2) for y in range(-1, 2) if x]

    def __init__ (self, size=8):
        self.board = [[Board.EMPTY] * size for _ in range(size)]

    def __str__(self):
        return "\n".join(map(str, self.board))

    def legal_from(self, row, col, dr, dc):
        while row >= 0 and row < len(self.board) and \
              col >= 0 and col < len(self.board[row]):
            if self.board[row][col] != Board.EMPTY:
                return False

            row += dr; col += dc

        return True

    def legal_move(self, row, col):
        return all([self.legal_from(row, col, *d) for d in Board.DIRS])

    def solve(self, row=0):
        if row >= len(self.board): 
            return self

        for col in range(len(self.board[row])):
            if self.legal_move(row, col):
                self.board[row][col] = Board.QUEEN

                if self.solve(row + 1):
                    return self

                self.board[row][col] = Board.EMPTY

if __name__ == "__main__":
    for result in [Board(i).solve() for i in range(9)]:
        print(result, "\n")

输出:

[1]

None

None

[0, 1, 0, 0]
[0, 0, 0, 1]
[1, 0, 0, 0]
[0, 0, 1, 0]

[1, 0, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 0, 1]
[0, 1, 0, 0, 0]
[0, 0, 0, 1, 0]

[0, 1, 0, 0, 0, 0]
[0, 0, 0, 1, 0, 0]
[0, 0, 0, 0, 0, 1]
[1, 0, 0, 0, 0, 0]
[0, 0, 1, 0, 0, 0]
[0, 0, 0, 0, 1, 0]

[1, 0, 0, 0, 0, 0, 0]
[0, 0, 1, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 0, 0]
[0, 0, 0, 0, 0, 0, 1]
[0, 1, 0, 0, 0, 0, 0]
[0, 0, 0, 1, 0, 0, 0]
[0, 0, 0, 0, 0, 1, 0]

[1, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 1]
[0, 0, 0, 0, 0, 1, 0, 0]
[0, 0, 1, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 1, 0]
[0, 1, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 1, 0, 0, 0, 0]

【讨论】:

感谢您的详细反馈。我是 Python 新手,您的观点对优化我的课程很有帮助。

以上是关于“自我”如何正确更新原始变量? N皇后问题中的递归/回溯(Python)的主要内容,如果未能解决你的问题,请参考以下文章

N皇后问题(递归)

N皇后递归程序

使用回溯和递归的 N 皇后问题中的错误

N皇后问题(递归回溯)

递归--N皇后问题

递归和回溯求解8皇后问题