【问题标题】:Puzzled about backtracking in Eight Queen对八皇后中的回溯感到困惑
【发布时间】:2013-07-29 13:59:49
【问题描述】:

虽然我做过一些简单的练习(例如斐波那契),但我很难理解递归和回溯。所以请允许我在这里展示我的“脑流”:

  1. 我读了教科书,知道如果前一个皇后的当前位置消除了将下一个皇后放在下一列的可能性,则可以使用回溯来删除前一个皇后。所以这似乎很容易,我需要做的就是删除它,让程序决定下一个可能的位置。

  2. 过了一会儿,我发现程序停在了第 6 个皇后,所以我发现如果我简单地删除第 5 个皇后,程序只需将其放回当前位置(即,前 4 个皇后是第 5 个皇后)皇后总是落入同一个地方,这并不奇怪)。所以我想我需要跟踪最后一个女王的位置。

  3. 这是我感到困惑的时候。如果我要跟踪最后一个女王的位置(这样当我回溯程序时不允许将女王放在同一个地方),有两个潜在的困难:

a) 假设我移除了第 5 个皇后,我必须编写代码来决定它的下一个可能位置。这可以通过忽略其当前位置(在被移除之前)来解决,并继续在接下来的行中寻找可能的位置。

b) 我应该追踪所有之前的皇后吗?似乎是这样。假设实际上我必须删除的不是一个皇后,而是两个皇后(甚至更多),我当然需要跟踪它们当前的所有位置。但这比看起来要复杂得多。

  1. 于是我开始在课本中寻找答案。遗憾的是它没有回溯代码,只有递归代码。然后我在这里找到了代码:

http://www.geeksforgeeks.org/backtracking-set-3-n-queen-problem/

这让我大吃一惊,因为它是如此简单,但它确实有效!唯一的回溯部分是删除最后一个女王!所以问题是:下面的代码如何确保当给定 4 个皇后的位置时,第 5 个皇后不会一次又一次地落到同一个地方?我想我不明白的是你怎么能递归地回溯(比如说你需要删除更多的皇后)。递归前行没问题,但是递归后退怎么办?

/* A recursive utility function to solve N Queen problem */
bool solveNQUtil(int board[N][N], int col)
{
/* base case: If all queens are placed then return true */
if (col >= N)
    return true;

/* Consider this column and try placing this queen in all rows
   one by one */
for (int i = 0; i < N; i++)
{
    /* Check if queen can be placed on board[i][col] */
    if ( isSafe(board, i, col) )
    {
        /* Place this queen in board[i][col] */
        board[i][col] = 1;

        /* recur to place rest of the queens */
        if ( solveNQUtil(board, col + 1) == true )
            return true;

        /* If placing queen in board[i][col] doesn't lead to a solution
           then remove queen from board[i][col] */
        board[i][col] = 0; // BACKTRACK
    }
}

 /* If queen can not be place in any row in this colum col
    then return false */
return false;
}

好的。现在我有一些可以工作的代码,但我主要根据上面的代码修改了我自己的代码,所以我很不稳定:

bool EightQueen(int& numQueen)  {   

if (numQueen == 8)  {
    return true;
}
if (numQueen == 0)  {
    PlaceQueen(0, 0);
    numQueen ++;
    EightQueen(numQueen);
}

int row = 0;

for (row = 0; row <= 7; row ++) {
    if (CheckThis(row, numQueen))   {   //Check if next queen can be  put
        PlaceQueen(row, numQueen);  //Place next queen
        numQueen ++;
        DisplayQueen();
        cout << endl;
        if (EightQueen(numQueen))   {   //Try next queen
            return true;
        }
        ClearQueen(numQueen - 1);
        numQueen --;
    }
}
return false;
}

假设 numQueen 是 5,那么在 for 循环中我们将检查是否可以放置第 6 个皇后。我们知道这对所有行都是不可能的,所以该函数返回 false。我假设它然后“收缩”回它被调用的位置,即当 numQueen 为 4 时。因此调用 ClearQueen(4) 并删除最后一个皇后(第 5 个)。显然 for 循环还没有完成,所以我们将尝试下一行,看看它是否允许进一步开发。即我们检查是否可以将第 5 个皇后放在下一行,如果可以,我们将进一步检查是否可以放置第 6 个皇后,依此类推。

