web-dev-qa-db-ja.com

バックトラッキングと再帰を使用したJavaの数独ソルバー

9x9グリッド用にJava)で数独ソルバーをプログラミングしています。

私には次の方法があります:

  • グリッドの印刷

  • 与えられた値でボードを初期化する

  • 競合のテスト(同じ番号が同じ行または3x3サブグリッドにある場合)

  • 数字を1つずつ配置する方法で、最も多くの作業が必要です。

その方法について詳しく説明する前に、再帰を使用して解決する必要があることと、バックトラックを使用する必要があることを覚えておいてください(例としてここのアプレットを見てください http://www.heimetli.ch/ffh/ simplesudoku.html

また、この数独は、左上から最初の列、2番目の列の順に垂直に下に移動することで解決しています。

これまでのところ、私は次のものを持っています:

public boolean placeNumber(int column){

    if (column == SUDOKU_SIZE){  // we have went through all the columns, game is over

        return true;

    }

    else
    {
        int row=0;  //takes you to the top of the row each time

        while (row < SUDOKU_SIZE)    loops through the column downwards, one by one
        {

            if (puzzle[row][column]==0){  //skips any entries already in there (the given values)

                puzzle[row][column]=1;   //starts with one

                while(conflictsTest(row,column)){   //conflictsTest is the method I wrote, which checks if the given parameters are in conflict with another number

                    puzzle[row][column] += 1;  

                }


           //BACK TRACKING 

                placeNumber(column);      //recursive call

            }
            else{
              row++;                  // row already has a number given, so skip it
            }
        }

        column++;              // move on to second column
        placeNumber(column);

    }
    return false; // no solutions to this puzzle
}

BACKTRACKINGというラベルを付けたのは、コードの残りの部分を実行する必要があると私が信じている場所です。

私は次のようなことを考えました:

  • 値が10の場合、その値をゼロに戻し、行に戻って、その値を1ずつインクリメントします。

そのバックトラックの「戦略」は、いくつかの理由で正確には機能しません。

  1. 前の行が指定された値だった場合はどうなりますか(別名、インクリメントしたりタッチしたりすることは想定されていませんが、代わりに、そこに配置した最後の値に戻ります)

  2. 前の値が9だった場合、それを1増やした場合、現在は10になっていますが、これは機能しません。

誰かが私を助けてくれますか?

24
Carleton U

数独をどのように解決するかはわかりませんが、力ずくの方法を使用したとしても(つまり、あなたが説明しているように聞こえます)、データ構造が適切でないことを考慮する必要があります。

つまり、すべてのセルは単なる数字ではなく、数字のセット(おそらくそこに配置される可能性があります)でなければならないということです。

したがって、指定された数値はシングルトンセットとして表されますが、空の数値は{1,2,3,4,5,6,7,8,9}で初期化できます。そして、目標は、すべてのセルがシングルトンになるまで、非シングルトンセルを減らすことです。

(鉛筆と紙で数独を解くとき、それを解く限り、そこで可能な数字を追跡するために、空白のセルに小さな数字を書くことがよくあります。)

そして、「次の番号を試す」ときは、セットから次の番号を取得します。与えられたセルには次の番号がないため、変更することはできません。このようにして、あなたが説明する困難は(少なくとも少しは)消えます。

------ブルートフォースIS必須)を学習した後、編集します。

あなたの先生は明らかにあなたに再帰の素晴らしさを教えたいと思っています。とても良い!

その場合、どのセルが与えられ、どのセルが与えられないかを知る必要があります。

ここで使用できる特定の簡単な方法は、指定されたセルが定義上1、2、3、4、5、6、7、8、9のいずれかであるため、指定されていないセルに0を配置することです。

次に、再帰的なブルートフォースを機能させる方法について考えてみましょう。

N個の空のセルを持つ数独を解くことを目標としています。 n-1個の空のセルで数独を解く(または解けないことを示す)関数がある場合、このタスクは簡単です:

_let c be some empty cell.
let f be the function that solves a sudoku with one empty cell less.
for i in 1..9
   check if i can be placed in c without conflict
   if not continue with next i
   place i in c
   if f() == SOLVED then return SOLVED
return NOTSOLVABLE
_

この擬似コードは空のセルを選択し、そこに収まるすべての数値を試します。数独には、定義上、単一のソリューションしかないため、次の場合のみがあります。

  • 正しい番号を選びました。次に、f()は残りの解を見つけ、SOLVEDを返します。
  • 間違った番号を選択しました:f()は、セル内のその間違った番号では数独が解けないことを示します。
  • 私たちはすべての番号をチェックしましたが、誰も正しくありませんでした。それから、私たちは自分たちで解決できない数独を手に入れ、これを発信者に知らせます。

言うまでもなく、アルゴリズムは、現在の状態と競合しない数値のみを配置するという前提に基づいています。たとえば、同じ行、列、またはボックスにすでに_9_がある場合、そこに_9_を配置しません。

神秘的でありながら未知の関数f()がどのように見えるかを考えてみると、すでに持っているものとほぼ同じになることがわかります。
まだ検討していない唯一のケースは、空のセルが0個の数独です。つまり、空のセルがなくなった場合は、数独を解決したばかりで、SOLVEDを返したことがわかります。

