【问题标题】:N-queen backtracking in Python: how to return solutions instead of printing them?Python中的N皇后回溯:如何返回解决方案而不是打印它们?
【发布时间】:2013-12-29 03:19:30
【问题描述】:
def solve(n):
    #prepare a board
    board = [[0 for x in range(n)] for x in range(n)]
    #set initial positions
    place_queen(board, 0, 0)

def place_queen(board, row, column):
    """place a queen that satisfies all the conditions"""
    #base case
    if row > len(board)-1:
        print board
    #check every column of the current row if its safe to place a queen
    while column < len(board):
        if is_safe(board, row, column):
            #place a queen
            board[row][column] = 1
            #place the next queen with an updated board
            return place_queen(board, row+1, 0)
        else:
            column += 1
    #there is no column that satisfies the conditions. Backtrack
    for c in range(len(board)):
        if board[row-1][c] == 1:
            #remove this queen
            board[row-1][c] = 0
            #go back to the previous row and start from the last unchecked column
            return place_queen(board, row-1, c+1)

def is_safe(board, row, column):
    """ if no other queens threaten a queen at (row, queen) return True """
    queens = []
    for r in range(len(board)):
        for c in range(len(board)):
            if board[r][c] == 1:
                queen = (r,c)
                queens.append(queen)
    for queen in queens:
        qr, qc = queen
        #check if the pos is in the same column or row
        if row == qr or column == qc:
            return False
        #check diagonals
        if (row + column) == (qr+qc) or (column-row) == (qc-qr):
            return False
    return True

solve(4)

我为 N-queen 问题编写了 Python 代码,它会在找到所有可能的解决方案时打印出来。但是,我想修改此代码,使其返回所有解决方案(板配置)的列表,而不是打印它们。我尝试修改如下代码:

def solve(n):
    #prepare a board
    board = [[0 for x in range(n)] for x in range(n)]
    #set initial positions
    solutions = []
    place_queen(board, 0, 0, solutions)

def place_queen(board, row, column, solutions):
    """place a queen that satisfies all the conditions"""
    #base case
    if row > len(board)-1:
        solutions.append(board)
        return solutions
    #check every column of the current row if its safe to place a queen
    while column < len(board):
        if is_safe(board, row, column):
            #place a queen
            board[row][column] = 1
            #place the next queen with an updated board
            return place_queen(board, row+1, 0, solutions)
        else:
            column += 1
    #there is no column that satisfies the conditions. Backtrack
    for c in range(len(board)):
        if board[row-1][c] == 1:
            #remove this queen
            board[row-1][c] = 0
            #go back to the previous row and start from the last unchecked column
            return place_queen(board, row-1, c+1, solutions)

但是,一旦找到第一个解决方案,它就会立即返回,因此solutions 仅包含一个可能的板配置。由于 print vs. return 让我对回溯算法感到困惑,如果有人能告诉我如何修改上述代码,以及将来如何解决类似问题,我将不胜感激。 p>

我认为使用全局变量会起作用,但我从某处了解到,不鼓励使用全局变量来解决此类问题。有人也可以解释一下吗?

编辑:

def solve(n):
    #prepare a board
    board = [[0 for x in range(n)] for x in range(n)]
    #set initial positions
    solutions = list()
    place_queen(board, 0, 0, solutions)
    return solutions

def place_queen(board, row, column, solutions):
    """place a queen that satisfies all the conditions"""
    #base case
    if row > len(board)-1:
        #print board
        solutions.append(deepcopy(board)) #Q1
    #check every column of the current row if its safe to place a queen
    while column < len(board):
        if is_safe(board, row, column):
            #place a queen
            board[row][column] = 1
            #place the next queen with an updated board
            return place_queen(board, row+1, 0, solutions) #Q2
        else:
            column += 1
    #there is no column that satisfies the conditions. Backtrack
    for c in range(len(board)):
        if board[row-1][c] == 1:
            #remove this queen
            board[row-1][c] = 0
            #go back to the previous row and start from the last unchecked column
            return place_queen(board, row-1, c+1, solutions) #Q3

上面返回所有找到的解决方案,而不是打印它们。我还有几个问题(相关部分在上面的代码中标记为Q1和Q2)

  1. 为什么我们需要solutions.append(deepcopy(board))?换句话说,当我们执行solutions.append(board) 时到底发生了什么?为什么这会导致附加初始板[[0,0,0,0] ...]
  2. return place_queen(board, row+1, 0)place_queen(board, row+1, 0) 替换时,为什么会遇到问题?我们实际上并没有返回任何东西(或None),但是没有return,列表就会超出索引。