是的,它似乎有效,嗯,嗯,是的。

【问题讨论】:

  • 取决于isSafe,此函数可能总是返回true。
  • 比较booltrue 没有意义。直接使用bool——这就是类型的用途。

标签: c++ n-queens


【解决方案1】:

考虑您的初始董事会:

+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+

当你第一次调用你的函数时,算法会在第 0 列和第 0 行放置一个皇后,因为你用col = 0 调用它并且因为for (int i = 0; i &lt; N; i++) 从 0 开始。你的棋盘变成了

+---+---+---+---+---+---+---+---+
| Q |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+

然后,您使用col = 1 递归调用该函数,因此您将尝试在col=1line=0 放置一个皇后。你得到了一个不可能的位置,因为皇后可以互相占据,所以你继续for (int i = 0; i &lt; N; i++)循环并最终成功地将皇后放置在col=1line=2,你得到这个板:

+---+---+---+---+---+---+---+---+
| Q |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   | Q |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+

现在你继续这样做,每次递增col。最终,你会到达这个图板:

+---+---+---+---+---+---+---+---+
| Q |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   | Q |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   | Q |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   | Q |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   | Q |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   | Q |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   | Q |   |
+---+---+---+---+---+---+---+---+

你有一个问题,因为这个板不承认第 7 列中的皇后位置。你必须回溯。发生的情况是,在尝试了列中的所有位置并且没有找到位置之后,递归函数才到达return false;。当函数返回false时,前面的函数调用会继续执行就行了

if ( solveNQUtil(board, col + 1) == true )

由于调用返回 true,for 循环体的其余部分将被执行,i 将递增,算法将继续尝试位置。将其视为一组巨大的嵌套 for 循环:

for(int i = 0; i < 8; ++i) {
  for(int j = 0; j < 8; ++j) {

    //Snip 6 other fors

    board[0, i] = 1;
    board[1, j] = 1;

    //Snip

    if(isValid(board)) return board;

    //Snip clean up
  }
}

用递归函数调用替换。这说明“回溯”实际上只是让前一个递归级别迭代到下一次尝试。在这种情况下,这意味着尝试一个新的位置,而在其他应用程序中则是尝试下一个生成的移动。

我想你需要明白的是,当你再次调用同一个函数时,之前递归调用的状态并没有丢失。当你到达终点线时

if ( solveNQUtil(board, col + 1) == true )

当前函数的状态仍在堆栈中,并为对solveNQUtil 的新调用创建了一个新的堆栈帧。当该函数返回时,前一个函数可以继续执行,在这种情况下,它会增加它试图将皇后放在哪一行。

希望这会有所帮助。解决这些问题的最佳方法是将您的问题减少到一个较小的域(比如较少数量的皇后)并使用调试器逐步执行。

【讨论】:

  • 非常感谢您的详细解答!我设法弄清楚问题出在哪里:我的大脑继续认为它需要记住先前皇后的位置才能回溯,而实际上它不需要这样做,因为递归内部的简单 for 循环负责处理问题已经——大脑只需要命令我的手把之前的皇后去掉,放到下一行,看看是否可行。但当然,我确信对于下一个递归回溯问题,我需要重新说服我的大脑。
  • @FanZhang 很高兴我能帮上忙!递归是你在某个时候“得到”的东西之一,这对你来说似乎是“Ohhhhh”时刻;)
  • 感谢 pwny 我设法根据上面的代码修改了我的代码,尽管我持怀疑态度,但它仍然有效。我会尝试更多的递归练习来弄湿我的脚。
【解决方案2】:

您的问题的直接答案很简单:您的定位和 循环删除皇后。下一次循环时,你 将尝试下一个位置。

这让我想到了下一点:你说教科书 没有回溯代码,只有递归代码。 递归代码回溯代码。递归时, 函数的每个实例都有自己的完整变量集。 所以在这种情况下,当solveNQUtil被调用时,问题就出现了 已经解决了第一个col - 1 列。这 函数遍历行,每次测试它是否 可以放置一个皇后,如果是,放置它并递归。这 迭代确保将检查所有可能的位置(如果 必要 --- 一旦我们找到解决方案,您的代码就会终止)。

