4

我很难理解递归和回溯,尽管我做了一些简单的练习(例如斐波那契)。所以请允许我在这里介绍我的“脑流”:

  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 个皇后,依此类推。

是的,它似乎工作,嗯,嗯,是的。

4

3 回答 3

8

考虑您的初始董事会:

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

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

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

然后,您使用 递归调用该函数col = 1,因此您将尝试在col=1和放置一个皇后line=0。你得到一个不可能的位置,因为皇后可以互相占据,所以你继续for (int i = 0; i < 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。当该函数返回时,前一个函数可以继续执行,在这种情况下,它会增加它试图将皇后放在哪一行。

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

于 2013-07-29T14:33:15.980 回答
2

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

  • 它不在一个安全的位置
  • 它处于安全位置,但是当您尝试放置下一个皇后时,递归调用返回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,这意味着你已经完成solveNQUtil(board, col + 1)对 Queen 5 的调用。你回到了forQueen 5 的循环中,接下来发生的事情就是i得到递增,然后您尝试将 Queen 5 放在第 5 行,依此类推...

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

于 2013-07-29T14:56:11.920 回答
2

你的问题的直接答案很简单:你在一个循环中定位和移除女王。下一次循环时,您将尝试下一个位置。

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

于 2013-07-29T14:13:39.320 回答