【问题讨论】:

  • 你在第一个 sn-p 中在哪里打印它们?
  • print board 当当前行超过板子大小时
  • 回答您的global 子问题:c2.com/cgi/wiki?GlobalVariablesAreBad
  • print vs. returnstackoverflow.com/questions/7664779/…(请不要在一篇文章中提出多个问题,尤其是重复的问题)。
  • 感谢@jonrsharpe 的链接,但恐怕这不是我要问的。 printreturn 之间的区别非常明显。让我烦恼的是,当我可以轻松地逐个打印预期结果时,我很难弄清楚如何返回一组预期结果。

标签: python recursion backtracking


【解决方案1】:

使用 Python 的强大功能!我建议 yield 你找到的解决方案,而不是 returning 他们。这样,您就为所有解决方案创建了一个 generator。用符号列出这个列表是非常简单的(如果你真的需要它)

[ solution for solution in solve(4) ]

或者干脆

list(solve(4))

编辑:

在您的情况下,solve()place_queen() 必须成为生成器。在solve() 你应该做最后一件事:return place_queen(board, 0, 0)。通过这个你返回一个生成器。 (您也可以使用for solution in place_queen(board, 0, 0): yield solution,但这只会传递产生的值。)

place_queen() 中,您应该执行for solution in place_queen(board, row+1, 0): yield solution 之类的操作,而不是return place_queen(board, row+1, 0) 之类的返回值。

EDIT2:

#!/usr/bin/env python

def solve(n):
    #prepare a board
    board = [[0 for x in range(n)] for x in range(n)]
    #set initial positions
    return place_queen(board, 0, 0)

def place_queen(board, row, column):
    """place a queen that satisfies all the conditions"""
    #base case
    if row > len(board)-1:
        yield board
    #check every column of the current row if its safe to place a queen
    while column < len(board):
        if is_safe(board, row, column):
            #place a queen
            board[row][column] = 1
            #place the next queen with an updated board
            for solution in place_queen(board, row+1, 0):
                yield solution
            return
        else:
            column += 1
    #there is no column that satisfies the conditions. Backtrack
    for c in range(len(board)):
        if board[row-1][c] == 1:
            #remove this queen
            board[row-1][c] = 0
            #go back to the previous row and start from the last unchecked column
            for solution in place_queen(board, row-1, c+1):
                yield solution

def is_safe(board, row, column):
    """ if no other queens threaten a queen at (row, queen) return True """
    queens = []
    for r in range(len(board)):
        for c in range(len(board)):
            if board[r][c] == 1:
                queen = (r,c)
                queens.append(queen)
    for queen in queens:
        qr, qc = queen
        #check if the pos is in the same column or row
        if row == qr or column == qc:
            return False
        #check diagonals
        if (row + column) == (qr+qc) or (column-row) == (qc-qr):
            return False
    return True

import sys, pprint
sys.setrecursionlimit(10000)
for solution in solve(8):
    pprint.pprint(solution)

【讨论】:

  • 这是一个有趣的方法。但是,用yield 替换所有return 会导致place_queen generator object,而我预计我会得到list 的生成器(这使您的解决方案有效)。也许我对生成器有些不理解。你有什么想法吗?
  • 当然,如果你yield 什么东西,那么你就是一个生成器,而不仅仅是一个函数。然后,任何使用您的人都应该迭代您,而不仅仅是期望获得价值。而不是solution = solve(4) 一个应该做某事像for solution in solve(4): print solution 或类似的。
  • 不幸的是,您的代码不能开箱即用(抱怨is_safe() 中缺少变量queens),否则我会迅速采用我的方法来处理您的代码。
  • 此方法似乎不起作用,因为在place_queen() 内的每个循环中没有return 语句,row 不断增加,并且您得到一个超出范围的索引错误。
  • 我在您的解决方案中采用了我的方法并粘贴了生成的代码。由于对称性,它为 8×8 棋盘找到 92 个解。
【解决方案2】:

您需要调整代码以在找到解决方案后回溯,而不是返回。当您发现自己从第一行回溯时,您只想返回(因为这表明您已经探索了所有可能的解决方案)。

我认为,如果您稍微更改代码结构并在列上无条件循环,当您超出行或列的范围时,回溯逻辑就会启动,这是最容易做到的:

import copy

def place_queen(board, row, column, solutions):
    """place a queen that satisfies all the conditions"""
    while True: # loop unconditionally
        if len(board) in (row, column): # out of bounds, so we'll backtrack
            if row == 0:   # base case, can't backtrack, so return solutions
                return solutions
            elif row == len(board): # found a solution, so add it to our list
                solutions.append(copy.deepcopy(board)) # copy, since we mutate board

            for c in range(len(board)): # do the backtracking
                if board[row-1][c] == 1:
                    #remove this queen
                    board[row-1][c] = 0
                    #go back to the previous row and start from the next column
                    return place_queen(board, row-1, c+1, solutions)

        if is_safe(board, row, column):
            #place a queen
            board[row][column] = 1
            #place the next queen with an updated board
            return place_queen(board, row+1, 0, solutions)

        column += 1