【讨论】:

  • 谢谢我开始看到问题,但我仍然无法弄清楚程序如何自动遍历行。我相信 for 循环在遍历 i (即行)时会起到作用。我会更仔细地阅读代码并在几分钟后回来。
  • 好的,谢谢,我现在解决了问题:算法像树一样工作(没有读过本章但有一些想法),每个皇后被视为一个节点,给你几个节点循环遍历下一个节点的可能位置,如果没有可能的解决方案,您只需删除最后一个节点。并且程序会自动将最后一个节点向下移动,因为当最后一次递归结束时,程序会返回到前一个 for 循环,该循环负责处理最后一个节点的位置。我知道这很混乱,但我想问题是我并不真正理解递归。
  • @FanZhang 是的。回溯有点像一棵树。但是一棵从未真正建成的树。并且遍历行不是自动的;它通过for 循环非常明确地完成。
  • 谢谢,现在我知道我的问题出在哪里了:当我读到递归时,教科书告诉我递归对于某些问题可能更直观,这可能是真的。即当我用手玩8皇后问题时,我的大脑实际上并没有将前一个皇后的位置“保存”到一个“堆栈”中,而是简单地看着前一个皇后并命令我的手向下移动到下一行(看看有没有其他皇后的攻击)。我确信我需要做更多的练习才能熟悉递归(不再有堆栈!)。
【解决方案3】:

您必须记住,有 两个 将皇后移到棋盘上的原因:

  • 它不在安全位置
  • 它处于安全位置,但是当您尝试放置下一个皇后时,递归调用返回 false,这意味着 您已经用尽下一列中的所有可能性 .

您的程序在 Queen 5 处停止,因为它没有考虑第二个条件。正如詹姆斯所说,不需要跟踪位置,每个递归调用都隐含跟踪它必须放置的女王。

尝试想象调用堆栈(实际上,您可以修改程序以生成相同类型的输出):

Queen 1 is safe on row 1
    Queen 2 is safe on row 3
        Queen 3 is safe on row 5
            Queen 4 is safe on row 2
                Queen 5 is safe on row 4
                    No more rows to try for Queen 6. Backtracking...
                Queen 5 is safe on row 8
                    No more rows to try for Queen 6. Backtracking...
                No more rows to try for Queen 5. Backtracking...
            Queen 4 is safe on row 7
                Queen 5 is safe on row 2
                    Queen 6 is safe on row 4
                        Queen 7 is safe on row 6
                           No more rows to try for Queen 8. Backtracking...

每次您回溯时,请意识到您回到了上一个函数调用,处于与您离开它相同的状态。因此,当您到达 Queen 6 并且没有任何可能性时,该函数返回 false,这意味着 您已经完成对 Queen 5 的 solveNQUtil(board, col + 1) 调用。您返回到 Queen 的 for 循环5,接下来会发生的事情是i 增加,你尝试将 Queen 5 放在第 5 行,依此类推......

我建议你玩一下this demo(尝试放置控制:“手动帮助”选项),我们的大脑在视觉上理解事物的能力要好得多。代码太抽象了。

【讨论】:

  • 谢谢米克洛斯·奥伯特。我确实手工玩过 4-皇后问题,最后我发现我的大脑固执地认为我需要为之前的皇后位置保留一个“筹码”。我还发现,虽然我知道当递归结束时它会返回到 for 循环,但我在编程时并不理解它。这恐怕只能通过几天的各种递归问题充斥我的大脑并让它接受这个概念来实现。
  • 我理解你的挣扎。很难看到一个 single 函数并想象它会在不同级别执行并总是返回它应该返回的位置。当一个函数调用一个不同的函数,然后调用另一个另一个函数时,大脑更容易理解调用堆栈的概念,尽管在实践中,它是相同的该死的递归调用!您必须看到递归调用就像任何其他函数调用一样,它与您所在的函数相同的事实并不意味着您“留在原地”。你真的在移动到一个新的状态。
  • 经过几分钟的考虑,我发现我也在为那个 for 循环而苦苦挣扎。这就像照镜子并说服自己迟早可以找到问题的核心并找到解决方案。我不知道该怎么说,但是,好吧,猜猜更多的练习会有所帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-13
  • 2012-07-22
相关资源
最近更新 更多