これは、問題を解決することになっている再帰関数を作成するときの一般的なトリックです。私たちはsolve()を書いていますが、問題はまったく解決可能であることがわかっています。したがって、再帰ごとに問題が何らかの形で解決策に近づくことを確認する限り、今書いている関数をすでに使用できます。最後に、いわゆる基本ケースに到達します。ここでは、さらに再帰することなくソリューションを提供できます。

私たちの場合、数独は解けることがわかっています。さらに、数独には正確に1つの解があることがわかります。空のセルにピースを配置することで、解決策(または解決策がないという診断)に近づき、今書いている関数に新しい小さな問題を再帰的に与えます。基本ケースは「空のセルが0個の数独」で、実際には解決策ですです。

(考えられる解決策がたくさんあると、状況は少し複雑になりますが、次のレッスンに残しておきます。)

7
Ingo

最初に、最適化の提案:セルに入力する数値が同じ行、列、またはミニグリッドにすでに存在するかどうかを確認するときに、を実行する必要はありません。ループかそのようなもの。配列のインデックス付けにより、インスタントチェックを実行できます。

3つの9x9ブール2次元配列を考えてみましょう。

boolean row[9][9], col[9][9], minigrid[9][9]

最初の配列を使用して同じ行に番号が存在するかどうかを確認し、2番目の配列を使用して同じ列に番号が存在するかどうかを確認し、3番目の配列をミニグリッドに使用します。

セルに数値nを入れたいとするとijrow [i] [n-1]がtrueかどうかを確認します。はいの場合、ith 行にはすでにnが含まれています。同様に、col [j] [n-1]およびminigrid [gridnum] [n-1]がtrueであるかどうかを確認します。

ここでgridnumは、数値を挿入するセルが存在するミニグリッドのインデックスです。セルのミニグリッド番号を計算するにはi、j、i&を除算します。 jに3を掛け、前者の積分部分に3を掛け、後者の積分部分に加算します。

これはどのように見えるか:

gridnum = (i/3)*3 + j/3

すべてのiとjのi/3とj/3の値を見ると、これがどのように機能するかがわかります。また、セルに数値を入力する場合は、配列も更新してください。例えば。 row [i] [n-1] = true

わからない部分があればコメントを投稿してください。答えを編集して説明します。

Secondly、これを解決するために再帰とバックトラッキングを使用するのは非常に簡単です。

boolean F( i, j) // invoke this function with i = j = 0
{
If i > 8: return true // solved

for n in 1..9
 {
 check if n exists in row, column, or mini grid by the method I described

 if it does: pass ( skip this iteration )

 if it doesn't
  {
   grid[i][j] = n
   update row[][], col[][], minigrid[][]

   if F( if j is 8 then i+1 else i, if j is 8 then 0 else j+1 ) return true // solved
  }
 }
 return false // no number could be entered in cell i,j
}
4
Rushil Paul

現在の行と列の両方を再帰的メソッドに渡してから、そのセルに許可されているすべての番号を見つけることをお勧めします。許可されている番号ごとに、次の列(または最後の列の場合は次の行)のメソッドを再帰的に呼び出し、移動が進んだ場合は元に戻しますデッドトラックへ

public boolean fillCell(int r, int c) {
    if last row and last cell {
        //report success
        return true
    }
    for i 1 to 9 {
        if can place i on r, c {
            board[r][c] = i
            if !fillCell(next empty row, next empty column) { //DONT change r or c here or you will not be able to undo the move
                board[r][c] = 0
            }
            /*
            else {
                return true; //returning true here will make it stop after 1 solution is found, doing nothing will keep looking for other solutions also
            }
            */

        }
    }
    return false        
}

各セルをチェックし、解決策が見つからない場合は再帰ステップに戻ります。

詳細:次のセルに移動します。値x == 0の場合、x + 1が有効かどうかを確認し、trueの場合は、次の可能なセルでメソッドを再帰的に呼び出して、次のセルに移動します。番号が有効でない場合はx + 2などを確認し、有効な番号がない場合はfalseを返し、前の呼び出しでx +1の手順を繰り返します。内部に番号が含まれるセルにヒットした場合は、再帰を呼び出さずに直接次のセルに移動するため、事前に入力されたセルにフラグを立てる必要はありません。

擬似コード:

fillcell cell
 while cell is not 0
  cell = next cell
 while cell value < 10
  increase cell value by one
  if cell is valid 
    if fillcell next cell is true
      return true
return false

これが正しいかどうかはわかりませんが、アイデアが示されているはずです。

0
morja

役立つかもしれないいくつかのアイデア(再帰とバックトラックに関して)

//some attributes you might need for storing e.g. the configuration to track back to.

boolean legal(Configuration configuration) {

}

int partSolution(Configuration configuration) {
  if (legal(configuration))
    return partSolution(nextConfiguration())
  else
    return partSolution(previousConfiguration())    
}

Configuration nextConfiguration() {
 //based on the current configuration and the previous tried ones,
 //return the next possible configuration:
 //next number to enter, next cell to visit
}

Configuration previousConfiguration() {
 //backtrack
}

solve () {
  call partSolution with start configuration while partSolution < 9x9
}

入力された数値と、入力されたサイズや#numbersなどの他の属性を含むグリッドを保持する構成クラスを記述し、他に何が必要かを考えます。

0
niklas