这适用于小板(如 4 个),但如果您尝试将它用于较大的板尺寸,则会达到 Python 的递归限制。 Python 没有优化尾递归,所以这段代码在从一行移动到另一行时会构建大量堆栈帧。

幸运的是,尾递归算法通常很容易变成迭代算法。以下是如何对上面的代码执行此操作:

import copy

def place_queen_iterative(n):
    board = [[0 for x in range(n)] for x in range(n)]
    solutions = []
    row = column = 0

    while True: # loop unconditionally
        if len(board) in (row, column):
            if row == 0:
                return solutions
            elif row == len(board):
                solutions.append(copy.deepcopy(board))

            for c in range(len(board)):
                if board[row-1][c] == 1:
                    board[row-1][c] = 0

                    row -= 1     # directly change row and column, rather than recursing
                    column = c+1
                    break        # break out of the for loop (not the while)

        elif is_safe(board, row, column):   # need "elif" here
            board[row][column] = 1

            row += 1      # directly update row and column
            column = 0

        else:             # need "else" here
            column += 1   # directly increment column value

请注意,迭代版本不需要使用不同的行和列起始值来调用,因此不需要这些作为参数。同样,可以在开始循环之前完成板子和结果列表的设置,而不是在辅助函数中(进一步减少函数参数)。

一个稍微更 Pythonic 的版本将是一个生成器,yields 其结果,而不是将它们收集在一个列表中以在最后返回,但这只需要一些小的更改(只需 yield致电solutions.append)。使用生成器还可以让您在每次有解决方案时跳过复制板,只要您可以依赖生成器的消费者在再次推进生成器之前立即使用结果(或制作自己的副本)。

另一个想法是简化您的董事会代表。您知道在给定的行中只能有一个皇后,那么为什么不将放置它的列存储在一维列表中(带有标记值,例如 1000 表示没有放置皇后)?所以[1, 3, 0, 2] 将是 4-queens 问题的解决方案,皇后在 (0, 1)(1, 3)(2, 0)(3, 2)(如果需要,可以使用 enumerate 获得这些元组)。这可以让您避免回溯步骤中的for 循环,并且可能还可以更轻松地检查一个正方形是否安全,因为您不需要在棋盘上搜索皇后。

编辑以解决您的其他问题:

在第 1 点,您必须对板进行深度复制,否则您最终会得到对同一板的引用列表。比较:

board = [0, 0]
results.append(board)    # results[0] is a reference to the same object as board
board[0] = 1             # this mutates results[0][0] too!
result.append(board)     # this appends another reference to board!
board[1] = 2             # this also appears in results[0][1] and result[1][1]

print(board)   # as expected, this prints [1, 2]
print(results) # this however, prints [[1, 2], [1, 2]], not [[0, 0], [1, 0]]

至于第二季度,您需要return 以阻止代码进一步运行。如果您想更清楚地说明返回值不重要,可以将 return 语句与递归调用分开:

place_queen(board, row+1, 0)
return

请注意,您当前的代码可能有效,但它正在做一些可疑的事情。您使用超出范围 (row == len(board)) 的 row 值调用 is_safe,这只是因为您的 is_safe 实现将它们报告为不安全(没有崩溃),您才能适当地回溯。当你在第 0 行的回溯代码中时(在运行的最后),你只会正确退出,因为 for 循环在 board[-1] 中找不到任何 1 值(即最后一行)。我建议对您的代码进行更多重构,以避免依赖此类怪癖来进行正确操作。

【讨论】:

  • 感谢您的精彩回答。就像你说的那样,我最好使用更简单的董事会代表。我更新了我的代码。你介意看看它并回答添加的问题吗?
  • @MaximusS:我已经编辑了您的其他问题的答案以及您最新代码的一些其他 cmets。
  • 另外,我从某处听说我应该避免使用deepcopy()。你知道这是为什么吗?
  • @MaximusS:好吧,如果不需要,您通常希望避免复制数据,这仅仅是因为它往往很慢(并且很明显它会增加程序的内存使用量)。但是如果你需要复制一个复杂的对象,copy.deepcopy 可能是最好的方法。对于像(非嵌套)列表这样的简单对象,您可以通过其他方式(如 list(old_lst)old_lst[:])进行复制,但这对于像 board 这样的嵌套结构是不够的。
  • 感谢您的大力帮助。我刚刚发布了另一个与此问题相同的问题。有空的时候看看。 stackoverflow.com/questions/20536523/…
猜你喜欢
  • 2020-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多