ナイト巡回問題とは、チェスを使った数学的パズルの一種。チェスで使う駒のひとつナイトをチェス盤の上を動かし、すべてのマスを1回ずつ通って最初の場所に戻ってくるようにするのはどうすればよいかという問題です。

ナイトは将棋の桂馬のような動きをしますが、桂馬と違うのは前方の2箇所ではなく全方向8箇所に移動できることです。将棋もチェスも知らんがなという方には申し訳ありませんが、これは知っているという前提で話を進めます。

バックトラック法で解く

一方、バックトラック法とは問題の解を見つけるために解の候補をすべて調べることを組織的にかつ効率よく行うための技法である。簡単にいえば「とりあえずやってみる。失敗したら後戻りしてやり直す」ということです。ウジウジ考えても分からないものはわからないので、とりあえずやってみろ、失敗したらそのとき考えよ(ちょっと違うか?)ということです。

最初はナイトは隅におきます。ここから桂馬飛びに移動します。移動できる部分は2箇所ありますが、ここは適当に選びます。そして移動したあとまた桂馬飛びで移動できる場所が複数あるのですが、ここもどれか一つを適当に選んで次に進みます。これだとどこかでどこへもいけなくなってしまうのですが、この場合はひとつ戻って別の候補を探します。ひとつ戻っただけでは別の候補が見つからない場合はさらにもうひとつ戻ります。

Positionクラス

最初にPositionクラスを作成します。これは座標を管理するためのクラスです。

Nodeクラス

次にNodeクラスを作成します。Nodeクラスの役割はナイトの現在位置、次の移動先を探して新しいNodeクラスのインスタンスを生成することです。次の移動先を探そうとしても見つからない場合はバックしなければならないので自分自身を作成したNodeへの参照もフィールド変数に含めます。

フィールド変数は以下のとおりです。

コンストラクタは以下のようになります。引数がたくさんありますが、上記のフィールド変数を初期化するために必要なデータです。

ナイトの最初の位置から移動できる座標のリストを取得する処理を示します。

現在位置から移動できる座標のリストを取得する

ナイトが現在位置から移動できる座標のリストを取得するための処理を示します。引数付きのGetNextsメソッドを2回使っています。現在位置から移動できる座標のリストを取得したうえで、さらにその位置から移動できる座標のリストを取得し、リストの要素数が少ない順にソートしています。これが重要な処理らしく、これをやらないと実行時間が極端に長くなり、いつまで経っても終わりません。候補が少ないものから調べて無駄に多く調べることを避けています。

これはソートの処理をしない場合の動画です。上記の動画は比較的簡単に解が見つかりましたが、この動画ではいつまでたっても終わりそうにありません。

ナイトが最初の場所に戻れないものは却下

ナイトがすべてのマス目を通って最初の場所に戻ってくるためには最後のとき以外はナイトが最初に移動できる位置のどれか一つをあけておかなければなりません。

移動の処理

移動先の座標のリストを取得することができたら、そのなかからひとつを選び、実際に移動します。そのときはリストの先頭から選び、先頭の要素はRemoveします。そうしないとなんども同じ場所を調べるループから抜けられなくなります。

引数なしのGetNextsメソッドを実行してNextsに移動先のリストが格納されるのは最初に実行したときだけです。2回目以降はなにもおきません。これもそのようにしておかないとなんども同じ場所を調べるループにハマってしまいます。

もしNextsが空のときはこれ以上前には進めないのでバックすることになります。このメソッドがnullを返す場合としてバックバックを繰り返して最初の位置に戻ってしまった場合とすべてのマス目を通って最初の位置に戻ってこれた場合があります。

最初の位置に戻ってしまった場合は解は存在しないことになります。そうでない場合はこれが求めている解のひとつを得たことになります。

結果取得用のメソッド

GetMapメソッドも作りました。使用目的としては処理が終わったときにナイトはどのような順番で移動したかを調べたり、途中経過がどうなっているかを調べることが考えられます。

KnightTourクラス

次にナイト巡回問題を解くためのKnightTourクラスをみてみましょう。フィールド変数は現在ナイトが存在するNodeを知るためのCurNodeと何回Node.GetNextメソッドを実行したかをカウントするCountがあります。

コンストラクタの引数はチェス盤の縦横のマス目の数、ナイトが最初に存在する座標です。

MoveNextメソッドはCurNodeからGetNextメソッドを呼び出し、戻り値をCurNodeに格納し直します。GetCurNodeMapメソッドはCurNode.GetMapメソッドの戻り値である二次元配列を返します。

GetResultメソッドはMoveNextメソッドをnullを返すまで実行し続けて、最後にCurNode.GetMapメソッドの戻り値を返します。

WindowsFormsアプリケーションで使ってみる

これだけあればバックトラック法でナイト巡回問題を解くことができます。最初はボタンを押したらKnightTour.MoveNextメソッドが1回だけ実行され、そのときにナイトがスタート位置からどのように移動しているかをRichTextBoxに番号で表示させるものをつくります。

GetMapTextメソッドは二次元配列をテキストに変換してRichTextBoxに表示させる文字列を取得するためのものです。

何回かボタンを押していると案外続くことがわかります。そして次に移動できる場所がなくなるとひとつバックして別の解を探そうとしていることがわかります。

これはそれを可視化した動画です。Labelを使って途中経過を表示させているので、ここに掲載しているものとは別のコードで動かしています。

動作をひとつずつ確認するのではなく、すぐに解を表示させる方法も用意しました。処理は一瞬で終わりますが、Nodeクラスの引数なしのGetNextsメソッド内でのソート処理をしていないととんでもなく時間がかかります。