4

簡単な演習 (例: フィボナッチ) を行ったにもかかわらず、再帰とバックトラックを理解するのに苦労しています。ここで私の「脳の流れ」を紹介させてください。

  1. 私は教科書を読み、現在の位置が次の列に次のクイーンを配置する可能性を排除する場合、バックトラックを使用して前のクイーンを削除できることを知っています. したがって、これは簡単に思えます。私がする必要があるのは、それを削除して、プログラムに次の可能な場所を決定させることだけです。

  2. しばらくすると、プログラムが 6 番目のクィーンで止まっていることがわかったので、単に 5 番目のクィーンを削除すると、プログラムは単純に現在の位置に戻すことがわかりました (つまり、最初の 4 つのクィーンが与えられた場合、5 番目のクィーンは常に同じクィーンに分類されます)。驚くべきことではありません)。そこで、最後の女王の位置を追跡する必要があると考えました。

  3. これは私が困惑したときです。最後のクイーンの位置を追跡する場合 (バックトラックしたときにプログラムがクイーンを同じ場所に配置できないようにするため)、2 つの潜在的な問題があります。

a) 5 番目のクイーンを削除し、次の可能な位置を決定するコードを作成する必要があるとします。これは、現在の位置 (削除される前) を無視することで解決でき、次の行で可能な場所を探し続けます。

b) 以前のすべてのクイーンを追跡する必要がありますか? そうらしい。実際には、1 つのクイーンではなく 2 つのクイーン (またはそれ以上) を削除する必要があるとしましょう。それらの現在の位置をすべて追跡する必要があります。しかし、これは見た目よりもはるかに複雑です。

  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++)

+---+---+---+---+---+---+---+---+
| 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

クイーンをボードの下に移動する理由は2 つあります。

  • 安全な位置にない
  • 安全な位置にありますが、次のクイーンを配置しようとすると、再帰呼び出しがfalseを返しました。これは、次の列のすべての可能性を使い果たしたことを意味します

2 番目の条件を説明できなかったため、プログラムはクイーン 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の呼び出しが完了したことを意味します。Queen 5 のループに戻り、次に起こることは、インクリメントし、クイーン 5 を行 5 に配置しようとします...solveNQUtil(board, col + 1)fori

このデモを試してみることをお勧めします(Placement Control : "Manual With Help" オプションを試してください)。私たちの脳は物事を視覚的に理解するのに優れています。コードが抽象的すぎる。

于 2013-07-29T14:56:11.920 に答える